Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ea1c06d91 | ||
|
|
5d6b46a914 | ||
|
|
77ff5b473b | ||
|
|
ac8b06b6bf | ||
|
|
dc12a0ff97 | ||
|
|
c329d5e69f | ||
|
|
009ac7eec2 | ||
|
|
2bf3e6ce13 | ||
|
|
babcd96fb6 | ||
|
|
bf6c89e263 | ||
|
|
72a02df646 | ||
|
|
462101dbf5 | ||
|
|
00f1463e94 | ||
|
|
9135e915ab | ||
|
|
d7242f1c70 | ||
|
|
fa4ea0640d | ||
|
|
8f94332d59 | ||
|
|
337ec963b7 | ||
|
|
cf5a395b29 | ||
|
|
e2d597401c | ||
|
|
3ae500ff68 | ||
|
|
779eadd787 | ||
|
|
110475a973 | ||
|
|
74469c2b61 | ||
|
|
d5238ac82f | ||
|
|
c861d95d69 |
@@ -211,7 +211,7 @@ android {
|
||||
reset()
|
||||
enable getEnvOrConfig('ANDROID_ABI_SPLIT').toBoolean()
|
||||
include getEnvOrConfig('ANDROID_ABI_INCLUDE').split(";")
|
||||
universalApk false
|
||||
universalApk getEnvOrConfig('ORG_GRADLE_PROJECT_universalApk').toBoolean()
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
/* Options section can't access functions in objects. */
|
||||
def isPRBuild = utils.isPRBuild()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
agent { label 'linux' }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
/* Options section can't access functions in objects. */
|
||||
def isPRBuild = utils.isPRBuild()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
/* Options section can't access functions in objects. */
|
||||
def isPRBuild = utils.isPRBuild()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
agent { label 'macos' }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
agent { label params.AGENT_LABEL }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
agent { label 'linux' }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.8'
|
||||
library 'status-jenkins-lib@v1.9.10'
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
# 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
|
||||
@@ -89,6 +90,7 @@ in stdenv.mkDerivation rec {
|
||||
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";
|
||||
|
||||
|
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 306 KiB |
@@ -91,14 +91,14 @@
|
||||
: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 [input-segments state chat-id new-text]} (transfer-mention-result result)]
|
||||
{:db (-> db
|
||||
(assoc-in [:chats/mentions chat-id :mentions] state)
|
||||
(assoc-in [:chat/inputs-with-mentions chat-id] input-segments))}))
|
||||
(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]}
|
||||
@@ -118,49 +118,32 @@
|
||||
{: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]} (transfer-mention-result result)]
|
||||
{:db (-> db
|
||||
(assoc-in [:chats/mention-suggestions chat-id] mentionable-users)
|
||||
(assoc-in [:chats/mentions chat-id :mentions] state)
|
||||
(assoc-in [:chat/inputs-with-mentions chat-id] input-segments))}))
|
||||
(let [{:keys [chat-id mentionable-users]} (transfer-mention-result result)]
|
||||
{:db (assoc-in db [:chats/mention-suggestions chat-id] mentionable-users)}))
|
||||
|
||||
(rf/defn on-select-mention-success
|
||||
{:events [:mention/on-select-mention-success]}
|
||||
[{:keys [db] :as cofx} result primary-name match searched-text public-key]
|
||||
[{: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 state input-segments]} (transfer-mention-result result)]
|
||||
{:db (-> db
|
||||
(assoc-in [:chats/mentions chat-id :mentions] state)
|
||||
(assoc-in [:chat/inputs-with-mentions chat-id] input-segments)
|
||||
(assoc-in [:chats/mention-suggestions chat-id] nil))
|
||||
:dispatch [:chat.ui/set-chat-input-text new-text chat-id]}))
|
||||
|
||||
(rf/defn clear-suggestions
|
||||
[{:keys [db]}]
|
||||
(log/debug "[mentions] clear suggestions")
|
||||
(let [chat-id (:current-chat-id db)]
|
||||
{:db (update db :chats/mention-suggestions dissoc chat-id)}))
|
||||
(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] :as cofx}]
|
||||
(log/debug "[mentions] clear mentions")
|
||||
[{:keys [db]}]
|
||||
(let [chat-id (:current-chat-id db)]
|
||||
(rf/merge
|
||||
cofx
|
||||
{:db (-> db
|
||||
(update-in [:chats/mentions chat-id] dissoc :mentions)
|
||||
(update :chat/inputs-with-mentions dissoc chat-id))
|
||||
:json-rpc/call [{:method "wakuext_chatMentionClearMentions"
|
||||
:params [chat-id]
|
||||
:on-success #()
|
||||
:on-error #(log/error "Error while calling wakuext_chatMentionClearMentions"
|
||||
{:error %})}]}
|
||||
(clear-suggestions))))
|
||||
{: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]}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[: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?]]
|
||||
[:on-button-press {:optional true} [:maybe fn?]]]]]
|
||||
:any])
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
(ns quo.components.banners.alert-banner.style
|
||||
(:require [quo.foundations.colors :as colors]))
|
||||
|
||||
(def container
|
||||
{:flex-direction :row
|
||||
:align-items :center
|
||||
:height 50
|
||||
:padding-horizontal 20
|
||||
:padding-vertical 12})
|
||||
(defn container
|
||||
[container-style]
|
||||
(merge
|
||||
{:flex-direction :row
|
||||
:align-items :center
|
||||
:height 50
|
||||
:padding-horizontal 20
|
||||
:padding-vertical 12}
|
||||
container-style))
|
||||
|
||||
(defn label
|
||||
[theme]
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
[schema.core :as schema]))
|
||||
|
||||
(defn- view-internal
|
||||
[{:keys [action? text button-text on-button-press]}]
|
||||
[{:keys [action? text button-text container-style on-button-press]}]
|
||||
(let [theme (quo.theme/use-theme)]
|
||||
[rn/view
|
||||
{:accessibility-label :alert-banner}
|
||||
[linear-gradient/linear-gradient
|
||||
{:style style/container
|
||||
{:style (style/container container-style)
|
||||
:start {:x 0 :y 0}
|
||||
:end {:x 0 :y 1}
|
||||
:colors [(colors/theme-colors
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
[:subtitle-type {:optional true} [:maybe [:enum :default :icon :network :account :editable]]]
|
||||
[:size {:optional true} [:maybe [:enum :default :small :large]]]
|
||||
[:title :string]
|
||||
[:subtitle {:optional true} [:maybe :string]]
|
||||
[:subtitle {:optional true} [:maybe [:or :string :double]]]
|
||||
[:custom-subtitle {:optional true} [:maybe fn?]]
|
||||
[:icon {:optional true} [:maybe :keyword]]
|
||||
[:emoji {:optional true} [:maybe :string]]
|
||||
|
||||
@@ -60,8 +60,12 @@
|
||||
shrinkable? (assoc :flex-shrink 1)))
|
||||
|
||||
(defn text
|
||||
[theme]
|
||||
{:color (colors/theme-colors colors/neutral-100 colors/white theme)})
|
||||
([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]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(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]
|
||||
@@ -17,15 +18,16 @@
|
||||
[schema.core :as schema]))
|
||||
|
||||
(defn- tag-skeleton
|
||||
[{:keys [size text theme shrinkable?]
|
||||
:or {size 24
|
||||
theme (quo.theme/use-theme)}}
|
||||
[{:keys [size text theme shrinkable? gray-text?]
|
||||
:or {size 24
|
||||
gray-text? false
|
||||
theme (quo.theme/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)
|
||||
{:style (style/text theme gray-text?)
|
||||
:weight :medium
|
||||
:size (if (= size 24) :paragraph-2 :paragraph-1)
|
||||
:number-of-lines 1
|
||||
@@ -94,92 +96,107 @@
|
||||
state :default
|
||||
theme (quo.theme/use-theme)}
|
||||
:as props}]
|
||||
[rn/view {:style (merge {: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
|
||||
:ring? false
|
||||
:customization-color customization-color}]]
|
||||
(let [[image-error? set-image-error] (rn/use-state false)]
|
||||
[rn/view {:style (merge {: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
|
||||
:ring? false
|
||||
:customization-color customization-color}]]
|
||||
|
||||
:multiuser
|
||||
[preview-list/view {:type :user :size :size-20}
|
||||
users]
|
||||
:multiuser
|
||||
[preview-list/view {:type :user :size :size-20}
|
||||
users]
|
||||
|
||||
:multinetwork
|
||||
[preview-list/view {:type :network :size :size-20}
|
||||
networks]
|
||||
: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}]]]
|
||||
: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/custom-color customization-color 50)}]]
|
||||
: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/custom-color customization-color 50)}]]
|
||||
|
||||
(:channel :community)
|
||||
[communities-tag (assoc props :channel? (= type :channel))]
|
||||
(: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)}]]
|
||||
: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}]]
|
||||
:network
|
||||
[tag-skeleton {:theme theme :size size :text network-name}
|
||||
[rn/image {:style (style/circle-logo size) :source network-logo}]]
|
||||
|
||||
:collectible
|
||||
[tag-skeleton
|
||||
{:theme theme
|
||||
:size size
|
||||
:text (str collectible-name " #" collectible-number)
|
||||
:shrinkable? true}
|
||||
[rn/image {:style (style/rounded-logo size) :source collectible}]]
|
||||
: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)}]]
|
||||
: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]
|
||||
:address
|
||||
[address-tag props]
|
||||
|
||||
:icon
|
||||
[icon-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}]]
|
||||
: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}]]
|
||||
:dapp
|
||||
[tag-skeleton {:theme theme :size size :text dapp-name}
|
||||
[rn/image {:style (style/circle-logo size) :source dapp-logo}]]
|
||||
|
||||
nil)]])
|
||||
nil)]]))
|
||||
|
||||
(def view (schema/instrument #'view-internal component-schema/?schema))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[: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])
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
(defn- view-internal
|
||||
[{:keys [status token-value token-symbol
|
||||
container-style button-props]
|
||||
container-style button-props show-view-button?]
|
||||
:as props}]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
customization-color (or (:customization-color props) :blue)
|
||||
@@ -56,7 +56,9 @@
|
||||
(i18n/label (status-message status)
|
||||
{:amount token-value
|
||||
:symbol token-symbol})]]
|
||||
(when button-props
|
||||
(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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(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
|
||||
@@ -11,11 +12,13 @@
|
||||
(colors/theme-colors colors/neutral-5 colors/neutral-90 theme))
|
||||
|
||||
(defn content
|
||||
[theme]
|
||||
{:border-width 1
|
||||
:border-radius 16
|
||||
:border-color (border-color theme)
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-95 theme)})
|
||||
[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?]
|
||||
|
||||
@@ -19,13 +19,15 @@
|
||||
[:props
|
||||
[:map {:closed true}
|
||||
[:type {:optional true} [:maybe [:enum :pay :receive]]]
|
||||
[:status {:optional true} [:maybe [:enum :default :disabled :loading]]]
|
||||
[: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]]
|
||||
@@ -33,6 +35,7 @@
|
||||
[: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?]]
|
||||
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
|
||||
@@ -41,13 +44,14 @@
|
||||
|
||||
(defn view-internal
|
||||
[{:keys [type status token value fiat-value show-approval-label? error? network-tag-props
|
||||
approval-label-props default-value enable-swap?
|
||||
approval-label-props default-value auto-focus? input-disabled? enable-swap?
|
||||
currency-symbol on-change-text show-keyboard?
|
||||
container-style on-swap-press on-token-press on-max-press]}]
|
||||
container-style on-swap-press on-token-press on-max-press on-input-focus]}]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
pay? (= type :pay)
|
||||
disabled? (= status :disabled)
|
||||
loading? (= status :loading)
|
||||
typing? (= status :typing)
|
||||
controlled-input? (some? value)
|
||||
input-ref (rn/use-ref-atom nil)
|
||||
set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref)) [])
|
||||
@@ -58,7 +62,7 @@
|
||||
[rn/view
|
||||
{:style container-style
|
||||
:accessibility-label :swap-input}
|
||||
[rn/view {:style (style/content theme)}
|
||||
[rn/view {:style (style/content typing? theme)}
|
||||
[rn/view
|
||||
{:style (style/row-1 loading?)}
|
||||
[rn/pressable {:on-press on-token-press}
|
||||
@@ -78,7 +82,9 @@
|
||||
colors/neutral-50
|
||||
theme)
|
||||
:keyboard-type :numeric
|
||||
:auto-focus true
|
||||
:editable (not input-disabled?)
|
||||
:auto-focus auto-focus?
|
||||
:on-focus on-input-focus
|
||||
:on-change-text on-change-text
|
||||
:show-soft-input-on-focus show-keyboard?
|
||||
:default-value default-value
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
[state]
|
||||
(:value state))
|
||||
|
||||
(defn numeric-value
|
||||
(defn value-numeric
|
||||
[state]
|
||||
(or (parse-double (input-value state)) 0))
|
||||
|
||||
(defn value-bn
|
||||
[state]
|
||||
(money/bignumber (input-value state)))
|
||||
|
||||
@@ -27,28 +31,32 @@
|
||||
(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]
|
||||
(let [num-value (numeric-value state)]
|
||||
(and
|
||||
(upper-limit state)
|
||||
(when (money/bignumber? num-value)
|
||||
(money/greater-than (numeric-value state) (upper-limit 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]
|
||||
(let [num-value (numeric-value state)]
|
||||
(and
|
||||
(lower-limit state)
|
||||
(when (money/bignumber? num-value)
|
||||
(money/less-than (numeric-value state) (lower-limit 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]
|
||||
@@ -62,7 +70,7 @@
|
||||
(assoc :value value)
|
||||
recheck-errorness))
|
||||
|
||||
(defn set-numeric-value
|
||||
(defn set-value-numeric
|
||||
[state value]
|
||||
(set-input-value state (str value)))
|
||||
|
||||
@@ -81,11 +89,11 @@
|
||||
|
||||
(defn increase
|
||||
[state]
|
||||
(set-input-value state (str (money/add (numeric-value state) 1))))
|
||||
(set-input-value state (str (money/add (value-bn state) 1))))
|
||||
|
||||
(defn decrease
|
||||
[state]
|
||||
(set-input-value state (str (money/add (numeric-value state) -1))))
|
||||
(set-input-value state (str (money/add (value-bn state) -1))))
|
||||
|
||||
(def ^:private not-digits-or-dot-pattern
|
||||
#"[^0-9+\.]")
|
||||
@@ -122,10 +130,12 @@
|
||||
state))
|
||||
|
||||
(defn delete-last
|
||||
[state]
|
||||
(let [value (input-value state)
|
||||
new-value (subs value 0 (dec (count value)))]
|
||||
(set-input-value state new-value)))
|
||||
([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]
|
||||
@@ -133,7 +143,7 @@
|
||||
|
||||
(defn empty-value?
|
||||
[state]
|
||||
(string/blank? (:value state)))
|
||||
(or (string/blank? (:value state)) (<= (value-numeric state) 0)))
|
||||
|
||||
(defn- fiat->crypto
|
||||
[value conversion-rate]
|
||||
|
||||
@@ -200,13 +200,13 @@
|
||||
(.remove keyboard-hide-listener))))
|
||||
|
||||
(defn screen
|
||||
[{:keys [initial-insets title keypair navigation-icon recovering-keypair? render-controls]}]
|
||||
(let [{navigation-bar-top :top} initial-insets
|
||||
banner-offset (rf/sub [:alert-banners/top-margin])]
|
||||
[{:keys [title keypair navigation-icon recovering-keypair? render-controls]}]
|
||||
(let [[insets _] (rn/use-state (safe-area/get-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 navigation-bar-top
|
||||
{:margin-top (:top insets)
|
||||
:background :blur
|
||||
:icon-name (or navigation-icon
|
||||
(if recovering-keypair? :i/close :i/arrow-left))
|
||||
@@ -216,15 +216,14 @@
|
||||
:keypair keypair
|
||||
:render-controls render-controls
|
||||
:banner-offset banner-offset
|
||||
:initial-insets initial-insets
|
||||
:initial-insets insets
|
||||
:recovering-keypair? recovering-keypair?}]]]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [insets (safe-area/get-insets)]
|
||||
(fn []
|
||||
(let [{:keys [recovering-keypair?]} (rf/sub [:get-screen-params])]
|
||||
[screen
|
||||
{:title (i18n/label :t/use-recovery-phrase)
|
||||
:initial-insets insets
|
||||
:recovering-keypair? recovering-keypair?}]))))
|
||||
(let [{:keys [recovering-keypair?]} (rf/sub [:get-screen-params])]
|
||||
(rn/use-unmount
|
||||
#(rf/dispatch [:onboarding/clear-navigated-to-enter-seed-phrase-from-screen]))
|
||||
[screen
|
||||
{:title (i18n/label :t/use-recovery-phrase)
|
||||
:recovering-keypair? recovering-keypair?}]))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
(: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]
|
||||
@@ -89,6 +90,17 @@
|
||||
(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]
|
||||
|
||||
@@ -551,6 +551,7 @@
|
||||
(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 multi-transaction-type-send 0)
|
||||
(def ^:const multi-transaction-type-approve 1)
|
||||
@@ -564,6 +565,7 @@
|
||||
(def ^:const bridge-name-erc-721-transfer "ERC721Transfer")
|
||||
(def ^:const bridge-name-erc-1155-transfer "ERC1155Transfer")
|
||||
(def ^:const bridge-name-hop "Hop")
|
||||
(def ^:const bridge-name-paraswap "Paraswap")
|
||||
|
||||
(def ^:const bridge-assets #{"ETH" "USDT" "USDC" "DAI"})
|
||||
|
||||
@@ -590,9 +592,20 @@
|
||||
(def ^:const default-slippage 0.5)
|
||||
(def ^:const max-recommended-slippage 5)
|
||||
(def ^:const max-slippage-decimal-places 2)
|
||||
(def ^:const swap-default-provider
|
||||
(def ^:const swap-provider-paraswap
|
||||
{:name :paraswap
|
||||
:full-name "Paraswap"
|
||||
:color :blue
|
||||
:contract-address "0xdef171fe48cf0115b1d80b88dc8eab59176fee57"
|
||||
:terms-and-conditions-url "https://files.paraswap.io/tos_v4.pdf"})
|
||||
(def ^:const swap-providers
|
||||
{:paraswap swap-provider-paraswap})
|
||||
|
||||
(def ^:const token-for-fees-symbol "ETH")
|
||||
|
||||
(def ^:const transaction-status-success "Success")
|
||||
(def ^:const transaction-status-pending "Pending")
|
||||
(def ^:const transaction-status-failed "Failed")
|
||||
|
||||
(def ^:const min-token-decimals-to-display 6)
|
||||
(def ^:const swap-proposal-refresh-interval-ms 15000)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
item])))))
|
||||
|
||||
(defn add-manage-members
|
||||
[{:keys [scroll-enabled? on-scroll]}]
|
||||
[{:keys [on-scroll]}]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
selected-participants (rf/sub [:group-chat/selected-participants])
|
||||
deselected-members (rf/sub [:group-chat/deselected-members])
|
||||
@@ -74,7 +74,6 @@
|
||||
(i18n/label (if admin? :t/manage-members :t/add-members))]
|
||||
[gesture/section-list
|
||||
{:key-fn :title
|
||||
:scroll-enabled @scroll-enabled?
|
||||
:on-scroll on-scroll
|
||||
:sticky-section-headers-enabled false
|
||||
:sections (rf/sub [:contacts/grouped-by-first-letter])
|
||||
|
||||
@@ -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 :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}])
|
||||
@@ -7,26 +7,14 @@
|
||||
{:height constants/actions-container-height
|
||||
:justify-content :space-between
|
||||
:align-items :center
|
||||
:z-index 2
|
||||
:flex-direction :row})
|
||||
|
||||
(defn send-button
|
||||
[opacity z-index]
|
||||
[opacity]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity}
|
||||
{:position :absolute
|
||||
:right 0
|
||||
:z-index z-index
|
||||
:padding-vertical 3
|
||||
:padding-left 2}))
|
||||
|
||||
(defn record-audio-container
|
||||
[]
|
||||
{:align-items :center
|
||||
:background-color :transparent
|
||||
:flex-direction :row
|
||||
:position :absolute
|
||||
:left -20
|
||||
:right -20
|
||||
:bottom 0
|
||||
:height constants/composer-default-height})
|
||||
|
||||
@@ -2,246 +2,60 @@
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[react-native.permissions :as permissions]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.common.alert.effects :as alert.effects]
|
||||
[status-im.common.device-permissions :as device-permissions]
|
||||
[status-im.config :as config]
|
||||
[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]
|
||||
[status-im.contexts.chat.messenger.composer.constants :as comp-constants]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn send-message
|
||||
"Minimize composer, animate-out background overlay, clear input and flush state"
|
||||
[{:keys [sending-images? sending-links?]}
|
||||
{:keys [text-value maximized?]}
|
||||
{:keys [height saved-height last-height opacity background-y]}
|
||||
window-height
|
||||
edit]
|
||||
(reanimated/animate height comp-constants/input-height)
|
||||
(reanimated/set-shared-value saved-height comp-constants/input-height)
|
||||
(reanimated/set-shared-value last-height comp-constants/input-height)
|
||||
(reanimated/animate opacity 0)
|
||||
(js/setTimeout #(reanimated/set-shared-value background-y
|
||||
(- window-height))
|
||||
300)
|
||||
[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-input-maximized false])
|
||||
(rf/dispatch [:chat.ui/set-input-content-height comp-constants/input-height])
|
||||
(rf/dispatch [:chat.ui/set-chat-input-text nil])
|
||||
(reset! maximized? false)
|
||||
(reset! text-value "")
|
||||
(reset! sending-links? false)
|
||||
(reset! sending-images? false)
|
||||
(when-not (some? edit)
|
||||
(rf/dispatch [:chat.ui/scroll-to-bottom])))
|
||||
|
||||
(defn f-send-button
|
||||
[props state animations window-height images? btn-opacity z-index edit]
|
||||
(let [{:keys [text-value]} state
|
||||
(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]))]
|
||||
chat-id]))
|
||||
on-press (rn/use-callback #(send-message input-ref edit btn-opacity) [edit])]
|
||||
(rn/use-effect (fn []
|
||||
;; Handle send button opacity animation and z-index when input content changes
|
||||
(if (or (seq @text-value) images?)
|
||||
(when (or (not= @z-index 1) (not= (reanimated/get-shared-value btn-opacity) 1))
|
||||
(reset! z-index 1)
|
||||
(if (or (seq input-text) images?)
|
||||
(when (not= (reanimated/get-shared-value btn-opacity) 1)
|
||||
(js/setTimeout #(reanimated/animate btn-opacity 1) 50))
|
||||
(when (or (not= @z-index 0) (not= (reanimated/get-shared-value btn-opacity) 0))
|
||||
(reanimated/animate btn-opacity 0)
|
||||
(js/setTimeout #(when (and (empty? @text-value) (not images?))
|
||||
(reset! z-index 0))
|
||||
300))))
|
||||
[(and (empty? @text-value) (not images?))])
|
||||
(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 @z-index)}
|
||||
{: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 #(send-message props state animations window-height edit)}
|
||||
:on-press on-press}
|
||||
:i/arrow-up]]))
|
||||
|
||||
(defn send-button
|
||||
[props {:keys [text-value] :as state} animations window-height images? edit btn-opacity]
|
||||
(let [z-index (reagent/atom (if (and (empty? @text-value) (not images?)) 0 1))]
|
||||
[:f> f-send-button props state animations window-height images? btn-opacity z-index edit]))
|
||||
|
||||
(defn disabled-audio-button
|
||||
[opacity]
|
||||
[reanimated/view {:style (reanimated/apply-animations-to-style {:opacity opacity} {})}
|
||||
[quo/composer-button
|
||||
{:on-press (fn []
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(rn/dismiss-keyboard!)
|
||||
(js/alert "to be implemented"))
|
||||
:icon :i/audio}]])
|
||||
|
||||
(defn audio-button
|
||||
[{:keys [record-reset-fn input-ref]}
|
||||
{:keys [record-permission? recording? gesture-enabled? focused?]}]
|
||||
(let [audio (rf/sub [:chats/sending-audio])]
|
||||
[rn/view
|
||||
{:style (style/record-audio-container)
|
||||
:pointer-events :box-none}
|
||||
[quo/record-audio
|
||||
{:record-audio-permission-granted @record-permission?
|
||||
:on-init (fn [reset-fn]
|
||||
(reset! record-reset-fn reset-fn))
|
||||
:on-start-recording (fn []
|
||||
(rf/dispatch [:chat.ui/set-recording true])
|
||||
(reset! recording? true)
|
||||
(reset! gesture-enabled? false))
|
||||
:audio-file audio
|
||||
:on-lock (fn []
|
||||
(rf/dispatch [:chat.ui/set-recording false]))
|
||||
:on-reviewing-audio (fn [file]
|
||||
(rf/dispatch [:chat.ui/set-recording false])
|
||||
(rf/dispatch [:chat.ui/set-input-audio file]))
|
||||
:on-send (fn [{:keys [file-path duration]}]
|
||||
(rf/dispatch [:chat.ui/set-recording false])
|
||||
(reset! recording? false)
|
||||
(reset! gesture-enabled? true)
|
||||
(rf/dispatch [:chat/send-audio file-path duration])
|
||||
(when @focused?
|
||||
(js/setTimeout #(when @input-ref (.focus ^js @input-ref))
|
||||
300))
|
||||
(rf/dispatch [:chat.ui/set-input-audio nil]))
|
||||
:on-cancel (fn []
|
||||
(when @recording?
|
||||
(rf/dispatch [:chat.ui/set-recording false])
|
||||
(reset! recording? false)
|
||||
(reset! gesture-enabled? true)
|
||||
(when @focused?
|
||||
(js/setTimeout #(when @input-ref
|
||||
(.focus ^js @input-ref))
|
||||
300))
|
||||
(rf/dispatch [:chat.ui/set-input-audio nil])))
|
||||
:on-check-audio-permissions (fn []
|
||||
(permissions/permission-granted?
|
||||
:record-audio
|
||||
#(reset! record-permission? %)
|
||||
#(reset! record-permission? false)))
|
||||
:on-request-record-audio-permission (fn []
|
||||
(rf/dispatch
|
||||
[:request-permissions
|
||||
{:permissions [:record-audio]
|
||||
:on-allowed
|
||||
#(reset! record-permission? true)
|
||||
:on-denied
|
||||
#(js/setTimeout
|
||||
(fn []
|
||||
(alert.effects/show-popup
|
||||
(i18n/label :t/audio-recorder-error)
|
||||
(i18n/label
|
||||
:t/audio-recorder-permissions-error)
|
||||
nil
|
||||
{:text (i18n/label :t/settings)
|
||||
:accessibility-label :settings-button
|
||||
:onPress (fn [] (permissions/open-settings))}))
|
||||
50)}]))
|
||||
:max-duration-ms constants/audio-max-duration-ms}]]))
|
||||
|
||||
(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 :camera-screen]))))
|
||||
|
||||
(defn camera-button
|
||||
[edit]
|
||||
(let [images-count (count (vals (rf/sub [:chats/sending-image])))]
|
||||
[quo/composer-button
|
||||
{:on-press (if edit
|
||||
#(js/alert "This feature is temporarily unavailable in edit mode.")
|
||||
#(go-to-camera images-count))
|
||||
:accessibility-label :camera-button
|
||||
:icon :i/camera
|
||||
:container-style {:margin-right 12}}]))
|
||||
|
||||
|
||||
(defn open-photo-selector
|
||||
[{:keys [input-ref]}
|
||||
{:keys [height]}]
|
||||
(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 [:chat.ui/set-input-content-height
|
||||
(reanimated/get-shared-value height)])
|
||||
(rf/dispatch [:photo-selector/navigate-to-photo-selector]))
|
||||
:on-denied (fn []
|
||||
(alert.effects/show-popup (i18n/label :t/error)
|
||||
(i18n/label
|
||||
:t/external-storage-denied)))}))
|
||||
|
||||
(defn image-button
|
||||
[props animations edit]
|
||||
[quo/composer-button
|
||||
{:on-press (if edit
|
||||
#(js/alert "This feature is temporarily unavailable in edit mode.")
|
||||
#(open-photo-selector props animations))
|
||||
:accessibility-label :open-images-button
|
||||
:container-style {:margin-right 12}
|
||||
:icon :i/image}])
|
||||
|
||||
(defn reaction-button
|
||||
[]
|
||||
[quo/composer-button
|
||||
{:icon :i/reaction
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(rn/dismiss-keyboard!)
|
||||
(js/alert "to be implemented"))
|
||||
:container-style {:margin-right 12}}])
|
||||
|
||||
(defn format-button
|
||||
[]
|
||||
[quo/composer-button
|
||||
{:on-press (fn []
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(rn/dismiss-keyboard!)
|
||||
(js/alert "to be implemented"))
|
||||
:icon :i/format}])
|
||||
|
||||
(defn view
|
||||
[props state animations window-height {:keys [edit images]}]
|
||||
(let [send-btn-opacity (reanimated/use-shared-value 0)
|
||||
audio-btn-opacity (reanimated/interpolate send-btn-opacity [0 1] [1 0])]
|
||||
[input-ref]
|
||||
(let [edit (rf/sub [:chats/edit-message])]
|
||||
[rn/view {:style style/actions-container}
|
||||
[rn/view
|
||||
{:style {:flex-direction :row
|
||||
:display (if @(:recording? state) :none :flex)}}
|
||||
[camera-button edit]
|
||||
[image-button props animations edit]
|
||||
(when config/show-not-implemented-features?
|
||||
[reaction-button])
|
||||
(when config/show-not-implemented-features?
|
||||
[format-button])]
|
||||
[:f> send-button props state animations window-height images edit send-btn-opacity]
|
||||
(when (and (not edit) (not images) config/show-not-implemented-features?)
|
||||
;; TODO(alwx): needs to be replaced with an `audio-button` later. See
|
||||
;; https://github.com/status-im/status-mobile/issues/16084 for more details.
|
||||
[:f> disabled-audio-button audio-btn-opacity])]))
|
||||
[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]]))
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
|
||||
(def ^:const line-height (if platform/ios? 18 (:line-height typography/paragraph-1)))
|
||||
|
||||
(def ^:const multiline-minimized-height (+ input-height line-height))
|
||||
|
||||
(def ^:const empty-opacity 0.7)
|
||||
|
||||
(def ^:const images-padding-top 12)
|
||||
(def ^:const images-padding-bottom 8)
|
||||
(def ^:const images-container-height
|
||||
@@ -33,16 +29,6 @@
|
||||
|
||||
(def ^:const mentions-max-height 240)
|
||||
|
||||
(def ^:const extra-content-offset (if platform/ios? 6 -8))
|
||||
|
||||
(def ^:const content-change-threshold 10)
|
||||
|
||||
(def ^:const drag-threshold 30)
|
||||
|
||||
(def ^:const velocity-threshold (if platform/ios? -1000 -500))
|
||||
|
||||
(def ^:const background-threshold 0.75)
|
||||
|
||||
(def ^:const max-text-size 4096)
|
||||
|
||||
(def ^:const unfurl-debounce-ms
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
[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
|
||||
[{:keys [text-value input-ref input-height]}]
|
||||
[input-ref]
|
||||
(let [theme (quo.theme/use-theme)]
|
||||
[rn/view
|
||||
{:style style/container
|
||||
@@ -32,18 +33,16 @@
|
||||
{:size 24
|
||||
:icon-only? true
|
||||
:accessibility-label :edit-cancel-button
|
||||
:on-press #(utils/cancel-edit-message text-value input-ref input-height)
|
||||
:on-press #(utils/cancel-edit-message input-ref)
|
||||
:type :outline}
|
||||
:i/close]]))
|
||||
|
||||
(defn- f-view
|
||||
[props]
|
||||
(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 props])]))
|
||||
|
||||
(defn view
|
||||
[props]
|
||||
[:f> f-view props])
|
||||
(when edit
|
||||
[edit-message input-ref])]))
|
||||
|
||||
@@ -1,225 +1,20 @@
|
||||
(ns status-im.contexts.chat.messenger.composer.effects
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[oops.core :as oops]
|
||||
[react-native.async-storage :as async-storage]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.contexts.chat.messenger.composer.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.composer.keyboard :as kb]
|
||||
[status-im.contexts.chat.messenger.composer.utils :as utils]
|
||||
[utils.number]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn reenter-screen-effect
|
||||
[{:keys [text-value saved-cursor-position maximized?]}
|
||||
{:keys [content-height]}
|
||||
{:keys [input-content-height input-text input-maximized?]}
|
||||
{:keys [height]}]
|
||||
(let [lines (utils/calc-lines input-content-height)
|
||||
minimized-height (if (or (= lines 1) (empty? input-text))
|
||||
constants/input-height
|
||||
constants/multiline-minimized-height)]
|
||||
(when (and (empty? @text-value) (not= input-text nil))
|
||||
(reset! text-value input-text)
|
||||
(reset! content-height input-content-height)
|
||||
(reset! saved-cursor-position (count input-text))
|
||||
(reanimated/set-shared-value height minimized-height))
|
||||
(when input-maximized?
|
||||
(reset! maximized? true))))
|
||||
|
||||
(defn maximized-effect
|
||||
[{:keys [maximized?]}
|
||||
{:keys [height saved-height last-height]}
|
||||
{:keys [max-height]}
|
||||
{:keys [input-content-height]}]
|
||||
(when (or @maximized? (>= input-content-height max-height))
|
||||
(reanimated/animate height max-height)
|
||||
(reanimated/set-shared-value saved-height max-height)
|
||||
(reanimated/set-shared-value last-height max-height)))
|
||||
|
||||
(defn layout-effect
|
||||
[{:keys [lock-layout?]}]
|
||||
(when-not @lock-layout?
|
||||
(js/setTimeout #(reset! lock-layout? true) 500)))
|
||||
|
||||
(defn kb-default-height-effect
|
||||
[{:keys [kb-default-height kb-height]}]
|
||||
(when (zero? @kb-default-height)
|
||||
(async-storage/get-item :kb-default-height
|
||||
(fn [height]
|
||||
(reset! kb-default-height (utils.number/parse-int height 0))
|
||||
(reset! kb-height (utils.number/parse-int height 0))))))
|
||||
|
||||
(defn background-effect
|
||||
[{:keys [maximized?]}
|
||||
{:keys [opacity background-y]}
|
||||
{:keys [max-height]}
|
||||
{:keys [input-content-height]}]
|
||||
(when (or @maximized? (>= input-content-height (* max-height constants/background-threshold)))
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reanimated/animate opacity 1)))
|
||||
|
||||
(defn link-preview-effect
|
||||
[{:keys [text-value]}]
|
||||
(let [text @text-value]
|
||||
(when-not (string/blank? text)
|
||||
(rf/dispatch [:link-preview/unfurl-urls text]))))
|
||||
|
||||
(defn audio-effect
|
||||
[{:keys [recording? gesture-enabled?]}
|
||||
audio]
|
||||
(when (and audio (not @recording?))
|
||||
(reset! recording? true)
|
||||
(reset! gesture-enabled? false)))
|
||||
|
||||
(defn empty-effect
|
||||
[{:keys [empty-input?]} subscriptions]
|
||||
(reanimated/set-shared-value
|
||||
empty-input?
|
||||
(utils/empty-input? subscriptions)))
|
||||
|
||||
(defn component-will-unmount
|
||||
[{:keys [keyboard-show-listener keyboard-hide-listener keyboard-frame-listener]}]
|
||||
(.remove ^js @keyboard-show-listener)
|
||||
(.remove ^js @keyboard-hide-listener)
|
||||
(.remove ^js @keyboard-frame-listener))
|
||||
|
||||
(defn initialize
|
||||
[props state animations {:keys [max-height] :as dimensions}
|
||||
{:keys [chat-input audio input-text images link-previews? reply edit] :as subscriptions}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(maximized-effect state animations dimensions chat-input)
|
||||
(layout-effect state)
|
||||
(kb-default-height-effect state)
|
||||
(background-effect state animations dimensions chat-input)
|
||||
(link-preview-effect state)
|
||||
(audio-effect state audio)
|
||||
(kb/add-kb-listeners props state animations dimensions)
|
||||
#(component-will-unmount props))
|
||||
[max-height])
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(empty-effect animations subscriptions))
|
||||
[input-text images link-previews? reply edit audio])
|
||||
(rn/use-mount #(reenter-screen-effect state dimensions subscriptions animations)))
|
||||
[utils.number]))
|
||||
|
||||
(defn use-edit
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value saved-cursor-position cursor-position]}
|
||||
{:keys [edit input-with-mentions]}
|
||||
chat-screen-layout-calculations-complete?]
|
||||
(let [mention? (some #(= :mention (first %)) (seq input-with-mentions))]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(let [mention-text (reduce (fn [acc item]
|
||||
(str acc (second item)))
|
||||
""
|
||||
input-with-mentions)
|
||||
edit-text (cond
|
||||
mention? mention-text
|
||||
;; NOTE: using text-value for cases when the user
|
||||
;; leaves the app with an unfinished edit and re-opens
|
||||
;; the chat.
|
||||
(and (seq @text-value)
|
||||
(not (reanimated/get-shared-value
|
||||
chat-screen-layout-calculations-complete?)))
|
||||
@text-value
|
||||
:else (get-in edit [:content :text]))
|
||||
selection-pos (count edit-text)
|
||||
inject-edit-text (fn []
|
||||
(reset! text-value edit-text)
|
||||
(reset! cursor-position selection-pos)
|
||||
(reset! saved-cursor-position selection-pos)
|
||||
(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:text edit-text}))))]
|
||||
|
||||
(when (and edit @input-ref)
|
||||
;; NOTE: A small setTimeout is necessary to ensure the focus is enqueued and is executed
|
||||
;; ASAP. Check https://github.com/software-mansion/react-native-screens/issues/472
|
||||
;;
|
||||
;; The nested setTimeout is necessary to avoid both `on-focus` and
|
||||
;; `on-content-size-change` handlers triggering the height animation simultaneously, as
|
||||
;; this causes a jump in the
|
||||
;; UI. This way, `on-focus` will trigger first without changing the height, after which
|
||||
;; `on-content-size-change` will animate the height of the input based on the injected
|
||||
;; text.
|
||||
(js/setTimeout #(do (when (reanimated/get-shared-value
|
||||
chat-screen-layout-calculations-complete?)
|
||||
(.focus ^js @input-ref))
|
||||
(reagent/next-tick inject-edit-text))
|
||||
600))))
|
||||
[(:message-id 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
|
||||
[{:keys [input-ref]}
|
||||
{:keys [reply]}
|
||||
chat-screen-layout-calculations-complete?]
|
||||
[input-ref reply]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when (and reply @input-ref (reanimated/get-shared-value chat-screen-layout-calculations-complete?))
|
||||
(when (and reply @input-ref)
|
||||
(js/setTimeout #(.focus ^js @input-ref) 600)))
|
||||
[(:message-id reply)]))
|
||||
|
||||
(defn update-input-mention
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value]}
|
||||
{:keys [input-text]}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when (and input-text (not= @text-value input-text))
|
||||
(when @input-ref
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text input-text})))
|
||||
(reset! text-value input-text)
|
||||
(rf/dispatch [:mention/on-change-text input-text])))
|
||||
[input-text]))
|
||||
|
||||
(defn link-previews
|
||||
[{:keys [sending-links?]}
|
||||
{:keys [text-value maximized?]}
|
||||
{:keys [height saved-height]}
|
||||
{:keys [link-previews?]}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(if-not @maximized?
|
||||
(when (not= @sending-links? link-previews?)
|
||||
(reanimated/animate height (reanimated/get-shared-value saved-height)))
|
||||
(let [curr-text @text-value]
|
||||
(reset! text-value (str @text-value " "))
|
||||
(js/setTimeout #(reset! text-value curr-text) 100)))
|
||||
(reset! sending-links? link-previews?))
|
||||
[link-previews?]))
|
||||
|
||||
(defn use-images
|
||||
[{:keys [sending-images? input-ref]}
|
||||
{:keys [text-value maximized?]}
|
||||
{:keys [height saved-height]}
|
||||
{:keys [images]}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when (and (not @sending-images?) (seq images) @input-ref)
|
||||
(.focus ^js @input-ref))
|
||||
(if-not @maximized?
|
||||
(when (not= @sending-images? (boolean (seq images)))
|
||||
(reanimated/animate height (reanimated/get-shared-value saved-height)))
|
||||
(let [curr-text @text-value]
|
||||
(reset! text-value (str @text-value " "))
|
||||
(js/setTimeout #(reset! text-value curr-text) 100)))
|
||||
(reset! sending-images? (boolean (seq images))))
|
||||
[(boolean (seq images))]))
|
||||
|
||||
(defn did-mount
|
||||
[{:keys [selectable-input-ref input-ref selection-manager]}]
|
||||
(rn/use-mount
|
||||
(fn []
|
||||
(when platform/android?
|
||||
(let [selectable-text-input-handle (rn/find-node-handle @selectable-input-ref)
|
||||
text-input-handle (rn/find-node-handle @input-ref)]
|
||||
(oops/ocall selection-manager
|
||||
:setupMenuItems
|
||||
selectable-text-input-handle
|
||||
text-input-handle))))))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[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.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
@@ -21,6 +22,12 @@
|
||||
(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-content-height
|
||||
{:events [:chat.ui/set-input-content-height]}
|
||||
[{db :db} content-height chat-id]
|
||||
@@ -69,15 +76,14 @@
|
||||
[{: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-n [[:chat.ui/set-chat-input-text nil current-chat-id]
|
||||
[:mention/to-input-field text current-chat-id]]}))
|
||||
{: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"
|
||||
@@ -257,6 +263,13 @@
|
||||
[{{: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)})))))
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
(ns status-im.contexts.chat.messenger.composer.gesture
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[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.utils :as utils]
|
||||
[utils.number]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn set-opacity
|
||||
[velocity opacity translation expanding? min-height max-height new-height saved-height]
|
||||
(let [remaining-height (if expanding?
|
||||
(- max-height (reanimated/get-shared-value saved-height))
|
||||
(- (reanimated/get-shared-value saved-height) min-height))
|
||||
progress (if (= new-height min-height) 1 (/ translation remaining-height))
|
||||
currently-expanding? (neg? velocity)
|
||||
max-opacity? (and currently-expanding? (= (reanimated/get-shared-value opacity) 1))
|
||||
min-opacity? (and (not currently-expanding?)
|
||||
(= (reanimated/get-shared-value opacity) 0))]
|
||||
(if (>= translation 0)
|
||||
(when (and (not expanding?) (not min-opacity?))
|
||||
(reanimated/set-shared-value opacity (- 1 progress)))
|
||||
(when (and expanding? (not max-opacity?))
|
||||
(reanimated/set-shared-value opacity (Math/abs progress))))))
|
||||
|
||||
(defn maximize
|
||||
[{:keys [maximized?]}
|
||||
{:keys [height saved-height background-y opacity]}
|
||||
{:keys [max-height]}]
|
||||
(reanimated/animate height max-height)
|
||||
(reanimated/set-shared-value saved-height max-height)
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reanimated/animate opacity 1)
|
||||
(js/setTimeout (fn []
|
||||
(reset! maximized? true)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized true]))
|
||||
300))
|
||||
|
||||
(defn minimize
|
||||
[{:keys [input-ref emoji-kb-extra-height saved-emoji-kb-extra-height]}
|
||||
{:keys [maximized?]}]
|
||||
(when @emoji-kb-extra-height
|
||||
(reset! saved-emoji-kb-extra-height @emoji-kb-extra-height)
|
||||
(reset! emoji-kb-extra-height nil))
|
||||
(reset! maximized? false)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])
|
||||
(utils/blur-input input-ref))
|
||||
|
||||
(defn bounce-back
|
||||
[{:keys [height saved-height opacity background-y]}
|
||||
{:keys [window-height]}
|
||||
starting-opacity]
|
||||
(reanimated/animate height (reanimated/get-shared-value saved-height))
|
||||
(when (zero? starting-opacity)
|
||||
(reanimated/animate opacity 0)
|
||||
(reanimated/animate-delay background-y (- window-height) 300)))
|
||||
|
||||
(defn drag-gesture
|
||||
[{:keys [input-ref] :as props}
|
||||
{:keys [gesture-enabled?] :as state}
|
||||
{:keys [height saved-height last-height opacity background-y] :as animations}
|
||||
{:keys [max-height lines] :as dimensions}
|
||||
keyboard-shown]
|
||||
(let [expanding? (atom true)
|
||||
starting-opacity (reanimated/get-shared-value opacity)]
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/enabled @gesture-enabled?)
|
||||
(gesture/on-start (fn [event]
|
||||
(if-not keyboard-shown
|
||||
(do ; focus and end
|
||||
(when (< (oops/oget event "velocityY") constants/velocity-threshold)
|
||||
(reanimated/set-shared-value last-height max-height)
|
||||
(maximize state animations dimensions))
|
||||
(when @input-ref
|
||||
(.focus ^js @input-ref))
|
||||
(reset! gesture-enabled? false))
|
||||
(do ; else, will start gesture
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reset! expanding? (neg? (oops/oget event "velocityY")))))))
|
||||
(gesture/on-update
|
||||
(fn [event]
|
||||
(let [translation (oops/oget event "translationY")
|
||||
min-height (utils/get-min-height lines)
|
||||
new-height (- (reanimated/get-shared-value saved-height) translation)
|
||||
bounded-height (utils.number/value-in-range new-height min-height max-height)]
|
||||
(when keyboard-shown
|
||||
(if (>= new-height min-height)
|
||||
(do ; expand sheet
|
||||
(reanimated/set-shared-value height bounded-height)
|
||||
(set-opacity (oops/oget event "velocityY")
|
||||
opacity
|
||||
translation
|
||||
@expanding?
|
||||
min-height
|
||||
max-height
|
||||
bounded-height
|
||||
saved-height))
|
||||
; sheet at min-height, collapse keyboard
|
||||
(utils/blur-input input-ref))))))
|
||||
(gesture/on-end (fn []
|
||||
(let [diff (- (reanimated/get-shared-value height)
|
||||
(reanimated/get-shared-value saved-height))]
|
||||
(if @gesture-enabled?
|
||||
(if (and @expanding? (>= diff 0))
|
||||
(if (> diff constants/drag-threshold)
|
||||
(maximize state animations dimensions)
|
||||
(bounce-back animations dimensions starting-opacity))
|
||||
(if (> (Math/abs diff) constants/drag-threshold)
|
||||
(minimize props state)
|
||||
(bounce-back animations dimensions starting-opacity)))
|
||||
(reset! gesture-enabled? true))))))))
|
||||
@@ -3,139 +3,17 @@
|
||||
[clojure.string :as string]
|
||||
[oops.core :as oops]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.contexts.chat.messenger.composer.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.composer.keyboard :as kb]
|
||||
[status-im.contexts.chat.messenger.composer.selection :as selection]
|
||||
[status-im.contexts.chat.messenger.composer.utils :as utils]
|
||||
[utils.debounce :as debounce]
|
||||
[utils.number]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn focus
|
||||
"Animate to the `saved-height`, display background-overlay if needed, and set cursor position"
|
||||
[{:keys [input-ref] :as props}
|
||||
{:keys [text-value focused? lock-selection? saved-cursor-position maximized?]}
|
||||
{:keys [height saved-height last-height opacity background-y composer-focused?]
|
||||
:as animations}
|
||||
{:keys [max-height] :as dimensions}]
|
||||
(reanimated/set-shared-value composer-focused? true)
|
||||
(reset! focused? true)
|
||||
(rf/dispatch [:chat.ui/set-input-focused true])
|
||||
(let [last-height-value (reanimated/get-shared-value last-height)
|
||||
new-height (min max-height last-height-value)]
|
||||
(reanimated/animate height new-height)
|
||||
(reanimated/set-shared-value saved-height new-height)
|
||||
(when (> last-height-value (* constants/background-threshold max-height))
|
||||
(reset! maximized? true)
|
||||
(reanimated/animate opacity 1)
|
||||
(reanimated/set-shared-value background-y 0)))
|
||||
|
||||
(js/setTimeout #(reset! lock-selection? false) 300)
|
||||
(when (and (not-empty @text-value) @input-ref)
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:selection {:start @saved-cursor-position :end @saved-cursor-position}})))
|
||||
(kb/handle-refocus-emoji-kb-ios props animations dimensions))
|
||||
|
||||
(defn blur
|
||||
"Save the current height, minimize the composer, animate-out the background, and save cursor position"
|
||||
[{:keys [text-value focused? lock-selection? cursor-position saved-cursor-position gradient-z-index
|
||||
maximized? recording?]}
|
||||
{:keys [height saved-height last-height gradient-opacity opacity background-y composer-focused?]}
|
||||
{:keys [content-height max-height window-height]}]
|
||||
(when-not @recording?
|
||||
(let [lines (utils/calc-lines (- @content-height constants/extra-content-offset))
|
||||
min-height (utils/get-min-height lines)
|
||||
reopen-height (utils/calc-reopen-height text-value
|
||||
min-height
|
||||
max-height
|
||||
content-height
|
||||
saved-height)]
|
||||
(reanimated/set-shared-value composer-focused? false)
|
||||
(reset! focused? false)
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(reanimated/set-shared-value last-height reopen-height)
|
||||
(reanimated/animate height min-height)
|
||||
(reanimated/set-shared-value saved-height min-height)
|
||||
(reanimated/animate opacity 0)
|
||||
(js/setTimeout #(reanimated/set-shared-value background-y (- window-height)) 300)
|
||||
(reanimated/animate gradient-opacity 0)
|
||||
(reset! lock-selection? true)
|
||||
(reset! saved-cursor-position @cursor-position)
|
||||
(reset! gradient-z-index (if (= (reanimated/get-shared-value gradient-opacity) 1) -1 0))
|
||||
(when (not= reopen-height max-height)
|
||||
(reset! maximized? false)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])))))
|
||||
|
||||
(defn content-size-change
|
||||
"Save new text height, expand composer if possible, show background overlay if needed"
|
||||
[event
|
||||
{:keys [maximized? lock-layout? text-value]}
|
||||
{:keys [height saved-height last-height opacity background-y]}
|
||||
{:keys [content-height window-height max-height]}
|
||||
keyboard-shown]
|
||||
(when keyboard-shown
|
||||
(let [event-size (oops/oget event "nativeEvent.contentSize.height")
|
||||
content-size (+ event-size constants/extra-content-offset)
|
||||
lines (utils/calc-lines event-size)
|
||||
content-size (if (or (= lines 1) (empty? @text-value))
|
||||
constants/input-height
|
||||
(if (= lines 2) constants/multiline-minimized-height content-size))
|
||||
new-height (utils.number/value-in-range content-size
|
||||
constants/input-height
|
||||
max-height)
|
||||
new-height (min new-height max-height)]
|
||||
(reset! content-height content-size)
|
||||
(when (utils/update-height? content-size height max-height)
|
||||
(reanimated/animate height new-height)
|
||||
(reanimated/set-shared-value last-height new-height)
|
||||
(reanimated/set-shared-value saved-height new-height))
|
||||
(when (= new-height max-height)
|
||||
(reset! maximized? true)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized true]))
|
||||
(if (utils/show-background? max-height new-height maximized?)
|
||||
(do
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reanimated/animate opacity 1))
|
||||
(when (= (reanimated/get-shared-value opacity) 1)
|
||||
(reanimated/animate opacity 0)
|
||||
(js/setTimeout #(reanimated/set-shared-value background-y (- window-height)) 300)))
|
||||
(rf/dispatch [:chat.ui/set-input-content-height content-size])
|
||||
(reset! lock-layout? (> lines 2)))))
|
||||
|
||||
(defn scroll
|
||||
"Hide or show top gradient while scroll"
|
||||
[event
|
||||
{:keys [scroll-y]}
|
||||
{:keys [gradient-z-index focused?]}
|
||||
{:keys [gradient-opacity]}
|
||||
{:keys [lines max-lines]}]
|
||||
(let [y (oops/oget event "nativeEvent.contentOffset.y")]
|
||||
(reset! scroll-y y)
|
||||
(when (utils/show-top-gradient? y lines max-lines gradient-opacity focused?)
|
||||
(reset! gradient-z-index 1)
|
||||
(js/setTimeout #(reanimated/animate gradient-opacity 1) 0))
|
||||
(when (utils/hide-top-gradient? y gradient-opacity)
|
||||
(reanimated/animate gradient-opacity 0)
|
||||
(js/setTimeout #(reset! gradient-z-index 0) 300))))
|
||||
|
||||
(defn change-text
|
||||
"Update `text-value`, update cursor selection, find links, find mentions"
|
||||
[text
|
||||
{:keys [input-ref record-reset-fn]}
|
||||
{:keys [text-value cursor-position recording?]}]
|
||||
(reset! text-value text)
|
||||
(reagent/next-tick #(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:selection {:start @cursor-position
|
||||
:end @cursor-position}}))))
|
||||
(when @recording?
|
||||
(@record-reset-fn)
|
||||
(reset! recording? false))
|
||||
[text]
|
||||
(rf/dispatch [:chat.ui/set-chat-input-text text])
|
||||
(debounce/debounce-and-dispatch [:link-preview/unfurl-urls text]
|
||||
constants/unfurl-debounce-ms)
|
||||
(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)))
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
[rn/view {:style style/remove-photo-inner-container}
|
||||
[quo/icon :i/clear {:size 20 :color colors/neutral-50 :color-2 colors/white}]]]])
|
||||
|
||||
(defn f-images-list
|
||||
(defn images-list
|
||||
[]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
images (rf/sub [:chats/sending-image])
|
||||
@@ -48,7 +48,3 @@
|
||||
:horizontal true
|
||||
:shows-horizontal-scroll-indicator false
|
||||
:keyboard-should-persist-taps :handled}]]))
|
||||
|
||||
(defn images-list
|
||||
[]
|
||||
[:f> f-images-list])
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
(ns status-im.contexts.chat.messenger.composer.keyboard
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[react-native.async-storage :as async-storage]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.composer.utils :as utils]))
|
||||
|
||||
(defn get-kb-height
|
||||
[curr-height default-height]
|
||||
(if (and default-height (< curr-height default-height))
|
||||
default-height
|
||||
curr-height))
|
||||
|
||||
(defn store-kb-height
|
||||
[event {:keys [kb-default-height kb-height]}]
|
||||
(let [height (- (:height (rn/get-window))
|
||||
(oops/oget event "endCoordinates.screenY"))]
|
||||
(reset! kb-height height)
|
||||
(when (zero? @kb-default-height)
|
||||
(async-storage/set-item! :kb-default-height (str height)))))
|
||||
|
||||
(defn handle-emoji-kb-ios
|
||||
"Opening emoji KB on iOS will cause a flicker up and down due to height differences.
|
||||
This method handles that by adding the extra difference between the keyboards. When the input is
|
||||
expanded to a point where the added difference will make the composer go beyond the screen causing a flicker,
|
||||
we're subtracting the difference so it only reaches the allowed max-height. We're not animating these
|
||||
changes to make it appear seamless during transitions between keyboard types when maximized."
|
||||
[event
|
||||
{:keys [emoji-kb-extra-height]}
|
||||
{:keys [text-value kb-height]}
|
||||
{:keys [height saved-height]}
|
||||
{:keys [max-height]}]
|
||||
(let [start-h (oops/oget event "startCoordinates.height")
|
||||
end-h (oops/oget event "endCoordinates.height")
|
||||
diff (- end-h start-h)
|
||||
max-height-diff (- max-height diff)
|
||||
curr-text @text-value
|
||||
bigger-than-default-kb? (> end-h @kb-height)
|
||||
almost-expanded? (> (reanimated/get-shared-value height) max-height-diff)]
|
||||
(if (and almost-expanded? bigger-than-default-kb? (pos? diff))
|
||||
(do
|
||||
(reanimated/set-shared-value height (- (reanimated/get-shared-value height) diff))
|
||||
(reanimated/set-shared-value saved-height (- (reanimated/get-shared-value saved-height) diff))
|
||||
(reset! text-value (str @text-value " "))
|
||||
(js/setTimeout #(reset! text-value curr-text) 0)
|
||||
(reset! emoji-kb-extra-height diff))
|
||||
(when @emoji-kb-extra-height
|
||||
(reanimated/set-shared-value height
|
||||
(+ (reanimated/get-shared-value height) @emoji-kb-extra-height))
|
||||
(reanimated/set-shared-value saved-height
|
||||
(+ (reanimated/get-shared-value saved-height)
|
||||
@emoji-kb-extra-height))
|
||||
(reset! emoji-kb-extra-height nil)))))
|
||||
|
||||
(defn add-kb-listeners
|
||||
[{:keys [keyboard-show-listener keyboard-frame-listener keyboard-hide-listener input-ref] :as props}
|
||||
state animations dimensions]
|
||||
(reset! keyboard-show-listener (.addListener
|
||||
rn/keyboard
|
||||
"keyboardDidShow"
|
||||
#(store-kb-height % state)))
|
||||
(reset! keyboard-frame-listener (.addListener
|
||||
rn/keyboard
|
||||
"keyboardWillChangeFrame"
|
||||
#(handle-emoji-kb-ios % props state animations dimensions)))
|
||||
(reset! keyboard-hide-listener (.addListener rn/keyboard
|
||||
"keyboardDidHide"
|
||||
#(when platform/android?
|
||||
(utils/blur-input input-ref)))))
|
||||
|
||||
(defn handle-refocus-emoji-kb-ios
|
||||
[{:keys [saved-emoji-kb-extra-height]}
|
||||
{:keys [height saved-height last-height]}
|
||||
{:keys [lines max-lines]}]
|
||||
(when @saved-emoji-kb-extra-height
|
||||
(js/setTimeout (fn []
|
||||
(when (> lines max-lines)
|
||||
(reanimated/animate height
|
||||
(+ (reanimated/get-shared-value last-height)
|
||||
@saved-emoji-kb-extra-height))
|
||||
(reanimated/set-shared-value saved-height
|
||||
(+ (reanimated/get-shared-value last-height)
|
||||
@saved-emoji-kb-extra-height)))
|
||||
(reset! saved-emoji-kb-extra-height nil))
|
||||
600)))
|
||||
@@ -21,7 +21,7 @@
|
||||
[previews?])
|
||||
height))
|
||||
|
||||
(defn f-view
|
||||
(defn view
|
||||
[]
|
||||
(let [previews (rf/sub [:chats/link-previews-unfurled])
|
||||
height (use-animated-height (boolean (seq previews)))]
|
||||
@@ -48,7 +48,3 @@
|
||||
:thumbnail (:data-uri thumbnail)
|
||||
:url url})
|
||||
previews)}]]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
[:f> f-view])
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.composer.constants :as constants]))
|
||||
|
||||
|
||||
(defn shadow
|
||||
[theme]
|
||||
(if platform/ios?
|
||||
@@ -16,12 +15,12 @@
|
||||
{:elevation 10}))
|
||||
|
||||
(defn container
|
||||
[opacity bottom theme]
|
||||
[opacity top theme]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity}
|
||||
(merge
|
||||
{:position :absolute
|
||||
:bottom bottom
|
||||
:top (- (+ 8 top))
|
||||
:left 8
|
||||
:right 8
|
||||
:border-radius 16
|
||||
|
||||
@@ -2,70 +2,45 @@
|
||||
(:require
|
||||
[quo.theme]
|
||||
[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.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.composer.utils :as utils]
|
||||
[status-im.contexts.chat.messenger.messages.constants :as messages.constants]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn update-cursor
|
||||
[user {:keys [cursor-position input-ref]}]
|
||||
(when platform/android?
|
||||
(let [new-cursor-pos (+ (count (:primary-name user)) @cursor-position 1)]
|
||||
(reset! cursor-position new-cursor-pos)
|
||||
(reagent/next-tick #(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:selection {:start new-cursor-pos
|
||||
:end
|
||||
new-cursor-pos}})))))))
|
||||
|
||||
(defn mention-item
|
||||
[user _ _ render-data]
|
||||
[user]
|
||||
[contact-list-item/contact-list-item
|
||||
{:on-press (fn []
|
||||
(rf/dispatch [:chat.ui/select-mention user])
|
||||
(update-cursor user render-data))}
|
||||
{:on-press #(rf/dispatch [:chat.ui/select-mention user])}
|
||||
user])
|
||||
|
||||
(defn- f-view
|
||||
[suggestions-atom props state animations max-height cursor-pos images link-previews? reply edit]
|
||||
(let [suggestions (rf/sub [:chat/mention-suggestions])
|
||||
theme (quo.theme/use-theme)
|
||||
opacity (reanimated/use-shared-value (if (seq suggestions) 1 0))
|
||||
size (count suggestions)
|
||||
data {:keyboard-height @(:kb-height state)
|
||||
:insets (safe-area/get-insets)
|
||||
:curr-height (reanimated/get-shared-value (:height animations))
|
||||
:window-height (:height (rn/get-window))
|
||||
:images images
|
||||
:link-previews? link-previews?
|
||||
:reply reply
|
||||
:edit edit}
|
||||
mentions-pos
|
||||
(utils/calc-suggestions-position cursor-pos max-height size state data images link-previews?)]
|
||||
(defn view
|
||||
[{:keys [layout-height]}]
|
||||
(let [suggestions (rf/sub [:chat/mention-suggestions])
|
||||
suggestions? (seq suggestions)
|
||||
theme (quo.theme/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/get-top)
|
||||
messages.constants/top-bar-height
|
||||
5)))]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(if (seq suggestions)
|
||||
(reset! suggestions-atom suggestions)
|
||||
(js/setTimeout #(reset! suggestions-atom suggestions) 300))
|
||||
(reanimated/animate opacity (if (seq suggestions) 1 0)))
|
||||
[(seq suggestions)])
|
||||
(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 mentions-pos theme)}
|
||||
{:style (style/container opacity top theme)}
|
||||
[rn/flat-list
|
||||
{:keyboard-should-persist-taps :always
|
||||
:data (vals @suggestions-atom)
|
||||
:data (vals suggestions-state)
|
||||
:key-fn :key
|
||||
:render-fn mention-item
|
||||
:render-data {:cursor-position (:cursor-position state)
|
||||
:input-ref (:input-ref props)}
|
||||
:accessibility-label :mentions-list}]]))
|
||||
|
||||
(defn view
|
||||
[props state animations max-height cursor-pos images link-previews? reply edit]
|
||||
(let [suggestions-atom (reagent/atom {})]
|
||||
[:f> f-view suggestions-atom props state animations max-height cursor-pos images link-previews? reply
|
||||
edit]))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[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]
|
||||
@@ -164,14 +165,12 @@
|
||||
:end {:x 0.7 :y 0}
|
||||
:style style/gradient}])]))
|
||||
|
||||
(defn- f-view
|
||||
[recording? input-ref]
|
||||
(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 recording? input-ref])]))
|
||||
|
||||
(defn view
|
||||
[{:keys [recording?]} input-ref]
|
||||
[:f> f-view @recording? input-ref])
|
||||
(when reply
|
||||
[quoted-message reply true false false input-ref])]))
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.foundations.typography :as typography]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.composer.constants :as constants]
|
||||
[status-im.contexts.shell.jump-to.constants :as shell.constants]
|
||||
@@ -10,34 +9,6 @@
|
||||
|
||||
(def border-top-radius 20)
|
||||
|
||||
(defn shadow
|
||||
[theme]
|
||||
(when platform/ios?
|
||||
{:shadow-radius 20
|
||||
:shadow-opacity (colors/theme-colors 0.1 0.7 theme)
|
||||
:shadow-color colors/neutral-100
|
||||
:shadow-offset {:width 0 :height (colors/theme-colors -4 -8 theme)}}))
|
||||
|
||||
(def composer-sheet-and-jump-to-container
|
||||
{:position :absolute
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0})
|
||||
|
||||
(defn sheet-container
|
||||
[insets {:keys [container-opacity composer-elevation]} theme]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity container-opacity
|
||||
:elevation composer-elevation}
|
||||
(merge
|
||||
{:border-top-left-radius border-top-radius
|
||||
:border-top-right-radius border-top-radius
|
||||
:padding-horizontal 20
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-95 theme)
|
||||
:z-index 3
|
||||
:padding-bottom (:bottom insets)}
|
||||
(shadow theme))))
|
||||
|
||||
(def bar-container
|
||||
{:height constants/bar-container-height
|
||||
:left 0
|
||||
@@ -54,65 +25,14 @@
|
||||
:border-radius 100
|
||||
:background-color (colors/theme-colors colors/neutral-100-opa-5 colors/white-opa-10 theme)})
|
||||
|
||||
(defn input-container
|
||||
[height max-height]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:height height}
|
||||
{:max-height max-height
|
||||
:z-index 1}))
|
||||
|
||||
(defn input-view
|
||||
[{:keys [recording?]}]
|
||||
{:overflow :hidden
|
||||
:z-index 1
|
||||
:flex 1
|
||||
:display (if @recording? :none :flex)
|
||||
:min-height constants/input-height})
|
||||
|
||||
(defn input-text
|
||||
[{:keys [saved-emoji-kb-extra-height]}
|
||||
{:keys [focused? maximized?]}
|
||||
{:keys [max-height theme]}]
|
||||
[theme]
|
||||
(assoc typography/paragraph-1
|
||||
:color (colors/theme-colors :black :white theme)
|
||||
:text-align-vertical :top
|
||||
:position (if @saved-emoji-kb-extra-height :relative :absolute)
|
||||
:top 0
|
||||
:left 0
|
||||
:right (when (or focused? platform/ios?) 0)
|
||||
:max-height max-height
|
||||
:padding-bottom (when @maximized? 0)))
|
||||
|
||||
(defn background
|
||||
[opacity background-y window-height]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity
|
||||
:transform [{:translate-y background-y}]}
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0
|
||||
:height window-height
|
||||
:background-color colors/neutral-95-opa-70}))
|
||||
|
||||
(defn blur-container
|
||||
[composer-default-height {:keys [blur-container-elevation]}]
|
||||
[{:elevation blur-container-elevation}
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0
|
||||
:height composer-default-height
|
||||
:border-top-right-radius border-top-radius
|
||||
:border-top-left-radius border-top-radius
|
||||
:overflow :hidden}])
|
||||
|
||||
(defn blur-view
|
||||
[theme]
|
||||
{:style {:flex 1}
|
||||
:blur-radius (if platform/ios? 20 10)
|
||||
:blur-type theme
|
||||
:blur-amount 20})
|
||||
:max-height 150))
|
||||
|
||||
(defn shell-button
|
||||
[translate-y opacity]
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
(ns status-im.contexts.chat.messenger.composer.sub-view
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.composer.style :as style]
|
||||
[status-im.feature-flags :as ff]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.worklets.chat.messenger.composer :as worklets]))
|
||||
|
||||
(defn bar
|
||||
[theme]
|
||||
[rn/view {:style style/bar-container}
|
||||
[rn/view {:style (style/bar theme)}]])
|
||||
|
||||
(defn- f-shell-button
|
||||
[{:keys [composer-focused?]} chat-list-scroll-y window-height]
|
||||
(let [customization-color (rf/sub [:profile/customization-color])
|
||||
scroll-down-button-opacity (worklets/scroll-down-button-opacity
|
||||
chat-list-scroll-y
|
||||
composer-focused?
|
||||
window-height)
|
||||
jump-to-button-opacity (worklets/jump-to-button-opacity
|
||||
scroll-down-button-opacity
|
||||
composer-focused?)
|
||||
jump-to-button-position (worklets/jump-to-button-position
|
||||
scroll-down-button-opacity
|
||||
composer-focused?)]
|
||||
[rn/view {:style (style/shell-button-container)}
|
||||
(when (ff/enabled? ::ff/shell.jump-to)
|
||||
[reanimated/view
|
||||
{:style (style/shell-button jump-to-button-position jump-to-button-opacity)}
|
||||
[quo/floating-shell-button
|
||||
{:jump-to
|
||||
{:on-press #(rf/dispatch [:shell/navigate-to-jump-to])
|
||||
:customization-color customization-color
|
||||
:label (i18n/label :t/jump-to)
|
||||
:style {:align-self :center}}}
|
||||
{}]])
|
||||
[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]]))
|
||||
|
||||
(defn shell-button
|
||||
[shared-values chat-list-scroll-y window-height]
|
||||
[:f> f-shell-button shared-values chat-list-scroll-y window-height])
|
||||
@@ -1,52 +1,8 @@
|
||||
(ns status-im.contexts.chat.messenger.composer.utils
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.contexts.chat.messenger.composer.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.composer.selection :as selection]
|
||||
[utils.number]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.worklets.chat.messenger.composer :as worklets]))
|
||||
|
||||
(defn bounded-val
|
||||
[v min-v max-v]
|
||||
(max min-v (min v max-v)))
|
||||
|
||||
(defn update-height?
|
||||
[content-size height max-height]
|
||||
(let [diff (Math/abs (- content-size (reanimated/get-shared-value height)))]
|
||||
(and (not= (reanimated/get-shared-value height) max-height)
|
||||
(> diff constants/content-change-threshold))))
|
||||
|
||||
(defn show-top-gradient?
|
||||
[y lines max-lines gradient-opacity focused?]
|
||||
(and
|
||||
(> y constants/line-height)
|
||||
(>= lines max-lines)
|
||||
(= (reanimated/get-shared-value gradient-opacity) 0)
|
||||
@focused?))
|
||||
|
||||
(defn hide-top-gradient?
|
||||
[y gradient-opacity]
|
||||
(and
|
||||
(<= y constants/line-height)
|
||||
(= (reanimated/get-shared-value gradient-opacity) 1)))
|
||||
|
||||
(defn show-bottom-gradient?
|
||||
[{:keys [text-value focused?]} {:keys [lines]}]
|
||||
(and (not-empty @text-value) (not @focused?) (> lines 2)))
|
||||
|
||||
(defn show-background?
|
||||
[max-height new-height maximized?]
|
||||
(or @maximized?
|
||||
(> new-height (* constants/background-threshold max-height))))
|
||||
|
||||
(defn calc-lines
|
||||
[height]
|
||||
(Math/floor (/ height constants/line-height)))
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn calc-top-content-height
|
||||
[reply? edit?]
|
||||
@@ -60,35 +16,6 @@
|
||||
(seq images) (+ constants/images-container-height)
|
||||
link-previews? (+ constants/links-container-height)))
|
||||
|
||||
(defn calc-reopen-height
|
||||
[text-value min-height max-height content-height saved-height]
|
||||
(if (empty? @text-value)
|
||||
min-height
|
||||
(let [input-height (min @content-height
|
||||
(reanimated/get-shared-value saved-height))]
|
||||
(min max-height input-height))))
|
||||
|
||||
(defn get-min-height
|
||||
[lines]
|
||||
(if (> lines 1)
|
||||
constants/multiline-minimized-height
|
||||
constants/input-height))
|
||||
|
||||
(defn calc-max-height
|
||||
[{:keys [reply edit images link-previews?]} window-height kb-height insets]
|
||||
(let [margin-top (if platform/ios? (:top insets) (+ 10 (:top insets)))]
|
||||
(- window-height
|
||||
margin-top
|
||||
kb-height
|
||||
constants/bar-container-height
|
||||
constants/actions-container-height
|
||||
(calc-top-content-height reply edit)
|
||||
(calc-bottom-content-height images link-previews?))))
|
||||
|
||||
(defn empty-input?
|
||||
[{:keys [input-text images link-previews? reply audio edit]}]
|
||||
(not (or (not-empty input-text) images link-previews? reply audio edit)))
|
||||
|
||||
(defn blur-input
|
||||
[input-ref]
|
||||
(when @input-ref
|
||||
@@ -101,36 +28,17 @@
|
||||
(rf/dispatch [:chat.ui/cancel-message-reply]))
|
||||
|
||||
(defn cancel-edit-message
|
||||
[text-value input-ref input-height]
|
||||
(reset! text-value "")
|
||||
[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)
|
||||
(reanimated/set-shared-value input-height constants/input-height))
|
||||
(blur-input input-ref))
|
||||
100)
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text ""}))
|
||||
(rf/dispatch [:chat.ui/set-input-content-height constants/input-height])
|
||||
(rf/dispatch [:chat.ui/cancel-message-edit]))
|
||||
|
||||
(defn count-lines
|
||||
[s]
|
||||
(-> s
|
||||
(string/split #"\n" -1)
|
||||
(butlast)
|
||||
count))
|
||||
|
||||
(defn cursor-y-position-relative-to-container
|
||||
[{:keys [scroll-y]}
|
||||
{:keys [cursor-position text-value]}]
|
||||
(let [sub-text (subs @text-value 0 @cursor-position)
|
||||
sub-text-lines (count-lines sub-text)
|
||||
scrolled-lines (Math/round (/ @scroll-y constants/line-height))
|
||||
sub-text-lines-in-view (- sub-text-lines scrolled-lines)]
|
||||
(* sub-text-lines-in-view constants/line-height)))
|
||||
|
||||
(defn calc-suggestions-position
|
||||
[cursor-pos max-height size
|
||||
{:keys [maximized?]}
|
||||
@@ -153,85 +61,3 @@
|
||||
(let [bottom-content-height (calc-bottom-content-height images link-previews?)]
|
||||
(+ base bottom-content-height))
|
||||
(+ constants/actions-container-height (:bottom insets) (- curr-height cursor-pos) 18)))))
|
||||
|
||||
(defn init-non-reactive-state
|
||||
[]
|
||||
{:input-ref (atom nil)
|
||||
:selectable-input-ref (atom nil)
|
||||
:keyboard-show-listener (atom nil)
|
||||
:keyboard-frame-listener (atom nil)
|
||||
:keyboard-hide-listener (atom nil)
|
||||
:emoji-kb-extra-height (atom nil)
|
||||
:saved-emoji-kb-extra-height (atom nil)
|
||||
:sending-images? (atom false)
|
||||
:sending-links? (atom false)
|
||||
:record-reset-fn (atom nil)
|
||||
:scroll-y (atom 0)
|
||||
:selection-event (atom nil)
|
||||
:selection-manager (rn/selectable-text-input-manager)})
|
||||
|
||||
(defn init-reactive-state
|
||||
[]
|
||||
{:text-value (reagent/atom "")
|
||||
:cursor-position (reagent/atom 0)
|
||||
:saved-cursor-position (reagent/atom 0)
|
||||
:gradient-z-index (reagent/atom 0)
|
||||
:kb-default-height (reagent/atom 0)
|
||||
:kb-height (reagent/atom 0)
|
||||
:gesture-enabled? (reagent/atom true)
|
||||
:lock-selection? (reagent/atom true)
|
||||
:focused? (reagent/atom false)
|
||||
:lock-layout? (reagent/atom false)
|
||||
:maximized? (reagent/atom false)
|
||||
:record-permission? (reagent/atom true)
|
||||
:recording? (reagent/atom false)
|
||||
:first-level? (reagent/atom true)
|
||||
:menu-items (reagent/atom selection/first-level-menu-items)})
|
||||
|
||||
(defn init-subs
|
||||
[]
|
||||
(let [chat-input (rf/sub [:chats/current-chat-input])]
|
||||
{:images (seq (rf/sub [:chats/sending-image]))
|
||||
:link-previews? (or (rf/sub [:chats/link-previews?])
|
||||
(rf/sub [:chats/status-link-previews?]))
|
||||
:audio (rf/sub [:chats/sending-audio])
|
||||
:reply (rf/sub [:chats/reply-message])
|
||||
:edit (rf/sub [:chats/edit-message])
|
||||
:input-with-mentions (rf/sub [:chat/input-with-mentions])
|
||||
:input-text (:input-text chat-input)
|
||||
:alert-banners-top-margin (rf/sub [:alert-banners/top-margin])
|
||||
:input-content-height (:input-content-height chat-input)}))
|
||||
|
||||
(defn init-shared-values
|
||||
[]
|
||||
(let [composer-focused? (reanimated/use-shared-value false)
|
||||
empty-input-shared-value? (reanimated/use-shared-value true)]
|
||||
{:composer-focused? composer-focused?
|
||||
:empty-input? empty-input-shared-value?
|
||||
:container-opacity (worklets/composer-container-opacity composer-focused?
|
||||
empty-input-shared-value?
|
||||
constants/empty-opacity)
|
||||
:blur-container-elevation (worklets/blur-container-elevation composer-focused?
|
||||
empty-input-shared-value?)
|
||||
:composer-elevation (worklets/composer-elevation composer-focused?
|
||||
empty-input-shared-value?)}))
|
||||
|
||||
(defn init-animations
|
||||
[lines content-height max-height opacity background-y shared-values]
|
||||
(let [initial-height (if (> lines 1)
|
||||
constants/multiline-minimized-height
|
||||
constants/input-height)
|
||||
bottom-content-height 0]
|
||||
(assoc shared-values
|
||||
:gradient-opacity (reanimated/use-shared-value 0)
|
||||
:height (reanimated/use-shared-value
|
||||
initial-height)
|
||||
:saved-height (reanimated/use-shared-value
|
||||
initial-height)
|
||||
:last-height (reanimated/use-shared-value
|
||||
(utils.number/value-in-range
|
||||
(+ @content-height bottom-content-height)
|
||||
constants/input-height
|
||||
max-height))
|
||||
:opacity opacity
|
||||
:background-y background-y)))
|
||||
|
||||
@@ -1,183 +1,56 @@
|
||||
(ns status-im.contexts.chat.messenger.composer.view
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[quo.core :as quo]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.hooks :as hooks]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[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.effects :as effects]
|
||||
[status-im.contexts.chat.messenger.composer.gesture :as drag-gesture]
|
||||
[status-im.contexts.chat.messenger.composer.gradients.view :as gradients]
|
||||
[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.selection :as selection]
|
||||
[status-im.contexts.chat.messenger.composer.style :as style]
|
||||
[status-im.contexts.chat.messenger.composer.sub-view :as sub-view]
|
||||
[status-im.contexts.chat.messenger.composer.utils :as utils]
|
||||
[status-im.contexts.chat.messenger.messages.contact-requests.bottom-drawer.view :as
|
||||
contact-requests.bottom-drawer]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn sheet-component
|
||||
[{:keys [insets
|
||||
chat-list-scroll-y
|
||||
chat-screen-layout-calculations-complete?
|
||||
opacity
|
||||
background-y
|
||||
theme
|
||||
window-height]} props state shared-values]
|
||||
(let [subscriptions (utils/init-subs)
|
||||
top-margin (if (pos? (:alert-banners-top-margin subscriptions))
|
||||
;; top margin increased to avoid composer overlapping with the
|
||||
;; alert banner
|
||||
(+ (:alert-banners-top-margin subscriptions) 12)
|
||||
0)
|
||||
window-height (- window-height top-margin)
|
||||
content-height (reagent/atom (or (:input-content-height ; Actual text height
|
||||
subscriptions)
|
||||
constants/input-height))
|
||||
{:keys [keyboard-shown]} (hooks/use-keyboard)
|
||||
max-height (utils/calc-max-height subscriptions ; Max allowed height for the
|
||||
; composer view
|
||||
window-height
|
||||
@(:kb-height state)
|
||||
insets)
|
||||
lines (utils/calc-lines (- @content-height constants/extra-content-offset)) ; Current
|
||||
; lines
|
||||
; count
|
||||
;; Maximum number of lines that can be displayed when composer in maximized
|
||||
max-lines (utils/calc-lines max-height)
|
||||
animations (utils/init-animations
|
||||
lines
|
||||
content-height
|
||||
max-height
|
||||
opacity
|
||||
background-y
|
||||
shared-values)
|
||||
dimensions {:content-height content-height
|
||||
:max-height max-height
|
||||
:window-height window-height
|
||||
:lines lines
|
||||
:max-lines max-lines}
|
||||
show-bottom-gradient? (utils/show-bottom-gradient? state dimensions)
|
||||
;; Cursor position, needed to determine where to display the mentions view
|
||||
cursor-pos (utils/cursor-y-position-relative-to-container
|
||||
props
|
||||
state)]
|
||||
(effects/did-mount props)
|
||||
(effects/initialize props
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
subscriptions)
|
||||
(effects/use-edit props state subscriptions chat-screen-layout-calculations-complete?)
|
||||
(effects/use-reply props subscriptions chat-screen-layout-calculations-complete?)
|
||||
(effects/update-input-mention props state subscriptions)
|
||||
(effects/link-previews props state animations subscriptions)
|
||||
(effects/use-images props state animations subscriptions)
|
||||
[:<>
|
||||
[mentions/view props state animations max-height cursor-pos
|
||||
(:images subscriptions)
|
||||
(:link-previews? subscriptions)
|
||||
(:reply subscriptions)
|
||||
(:edit subscriptions)]
|
||||
[rn/view
|
||||
{:style style/composer-sheet-and-jump-to-container}
|
||||
[sub-view/shell-button shared-values chat-list-scroll-y window-height]
|
||||
[gesture/gesture-detector
|
||||
{:gesture
|
||||
(drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
|
||||
[reanimated/view
|
||||
{:style (style/sheet-container insets animations theme)}
|
||||
[sub-view/bar theme]
|
||||
[:<>
|
||||
[reply/view state (:input-ref props)]
|
||||
[edit/view
|
||||
{:text-value (:text-value state)
|
||||
:input-height (:height animations)
|
||||
:input-ref (:input-ref props)}]]
|
||||
[reanimated/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:on-press (fn []
|
||||
(when-let [ref @(:input-ref props)]
|
||||
(.focus ^js ref)))
|
||||
:style (style/input-container (:height animations) max-height)
|
||||
:accessibility-label :message-input-container}
|
||||
[rn/selectable-text-input
|
||||
{:ref #(reset! (:selectable-input-ref props) %)
|
||||
:menu-items @(:menu-items state)
|
||||
:style (style/input-view state)}
|
||||
[rn/text-input
|
||||
{:ref #(reset! (:input-ref props) %)
|
||||
:default-value @(:text-value state)
|
||||
:on-focus #(handler/focus props state animations dimensions)
|
||||
:on-blur #(handler/blur state animations dimensions)
|
||||
:on-content-size-change #(handler/content-size-change %
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
(or keyboard-shown
|
||||
(:edit subscriptions)))
|
||||
:on-scroll #(handler/scroll % props state animations dimensions)
|
||||
:on-change-text #(handler/change-text % props state)
|
||||
:on-selection-change #(handler/selection-change % props state)
|
||||
:on-selection #(selection/on-selection % props state)
|
||||
: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)
|
||||
:style (style/input-text props
|
||||
state
|
||||
{:max-height max-height
|
||||
:theme theme})
|
||||
:max-length constants/max-text-size
|
||||
:accessibility-label :chat-message-input}]]]
|
||||
[:<>
|
||||
[gradients/view props state animations show-bottom-gradient?]
|
||||
[link-preview/view]
|
||||
[images/images-list]]
|
||||
[:f> actions/view props state animations window-height subscriptions]]]]]))
|
||||
(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 f-composer
|
||||
(defn view
|
||||
[props]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
opacity (reanimated/use-shared-value 0)
|
||||
window-height (:height (rn/get-window))
|
||||
background-y (reanimated/use-shared-value (- window-height))
|
||||
composer-default-height (+ constants/composer-default-height (:bottom (:insets props)))
|
||||
shared-values (utils/init-shared-values)
|
||||
extra-params (assoc props
|
||||
:window-height window-height
|
||||
:opacity opacity
|
||||
:background-y background-y
|
||||
:theme theme)
|
||||
props (utils/init-non-reactive-state)
|
||||
state (utils/init-reactive-state)]
|
||||
[rn/view (when platform/ios? {:style {:z-index 1}})
|
||||
[reanimated/view {:style (style/background opacity background-y window-height)}]
|
||||
[reanimated/view {:style (style/blur-container composer-default-height shared-values)}
|
||||
[quo/blur (style/blur-view theme)]]
|
||||
[:f> sheet-component extra-params props state shared-values]]))
|
||||
|
||||
(defn composer
|
||||
[props]
|
||||
(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?
|
||||
[:f> f-composer props]
|
||||
[contact-requests.bottom-drawer/view
|
||||
{:contact-id current-chat-id}]))))
|
||||
(let [theme (quo.theme/use-theme)
|
||||
bottom (safe-area/get-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 props]
|
||||
[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]]]))
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
[{:keys [outgoing content pinned-by outgoing-status deleted? deleted-for-me? content-type
|
||||
bridge-message]
|
||||
:as message-data}
|
||||
{:keys [able-to-send-message? community? can-delete-message-for-everyone?
|
||||
{:keys [able-to-send-message? community? community-member? can-delete-message-for-everyone?
|
||||
message-pin-enabled group-chat group-admin?]}]
|
||||
(concat
|
||||
(when (and outgoing
|
||||
@@ -131,7 +131,8 @@
|
||||
:id :copy}])
|
||||
;; pinning images are temporarily disabled
|
||||
(when (and message-pin-enabled
|
||||
(not= content-type constants/content-type-image))
|
||||
(not= content-type constants/content-type-image)
|
||||
(or community-member? (not community?)))
|
||||
[{:type :main
|
||||
:on-press #(pin-message message-data)
|
||||
:label (i18n/label (if pinned-by
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.messages.constants :as messages.constants]))
|
||||
|
||||
(def permission-context-height 46)
|
||||
|
||||
(defn keyboard-avoiding-container
|
||||
[theme]
|
||||
{:flex 1
|
||||
@@ -73,3 +75,8 @@
|
||||
(reanimated/apply-animations-to-style
|
||||
{:top top}
|
||||
{:row-gap 16}))
|
||||
|
||||
(defn permission-context-sheet
|
||||
[margin-bottom?]
|
||||
{:flex 3 ;; Pushes composer to bottom
|
||||
:margin-bottom (when margin-bottom? permission-context-height)})
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
[utils.worklets.chat.messenger.messages :as worklets]))
|
||||
|
||||
(defonce ^:const distance-from-last-message 4)
|
||||
(defonce ^:const loading-indicator-extra-spacing 250)
|
||||
(defonce ^:const loading-indicator-page-loading-height 100)
|
||||
(defonce ^:const min-message-height 32)
|
||||
|
||||
(defn list-key-fn [{:keys [message-id value]}] (or message-id value))
|
||||
(defn list-ref [ref] (reset! state/messages-list-ref ref))
|
||||
@@ -336,8 +334,7 @@
|
||||
|
||||
(defn list-group-chat-header
|
||||
[{:keys [chat-id invitation-admin]}]
|
||||
[rn/view
|
||||
[chat.group/group-chat-footer chat-id invitation-admin]])
|
||||
[chat.group/group-chat-footer chat-id invitation-admin])
|
||||
|
||||
(defn render-fn
|
||||
[{:keys [type value] :as message-data} _ _
|
||||
@@ -400,18 +397,18 @@
|
||||
{:keys [keyboard-shown]} (hooks/use-keyboard)
|
||||
{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])
|
||||
margin-bottom? (and community-channel? (not able-to-send-message?))
|
||||
recording? (rf/sub [:chats/recording?])]
|
||||
[rn/view {:style {:flex 3}} ;; Pushes composer to bottom
|
||||
[rn/view {:style (style/permission-context-sheet margin-bottom?)}
|
||||
[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 [:<>
|
||||
[list-header insets (:able-to-send-message? context)]
|
||||
(when (= (:chat-type chat) constants/private-group-chat-type)
|
||||
[list-group-chat-header chat])]
|
||||
:header (when (= (:chat-type chat) constants/private-group-chat-type)
|
||||
[list-group-chat-header chat])
|
||||
:footer [list-footer
|
||||
{:theme theme
|
||||
:chat chat
|
||||
@@ -431,15 +428,11 @@
|
||||
: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
|
||||
:scroll-indicator-insets {:top (if (:able-to-send-message? context)
|
||||
(- composer.constants/composer-default-height 16)
|
||||
0)
|
||||
:right 1}
|
||||
:keyboard-dismiss-mode :interactive
|
||||
:keyboard-should-persist-taps :always
|
||||
:on-scroll-begin-drag #(do
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(rn/dismiss-keyboard!))
|
||||
: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
|
||||
@@ -461,4 +454,5 @@
|
||||
:chat-screen-layout-calculations-complete?
|
||||
chat-screen-layout-calculations-complete?})
|
||||
:scroll-enabled (not recording?)
|
||||
:content-inset-adjustment-behavior :never}]]]))
|
||||
:content-inset-adjustment-behavior :never
|
||||
:scroll-indicator-insets {:right 1}}]]]))
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
(defn navigation-view
|
||||
[navigation-view-height pinned-banner-height]
|
||||
{:top 0
|
||||
:left 0
|
||||
:right 0
|
||||
:position :absolute
|
||||
:height (+ navigation-view-height pinned-banner-height)
|
||||
:z-index 1})
|
||||
{:top 0
|
||||
:left 0
|
||||
:right 0
|
||||
:position :absolute
|
||||
:pointer-events :box-none
|
||||
:height (+ navigation-view-height pinned-banner-height)
|
||||
:z-index 1})
|
||||
|
||||
(defn animated-background-view
|
||||
[background-opacity navigation-view-height]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
(ns status-im.contexts.chat.messenger.messages.scroll-to-bottom.style
|
||||
(:require [status-im.contexts.shell.jump-to.constants :as shell.constants]))
|
||||
|
||||
(def shell-button-container
|
||||
{:z-index 1
|
||||
:bottom shell.constants/floating-shell-button-height})
|
||||
|
||||
(def scroll-to-bottom-button
|
||||
{:position :absolute
|
||||
:right 0
|
||||
:left 0})
|
||||
@@ -0,0 +1,20 @@
|
||||
(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
|
||||
[{:keys [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)]
|
||||
[rn/view {:style style/shell-button-container}
|
||||
[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]]))
|
||||
@@ -1,19 +1,32 @@
|
||||
(ns status-im.contexts.chat.messenger.messages.view
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[quo.theme :as quo.theme]
|
||||
[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.view]
|
||||
[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]
|
||||
[status-im.feature-flags :as ff]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn- footer
|
||||
[props]
|
||||
(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 props]
|
||||
[contact-requests.bottom-drawer/view {:contact-id current-chat-id}]))))
|
||||
|
||||
(defn- chat-screen
|
||||
[{:keys [insets] :as props}]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
@@ -23,9 +36,11 @@
|
||||
[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 props]
|
||||
[:<>
|
||||
[list.view/messages-list-content props]
|
||||
[scroll-to-bottom/button props]]
|
||||
[messages.navigation/view props]
|
||||
[composer.view/composer props]])))
|
||||
[footer props]])))
|
||||
|
||||
(defn lazy-chat-screen
|
||||
[chat-screen-layout-calculations-complete? *screen-loaded?*]
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
[rn/view {:style (style/view-button-container false)}
|
||||
children])]))
|
||||
|
||||
(defn- f-page
|
||||
(defn- page
|
||||
[{:keys [onboarding-profile-data navigation-bar-top]}]
|
||||
(reagent/with-let [show-keyboard? (reagent/atom false)
|
||||
content-scroll-y (reagent/atom 0)
|
||||
@@ -205,7 +205,6 @@
|
||||
[]
|
||||
(let [{:keys [top]} (safe-area/get-insets)
|
||||
onboarding-profile-data (rf/sub [:onboarding/profile])]
|
||||
[:<>
|
||||
[:f> f-page
|
||||
{:navigation-bar-top top
|
||||
:onboarding-profile-data onboarding-profile-data}]]))
|
||||
[page
|
||||
{:navigation-bar-top top
|
||||
:onboarding-profile-data onboarding-profile-data}]))
|
||||
|
||||
@@ -10,15 +10,16 @@
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(rf/defn profile-data-set
|
||||
{:events [:onboarding/profile-data-set]}
|
||||
[{:keys [db]} onboarding-data]
|
||||
{:db (update db :onboarding/profile merge onboarding-data)
|
||||
:dispatch [:navigate-to-within-stack
|
||||
[:screen/onboarding.create-profile-password
|
||||
(get db
|
||||
:onboarding/navigated-to-enter-seed-phrase-from-screen
|
||||
:screen/onboarding.new-to-status)]]})
|
||||
(rf/reg-event-fx
|
||||
:onboarding/profile-data-set
|
||||
(fn [{:keys [db]} [onboarding-data]]
|
||||
(let [navigate-from-screen (get db
|
||||
:onboarding/navigated-to-enter-seed-phrase-from-screen
|
||||
:screen/onboarding.new-to-status)]
|
||||
{:db (update db :onboarding/profile merge onboarding-data)
|
||||
:fx [[:dispatch
|
||||
[:navigate-to-within-stack
|
||||
[:screen/onboarding.create-profile-password navigate-from-screen]]]]})))
|
||||
|
||||
(rf/defn enable-biometrics
|
||||
{:events [:onboarding/enable-biometrics]}
|
||||
@@ -33,6 +34,11 @@
|
||||
{: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]]]]}))
|
||||
|
||||
(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-from-syncing
|
||||
(fn [{:keys [db]}]
|
||||
{:db (dissoc db :onboarding/profile)
|
||||
|
||||
@@ -8,28 +8,30 @@
|
||||
|
||||
(defn login
|
||||
[]
|
||||
{;; Temporary fix until https://github.com/status-im/status-go/issues/3024 is resolved
|
||||
:wakuV2Nameserver "8.8.8.8"
|
||||
: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
|
||||
: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
|
||||
:alchemyEthereumGoerliToken config/ALCHEMY_ETHEREUM_GOERLI_TOKEN
|
||||
:alchemyEthereumSepoliaToken config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN
|
||||
:alchemyOptimismMainnetToken config/ALCHEMY_OPTIMISM_MAINNET_TOKEN
|
||||
:alchemyOptimismGoerliToken config/ALCHEMY_OPTIMISM_GOERLI_TOKEN
|
||||
:alchemyOptimismSepoliaToken config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN
|
||||
:alchemyArbitrumMainnetToken config/ALCHEMY_ARBITRUM_MAINNET_TOKEN
|
||||
:alchemyArbitrumGoerliToken config/ALCHEMY_ARBITRUM_GOERLI_TOKEN
|
||||
:alchemyArbitrumSepoliaToken config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN})
|
||||
(let [log-enabled? (boolean (not-empty config/log-level))]
|
||||
{;; Temporary fix until https://github.com/status-im/status-go/issues/3024 is resolved
|
||||
:wakuV2Nameserver "8.8.8.8"
|
||||
:runtimeLogLevel (when log-enabled? config/log-level)
|
||||
: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
|
||||
: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
|
||||
:alchemyEthereumGoerliToken config/ALCHEMY_ETHEREUM_GOERLI_TOKEN
|
||||
:alchemyEthereumSepoliaToken config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN
|
||||
:alchemyOptimismMainnetToken config/ALCHEMY_OPTIMISM_MAINNET_TOKEN
|
||||
:alchemyOptimismGoerliToken config/ALCHEMY_OPTIMISM_GOERLI_TOKEN
|
||||
:alchemyOptimismSepoliaToken config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN
|
||||
:alchemyArbitrumMainnetToken config/ALCHEMY_ARBITRUM_MAINNET_TOKEN
|
||||
:alchemyArbitrumGoerliToken config/ALCHEMY_ARBITRUM_GOERLI_TOKEN
|
||||
:alchemyArbitrumSepoliaToken config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN}))
|
||||
|
||||
(defn create
|
||||
[]
|
||||
|
||||
@@ -62,6 +62,9 @@
|
||||
;; 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]
|
||||
|
||||
@@ -95,9 +98,10 @@
|
||||
[:dispatch-later [{:ms 1500 :dispatch [:profile.login/non-critical-initialization]}]]
|
||||
[:dispatch [:network/check-expensive-connection]]
|
||||
[:profile.settings/get-profile-picture key-uid]
|
||||
[:settings/get-currencies]
|
||||
(when (ff/enabled? ::ff/wallet.wallet-connect)
|
||||
[:dispatch [:wallet-connect/init]])
|
||||
(when (ff/enabled? ::ff/wallet.swap)
|
||||
[:dispatch [:wallet.tokens/get-token-list]])
|
||||
(when notifications-enabled?
|
||||
[:effects/push-notifications-enable])]})))
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[status-im.common.check-before-syncing.view :as check-before-syncing]
|
||||
[status-im.common.confirmation-drawer.view :as confirmation-drawer]
|
||||
[status-im.common.metrics-confirmation-modal.view :as metrics-modal]
|
||||
[status-im.common.standard-authentication.core :as standard-authentication]
|
||||
@@ -44,32 +43,32 @@
|
||||
:linear
|
||||
50))
|
||||
|
||||
(defn- show-check-before-syncing
|
||||
(defn- navigate-to-new-to-status
|
||||
[]
|
||||
(rf/dispatch
|
||||
[:show-bottom-sheet
|
||||
{:content (fn [] [check-before-syncing/view
|
||||
{:on-submit
|
||||
#(debounce/throttle-and-dispatch
|
||||
[:open-modal :screen/onboarding.sign-in]
|
||||
1000)}])
|
||||
:shell? true}]))
|
||||
(when @push-animation-fn-atom
|
||||
(@push-animation-fn-atom))
|
||||
(debounce/throttle-and-dispatch
|
||||
[:open-modal :screen/onboarding.new-to-status]
|
||||
1000))
|
||||
|
||||
(defn- navigate-to-sync-or-recover-profile
|
||||
[]
|
||||
(when @push-animation-fn-atom
|
||||
(@push-animation-fn-atom))
|
||||
(debounce/throttle-and-dispatch
|
||||
[:open-modal :screen/onboarding.sync-or-recover-profile]
|
||||
1000))
|
||||
|
||||
(defn new-account-options
|
||||
[]
|
||||
[quo/action-drawer
|
||||
[[{:icon :i/profile
|
||||
:label (i18n/label :t/create-new-profile)
|
||||
:on-press (fn []
|
||||
(when @push-animation-fn-atom
|
||||
(@push-animation-fn-atom))
|
||||
(debounce/throttle-and-dispatch
|
||||
[:open-modal :screen/onboarding.new-to-status]
|
||||
1000))
|
||||
:on-press navigate-to-new-to-status
|
||||
:accessibility-label :create-new-profile}
|
||||
{:icon :i/multi-profile
|
||||
:label (i18n/label :t/add-existing-status-profile)
|
||||
:on-press show-check-before-syncing
|
||||
: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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
(ns status-im.contexts.settings.language-and-currency.events
|
||||
(:require [status-im.common.json-rpc.events :as json-rpc]
|
||||
[status-im.contexts.settings.language-and-currency.data-store :as data-store]
|
||||
(:require [status-im.contexts.settings.language-and-currency.data-store :as data-store]
|
||||
[utils.collection]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
@@ -11,8 +10,9 @@
|
||||
:currencies
|
||||
(utils.collection/index-by :id all-currencies))})))
|
||||
|
||||
(rf/reg-fx :settings/get-currencies
|
||||
(rf/reg-event-fx :settings/get-currencies
|
||||
(fn []
|
||||
(json-rpc/call {:method "appgeneral_getCurrencies"
|
||||
:on-success [:settings/get-currencies-success]
|
||||
:on-error [:log-rpc-error {:event :settings/get-currencies}]})))
|
||||
{:fx [[:json-rpc/call
|
||||
[{:method "appgeneral_getCurrencies"
|
||||
:on-success [:settings/get-currencies-success]
|
||||
:on-error [:log-rpc-error {:event :settings/get-currencies}]}]]]}))
|
||||
|
||||
@@ -9,25 +9,45 @@
|
||||
[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
|
||||
[message]
|
||||
(let [parsed-text (get-in message [:content :parsed-text])
|
||||
parsed-text-children (:children (first parsed-text))]
|
||||
(into [quo/text
|
||||
{:number-of-lines 1
|
||||
:style style/tag-text
|
||||
:accessibility-label :activity-message-body
|
||||
:size :paragraph-1}]
|
||||
(map-indexed (fn [index {:keys [type literal destination]}]
|
||||
^{:key index}
|
||||
(case type
|
||||
"mention" [quo/text
|
||||
{:style style/mention-text
|
||||
:size :paragraph-1}
|
||||
(str "@" (rf/sub [:messages/resolve-mention literal]))]
|
||||
"link" destination
|
||||
literal))
|
||||
parsed-text-children))))
|
||||
[{: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]
|
||||
@@ -41,12 +61,12 @@
|
||||
|
||||
(defn view
|
||||
[{:keys [notification extra-fn]}]
|
||||
(let [{:keys [author chat-name community-id chat-id
|
||||
message read timestamp]} 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])]
|
||||
(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 []
|
||||
@@ -74,4 +94,6 @@
|
||||
:group-name chat-name
|
||||
:blur? true
|
||||
:size 24}])]
|
||||
:message {:body (message-body message)}}]]]))
|
||||
:message {:body (if album-messages
|
||||
[album-message album-messages]
|
||||
[simple-message message])}}]]]))
|
||||
|
||||
@@ -24,35 +24,32 @@
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn notification-component
|
||||
[{:keys [type] :as notification} index]
|
||||
[{notification-type :type :as notification} index]
|
||||
(let [extra-fn (rn/use-callback
|
||||
(fn []
|
||||
{:notification notification})
|
||||
[notification])
|
||||
props {:notification notification
|
||||
:extra-fn extra-fn}]
|
||||
;; Notifications are expensive to render. Without `delay-render` the opening
|
||||
;; animation of the Activity Center can be clunky and the time to open the
|
||||
;; AC after pressing the bell icon can be high.
|
||||
[rn/view {:style (style/notification-container index)}
|
||||
(cond
|
||||
(= type types/contact-verification)
|
||||
(= notification-type types/contact-verification)
|
||||
[contact-verification/view props]
|
||||
|
||||
(= type types/contact-request)
|
||||
(= notification-type types/contact-request)
|
||||
[contact-requests/view props]
|
||||
|
||||
(= type types/mention)
|
||||
(= notification-type types/mention)
|
||||
[mentions/view props]
|
||||
|
||||
(= type types/reply)
|
||||
(= notification-type types/reply)
|
||||
[reply/view props]
|
||||
|
||||
(= type types/admin)
|
||||
(= notification-type types/admin)
|
||||
[admin/view props]
|
||||
|
||||
(some types/membership [type])
|
||||
(condp = type
|
||||
(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]
|
||||
|
||||
@@ -70,10 +70,10 @@
|
||||
(when pass-through?
|
||||
[reanimated/blur-view (blur-overlay-params bottom-tabs-blur-overlay-style)])
|
||||
[rn/view {:style (style/bottom-tabs)}
|
||||
[gesture/gesture-detector {:gesture communities-double-tab-gesture}
|
||||
[bottom-tab :i/communities :communities-stack shared-values notifications-data]]
|
||||
[bottom-tab :i/wallet :wallet-stack shared-values notifications-data]
|
||||
[gesture/gesture-detector {:gesture messages-double-tap-gesture}
|
||||
[bottom-tab :i/messages :chats-stack shared-values notifications-data]]
|
||||
[bottom-tab :i/wallet :wallet-stack shared-values notifications-data]
|
||||
[gesture/gesture-detector {:gesture communities-double-tab-gesture}
|
||||
[bottom-tab :i/communities :communities-stack shared-values notifications-data]]
|
||||
(when config/show-not-implemented-features?
|
||||
[bottom-tab :i/browser :browser-stack shared-values notifications-data])]]]))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
(def ^:const switcher-card-size 160)
|
||||
(def ^:const floating-shell-button-height 44)
|
||||
|
||||
(def ^:const default-selected-stack :communities-stack)
|
||||
(def ^:const default-selected-stack :wallet-stack)
|
||||
|
||||
;; Bottom tabs
|
||||
(def ^:const bottom-tabs-container-height-android 57)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[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 :as wc-utils]
|
||||
[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]
|
||||
@@ -100,7 +100,7 @@
|
||||
nil
|
||||
|
||||
(and
|
||||
(wc-utils/valid-uri? scanned-text)
|
||||
(wc-uri/valid-uri? scanned-text)
|
||||
(ff/enabled? ::ff/wallet.wallet-connect))
|
||||
(handle-wallet-connect scanned-text)
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
switcher-type :account-options
|
||||
type :no-title}}]
|
||||
(let [{:keys [color emoji watch-only?]} (rf/sub [:wallet/current-viewing-account])
|
||||
networks (rf/sub [:wallet/selected-network-details])]
|
||||
networks (rf/sub [:wallet/selected-network-details])
|
||||
sending-collectible? (rf/sub [:wallet/sending-collectible?])]
|
||||
[quo/page-nav
|
||||
{:type type
|
||||
:icon-name icon-name
|
||||
@@ -41,8 +42,9 @@
|
||||
(not watch-only?))
|
||||
{:icon-name :i/dapps
|
||||
:on-press #(rf/dispatch [:navigate-to :screen/wallet.connected-dapps])})
|
||||
{:content-type :account-switcher
|
||||
:customization-color color
|
||||
:on-press #(on-dapps-press switcher-type)
|
||||
:emoji emoji
|
||||
:type (when watch-only? :watch-only)}]}]))
|
||||
(when-not sending-collectible?
|
||||
{:content-type :account-switcher
|
||||
:customization-color color
|
||||
:on-press #(on-dapps-press switcher-type)
|
||||
:emoji emoji
|
||||
:type (when watch-only? :watch-only)})]}]))
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
:collectible-name (if (> amount 1)
|
||||
(str amount " " nft-name)
|
||||
nft-name)
|
||||
:collectible-number token-id}
|
||||
:collectible-number (when (not= token-id "0")
|
||||
token-id)}
|
||||
:second-tag-prefix :t/from
|
||||
:second-tag {:type :address :address sender}
|
||||
:third-tag-prefix :t/to
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
(ns status-im.contexts.wallet.common.utils
|
||||
(:require [clojure.string :as string]
|
||||
[native-module.core :as native-module]
|
||||
[quo.foundations.resources :as resources]
|
||||
[status-im.common.qr-codes.view :as qr-codes]
|
||||
[status-im.constants :as constants]
|
||||
[utils.hex :as utils.hex]
|
||||
[utils.money :as money]
|
||||
[utils.number :as number]
|
||||
[utils.string]))
|
||||
@@ -348,3 +350,131 @@
|
||||
[tokens]
|
||||
(let [priority #(get constants/token-sort-priority (:symbol %) ##Inf)]
|
||||
(sort-by (juxt (comp - :balance) priority) tokens)))
|
||||
|
||||
(defn- transaction-data
|
||||
[{:keys [from-address to-address token-address route data eth-transfer?]}]
|
||||
(let [{:keys [amount-in gas-amount gas-fees]} route
|
||||
eip-1559-enabled? (:eip-1559-enabled gas-fees)
|
||||
{:keys [gas-price max-fee-per-gas-medium
|
||||
max-priority-fee-per-gas]} gas-fees]
|
||||
(cond-> {:From from-address
|
||||
:To (or token-address to-address)
|
||||
:Gas (money/to-hex gas-amount)
|
||||
:Value (when eth-transfer? amount-in)
|
||||
:Nonce nil
|
||||
:Input ""
|
||||
:Data (or data "0x")}
|
||||
eip-1559-enabled? (assoc
|
||||
:TxType "0x02"
|
||||
:MaxFeePerGas
|
||||
(money/to-hex
|
||||
(money/->wei
|
||||
:gwei
|
||||
max-fee-per-gas-medium))
|
||||
:MaxPriorityFeePerGas
|
||||
(money/to-hex
|
||||
(money/->wei
|
||||
:gwei
|
||||
max-priority-fee-per-gas)))
|
||||
(not eip-1559-enabled?) (assoc :TxType "0x00"
|
||||
:GasPrice
|
||||
(money/to-hex
|
||||
(money/->wei
|
||||
:gwei
|
||||
gas-price))))))
|
||||
|
||||
(defn approval-path
|
||||
[{:keys [route from-address to-address token-address]}]
|
||||
(let [{:keys [from]} route
|
||||
from-chain-id (:chain-id from)
|
||||
approval-amount-required (:approval-amount-required route)
|
||||
approval-amount-required-sanitized (-> approval-amount-required
|
||||
(utils.hex/normalize-hex)
|
||||
(native-module/hex-to-number))
|
||||
approval-contract-address (:approval-contract-address route)
|
||||
data (native-module/encode-function-call
|
||||
constants/contract-function-signature-erc20-approve
|
||||
[approval-contract-address
|
||||
approval-amount-required-sanitized])
|
||||
tx-data (transaction-data {:from-address from-address
|
||||
:to-address to-address
|
||||
:token-address token-address
|
||||
:route route
|
||||
:data data
|
||||
:eth-transfer? false})]
|
||||
{:BridgeName constants/bridge-name-transfer
|
||||
:ChainID from-chain-id
|
||||
:TransferTx tx-data}))
|
||||
|
||||
(defn transaction-path
|
||||
[{:keys [from-address to-address token-id-from token-address token-id-to route data
|
||||
slippage-percentage eth-transfer?]}]
|
||||
(let [{:keys [bridge-name amount-in bonder-fees from
|
||||
to]} route
|
||||
tx-data (transaction-data {:from-address from-address
|
||||
:to-address to-address
|
||||
:token-address token-address
|
||||
:route route
|
||||
:data data
|
||||
:eth-transfer? eth-transfer?})
|
||||
to-chain-id (:chain-id to)
|
||||
from-chain-id (:chain-id from)]
|
||||
(cond-> {:BridgeName bridge-name
|
||||
:ChainID from-chain-id}
|
||||
|
||||
(= bridge-name constants/bridge-name-erc-721-transfer)
|
||||
(assoc :ERC721TransferTx
|
||||
(assoc tx-data
|
||||
:Recipient to-address
|
||||
:TokenID token-id-from
|
||||
:ChainID to-chain-id))
|
||||
|
||||
(= bridge-name constants/bridge-name-erc-1155-transfer)
|
||||
(assoc :ERC1155TransferTx
|
||||
(assoc tx-data
|
||||
:Recipient to-address
|
||||
:TokenID token-id-from
|
||||
:ChainID to-chain-id
|
||||
:Amount amount-in))
|
||||
|
||||
(= bridge-name constants/bridge-name-transfer)
|
||||
(assoc :TransferTx tx-data)
|
||||
|
||||
(= bridge-name constants/bridge-name-hop)
|
||||
(assoc :HopTx
|
||||
(assoc tx-data
|
||||
:ChainID from-chain-id
|
||||
:ChainIDTo to-chain-id
|
||||
:Symbol token-id-from
|
||||
:Recipient to-address
|
||||
:Amount amount-in
|
||||
:BonderFee bonder-fees))
|
||||
|
||||
(= bridge-name constants/bridge-name-paraswap)
|
||||
(assoc :SwapTx
|
||||
(assoc tx-data
|
||||
:ChainID from-chain-id
|
||||
:ChainIDTo to-chain-id
|
||||
:TokenIDFrom token-id-from
|
||||
:TokenIDTo token-id-to
|
||||
:SlippagePercentage slippage-percentage))
|
||||
|
||||
(not (or (= bridge-name constants/bridge-name-erc-721-transfer)
|
||||
(= bridge-name constants/bridge-name-transfer)
|
||||
(= bridge-name constants/bridge-name-hop)))
|
||||
(assoc :CbridgeTx
|
||||
(assoc tx-data
|
||||
:ChainID to-chain-id
|
||||
:Symbol token-id-from
|
||||
:Recipient to-address
|
||||
:Amount amount-in)))))
|
||||
|
||||
(defn multi-transaction-command
|
||||
[{:keys [from-address to-address from-asset to-asset amount-out multi-transaction-type]
|
||||
:or {multi-transaction-type constants/multi-transaction-type-unknown}}]
|
||||
{:fromAddress from-address
|
||||
:toAddress to-address
|
||||
:fromAsset from-asset
|
||||
:toAsset to-asset
|
||||
:fromAmount amount-out
|
||||
:type multi-transaction-type})
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[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)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[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.core :as core]
|
||||
[status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.string]))
|
||||
@@ -107,9 +107,14 @@
|
||||
:content-container-style (style/dapps-list theme)
|
||||
:render-fn (fn [{:keys [topic pairingTopic name url iconUrl]}]
|
||||
[quo/dapp
|
||||
{:dapp {:avatar (core/compute-dapp-icon-path iconUrl
|
||||
url)
|
||||
:name (core/compute-dapp-name name url)
|
||||
{: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
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[status-im.contexts.wallet.data-store :as data-store]
|
||||
[status-im.contexts.wallet.db :as db]
|
||||
[status-im.contexts.wallet.item-types :as item-types]
|
||||
[status-im.contexts.wallet.tokens.events]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.collection]
|
||||
[utils.ethereum.chain :as chain]
|
||||
@@ -85,8 +86,9 @@
|
||||
(rf/reg-event-fx :wallet/close-account-page
|
||||
(fn [{:keys [db]}]
|
||||
(let [just-completed-transaction? (get-in db [:wallet :ui :send :just-completed-transaction?])]
|
||||
(when-not just-completed-transaction?
|
||||
{:fx [[:dispatch [:wallet/clear-account-tab]]]}))))
|
||||
{:db (update db :wallet dissoc :current-viewing-account-address)
|
||||
:fx [(when-not just-completed-transaction?
|
||||
[:dispatch [:wallet/clear-account-tab]])]})))
|
||||
|
||||
(defn log-rpc-error
|
||||
[_ [{:keys [event params]} error]]
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
:size 32
|
||||
:default-active default-active
|
||||
:data data
|
||||
:on-change #(on-change %)}])
|
||||
:on-change on-change}])
|
||||
|
||||
(defn view
|
||||
[]
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[taoensso.timbre :as log]
|
||||
[utils.address :as address]
|
||||
[utils.hex :as utils.hex]
|
||||
[utils.money :as money]
|
||||
[utils.money :as utils.money]
|
||||
[utils.number]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
@@ -263,9 +263,12 @@
|
||||
|
||||
(rf/reg-event-fx
|
||||
:wallet/set-collectible-to-send
|
||||
(fn [{db :db} [{:keys [collectible current-screen start-flow?]}]]
|
||||
(fn [{db :db} [{:keys [collectible current-screen start-flow? entry-point]}]]
|
||||
(let [viewing-account? (some? (-> db :wallet :current-viewing-account-address))
|
||||
entry-point (when-not viewing-account? :wallet-stack)
|
||||
entry-point (cond
|
||||
entry-point entry-point
|
||||
viewing-account? :account-collectible-tab
|
||||
:else :wallet-stack)
|
||||
collection-data (:collection-data collectible)
|
||||
collectible-data (:collectible-data collectible)
|
||||
contract-type (:contract-type collectible)
|
||||
@@ -475,7 +478,7 @@
|
||||
:receiver-network-values receiver-network-values)
|
||||
(dissoc :network-links)
|
||||
(cond-> token (assoc :token token))))
|
||||
:json-rpc/call [{:method "wallet_getSuggestedRoutesV2Async"
|
||||
:json-rpc/call [{:method "wallet_getSuggestedRoutesAsync"
|
||||
:params params
|
||||
:on-error (fn [error]
|
||||
(rf/dispatch [:wallet/suggested-routes-error error])
|
||||
@@ -487,7 +490,7 @@
|
||||
(rf/reg-event-fx :wallet/stop-get-suggested-routes
|
||||
(fn []
|
||||
{:fx [[:dispatch [:wallet/clean-routes-calculation]]]
|
||||
:json-rpc/call [{:method "wallet_stopSuggestedRoutesV2AsyncCalcualtion"
|
||||
:json-rpc/call [{:method "wallet_stopSuggestedRoutesAsyncCalculation"
|
||||
:params []
|
||||
:on-error (fn [error]
|
||||
(log/error "failed to stop suggested routes calculation"
|
||||
@@ -518,23 +521,29 @@
|
||||
|
||||
(rf/reg-event-fx
|
||||
:wallet/handle-suggested-routes
|
||||
(fn [_ [data]]
|
||||
(if-let [{:keys [code details]} (-> data :ErrorResponse)]
|
||||
(let [error-message (if (= code "0") "An error occurred" details)]
|
||||
(log/error "failed to get suggested routes (async)"
|
||||
{:event :wallet/handle-suggested-routes
|
||||
:error error-message})
|
||||
{:fx [[:dispatch [:wallet/suggested-routes-error error-message]]]})
|
||||
(let [best-routes-fix (comp ->old-route-paths
|
||||
remove-invalid-bonder-fees-routes
|
||||
remove-multichain-routes)
|
||||
candidates-fix (comp ->old-route-paths
|
||||
remove-invalid-bonder-fees-routes)
|
||||
routes (-> data
|
||||
(data-store/rpc->suggested-routes)
|
||||
(update :best best-routes-fix)
|
||||
(update :candidates candidates-fix))]
|
||||
{:fx [[:dispatch [:wallet/suggested-routes-success routes]]]}))))
|
||||
(fn [{:keys [db]} [data]]
|
||||
(let [swap? (get-in db [:wallet :ui :swap])
|
||||
{:keys [code details] :as error-response} (-> data :ErrorResponse)]
|
||||
(if (and (not swap?) error-response)
|
||||
(let [error-message (if (= code "0") "An error occurred" details)]
|
||||
(log/error "failed to get suggested routes (async)"
|
||||
{:event :wallet/handle-suggested-routes
|
||||
:error error-message})
|
||||
{:fx [(if swap?
|
||||
[:dispatch [:wallet/swap-proposal-error error-message]]
|
||||
[:dispatch [:wallet/suggested-routes-error error-message]])]})
|
||||
(let [best-routes-fix (comp ->old-route-paths
|
||||
remove-invalid-bonder-fees-routes
|
||||
remove-multichain-routes)
|
||||
candidates-fix (comp ->old-route-paths
|
||||
remove-invalid-bonder-fees-routes)
|
||||
routes (-> data
|
||||
(data-store/rpc->suggested-routes)
|
||||
(update :best best-routes-fix)
|
||||
(update :candidates candidates-fix))]
|
||||
{:fx [(if swap?
|
||||
[:dispatch [:wallet/swap-proposal-success routes]]
|
||||
[:dispatch [:wallet/suggested-routes-success routes]])]})))))
|
||||
|
||||
(rf/reg-event-fx :wallet/add-authorized-transaction
|
||||
(fn [{:keys [db]} [transaction]]
|
||||
@@ -576,124 +585,6 @@
|
||||
[{:ms 20
|
||||
:dispatch [:wallet/clean-up-transaction-flow]}]]]})))
|
||||
|
||||
(defn- transaction-data
|
||||
[{:keys [from-address to-address token-address route data eth-transfer?]}]
|
||||
(let [{:keys [amount-in gas-amount gas-fees]} route
|
||||
eip-1559-enabled? (:eip-1559-enabled gas-fees)
|
||||
{:keys [gas-price max-fee-per-gas-medium
|
||||
max-priority-fee-per-gas]} gas-fees]
|
||||
(cond-> {:From from-address
|
||||
:To (or token-address to-address)
|
||||
:Gas (money/to-hex gas-amount)
|
||||
:Value (when eth-transfer? amount-in)
|
||||
:Nonce nil
|
||||
:Input ""
|
||||
:Data (or data "0x")}
|
||||
eip-1559-enabled? (assoc
|
||||
:TxType "0x02"
|
||||
:MaxFeePerGas
|
||||
(money/to-hex
|
||||
(money/->wei
|
||||
:gwei
|
||||
max-fee-per-gas-medium))
|
||||
:MaxPriorityFeePerGas
|
||||
(money/to-hex
|
||||
(money/->wei
|
||||
:gwei
|
||||
max-priority-fee-per-gas)))
|
||||
(not eip-1559-enabled?) (assoc :TxType "0x00"
|
||||
:GasPrice
|
||||
(money/to-hex
|
||||
(money/->wei
|
||||
:gwei
|
||||
gas-price))))))
|
||||
|
||||
(defn- approval-path
|
||||
[{:keys [route from-address to-address token-address]}]
|
||||
(let [{:keys [from]} route
|
||||
from-chain-id (:chain-id from)
|
||||
approval-amount-required (:approval-amount-required route)
|
||||
approval-amount-required-sanitized (-> approval-amount-required
|
||||
(utils.hex/normalize-hex)
|
||||
(native-module/hex-to-number))
|
||||
approval-contract-address (:approval-contract-address route)
|
||||
data (native-module/encode-function-call
|
||||
constants/contract-function-signature-erc20-approve
|
||||
[approval-contract-address
|
||||
approval-amount-required-sanitized])
|
||||
tx-data (transaction-data {:from-address from-address
|
||||
:to-address to-address
|
||||
:token-address token-address
|
||||
:route route
|
||||
:data data
|
||||
:eth-transfer? false})]
|
||||
{:BridgeName constants/bridge-name-transfer
|
||||
:ChainID from-chain-id
|
||||
:TransferTx tx-data}))
|
||||
|
||||
(defn- transaction-path
|
||||
[{:keys [from-address to-address token-id token-address route data eth-transfer?]}]
|
||||
(let [{:keys [bridge-name amount-in bonder-fees from
|
||||
to]} route
|
||||
tx-data (transaction-data {:from-address from-address
|
||||
:to-address to-address
|
||||
:token-address token-address
|
||||
:route route
|
||||
:data data
|
||||
:eth-transfer? eth-transfer?})
|
||||
to-chain-id (:chain-id to)
|
||||
from-chain-id (:chain-id from)]
|
||||
(cond-> {:BridgeName bridge-name
|
||||
:ChainID from-chain-id}
|
||||
|
||||
(= bridge-name constants/bridge-name-erc-721-transfer)
|
||||
(assoc :ERC721TransferTx
|
||||
(assoc tx-data
|
||||
:Recipient to-address
|
||||
:TokenID token-id
|
||||
:ChainID to-chain-id))
|
||||
|
||||
(= bridge-name constants/bridge-name-erc-1155-transfer)
|
||||
(assoc :ERC1155TransferTx
|
||||
(assoc tx-data
|
||||
:Recipient to-address
|
||||
:TokenID token-id
|
||||
:ChainID to-chain-id
|
||||
:Amount amount-in))
|
||||
|
||||
(= bridge-name constants/bridge-name-transfer)
|
||||
(assoc :TransferTx tx-data)
|
||||
|
||||
(= bridge-name constants/bridge-name-hop)
|
||||
(assoc :HopTx
|
||||
(assoc tx-data
|
||||
:ChainID from-chain-id
|
||||
:ChainIDTo to-chain-id
|
||||
:Symbol token-id
|
||||
:Recipient to-address
|
||||
:Amount amount-in
|
||||
:BonderFee bonder-fees))
|
||||
|
||||
(not (or (= bridge-name constants/bridge-name-erc-721-transfer)
|
||||
(= bridge-name constants/bridge-name-transfer)
|
||||
(= bridge-name constants/bridge-name-hop)))
|
||||
(assoc :CbridgeTx
|
||||
(assoc tx-data
|
||||
:ChainID to-chain-id
|
||||
:Symbol token-id
|
||||
:Recipient to-address
|
||||
:Amount amount-in)))))
|
||||
|
||||
(defn- multi-transaction-command
|
||||
[{:keys [from-address to-address from-asset to-asset amount-out multi-transaction-type]
|
||||
:or {multi-transaction-type constants/multi-transaction-type-unknown}}]
|
||||
{:fromAddress from-address
|
||||
:toAddress to-address
|
||||
:fromAsset from-asset
|
||||
:toAsset to-asset
|
||||
:fromAmount amount-out
|
||||
:type multi-transaction-type})
|
||||
|
||||
(rf/reg-event-fx :wallet/send-transaction
|
||||
(fn [{:keys [db]} [sha3-pwd]]
|
||||
(let [routes (get-in db [:wallet :ui :send :route])
|
||||
@@ -725,27 +616,27 @@
|
||||
(native-module/encode-transfer
|
||||
(address/normalized-hex to-address)
|
||||
(:amount-in route)))
|
||||
base-path (transaction-path
|
||||
base-path (utils/transaction-path
|
||||
{:to-address to-address
|
||||
:from-address from-address
|
||||
:route route
|
||||
:token-address token-address
|
||||
:token-id (if collectible
|
||||
:token-id-from (if collectible
|
||||
(utils.hex/number-to-hex
|
||||
token-id)
|
||||
token-id)
|
||||
:data data
|
||||
:eth-transfer? eth-transfer?})]
|
||||
(if approval-required?
|
||||
[(approval-path {:route route
|
||||
:token-address token-address
|
||||
:from-address from-address
|
||||
:to-address to-address})
|
||||
[(utils/approval-path {:route route
|
||||
:token-address token-address
|
||||
:from-address from-address
|
||||
:to-address to-address})
|
||||
base-path]
|
||||
[base-path]))))
|
||||
routes)
|
||||
request-params
|
||||
[(multi-transaction-command
|
||||
[(utils/multi-transaction-command
|
||||
{:from-address from-address
|
||||
:to-address to-address
|
||||
:from-asset token-id
|
||||
@@ -814,3 +705,27 @@
|
||||
{:current-screen stack-id
|
||||
:start-flow? start-flow?
|
||||
:flow-id flow-id}]]]})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:wallet/transaction-confirmation-navigate-back
|
||||
(fn [{db :db} [{:keys []}]]
|
||||
(let [tx-type (-> db :wallet :ui :send :tx-type)
|
||||
keep-tx-data? (#{:account-collectible-tab :wallet-stack}
|
||||
(-> db :wallet :ui :send :entry-point))]
|
||||
{:db (cond-> db
|
||||
(and (= tx-type :tx/collectible-erc-721) (not keep-tx-data?))
|
||||
(update-in [:wallet :ui :send] dissoc :tx-type :amount :route :suggested-routes)
|
||||
|
||||
(= tx-type :tx/collectible-erc-1155)
|
||||
(update-in [:wallet :ui :send] dissoc :route :suggested-routes))
|
||||
:fx [[:dispatch [:navigate-back]]]})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:wallet/collectible-amount-navigate-back
|
||||
(fn [{db :db} [{:keys []}]]
|
||||
(let [keep-tx-data? (#{:account-collectible-tab :wallet-stack}
|
||||
(-> db :wallet :ui :send :entry-point))]
|
||||
{:db (cond-> db
|
||||
:always (update-in [:wallet :ui :send] dissoc :amount :route)
|
||||
(not keep-tx-data?) (update-in [:wallet :ui :send] dissoc :tx-type))
|
||||
:fx [[:dispatch [:navigate-back]]]})))
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
:skip-step? (fn [db] (or (token-selected? db) (collectible-selected? db)))}
|
||||
{:screen-id :screen/wallet.send-input-amount
|
||||
:skip-step? (fn [db]
|
||||
(send-utils/tx-type-collectible? (get-in db [:wallet :ui :send :tx-type])))}
|
||||
(-> db :wallet :ui :send :tx-type send-utils/tx-type-collectible?))}
|
||||
{:screen-id :screen/wallet.select-collectible-amount
|
||||
:skip-step? (fn [db]
|
||||
(or (not (collectible-selected? db))
|
||||
|
||||
@@ -98,9 +98,10 @@
|
||||
:related-chain-id 1
|
||||
:layer 1}]
|
||||
:wallet/wallet-send-enabled-from-chain-ids [1]
|
||||
:wallet/wallet-send-amount 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/total-amount (money/bignumber "250")})
|
||||
|
||||
(h/describe "Send > input amount screen"
|
||||
|
||||
@@ -262,8 +262,8 @@
|
||||
(string/upper-case
|
||||
constants/mainnet-short-name))
|
||||
(money/equal-to
|
||||
(controlled-input/numeric-value input-state)
|
||||
(controlled-input/upper-limit input-state))
|
||||
(controlled-input/value-bn input-state)
|
||||
(controlled-input/upper-limit-bn input-state))
|
||||
(money/equal-to (:total-balance
|
||||
owned-eth-token)
|
||||
0)))
|
||||
@@ -338,10 +338,10 @@
|
||||
{:limit (if crypto-currency?
|
||||
(utils/prettify-crypto-balance
|
||||
(or (clj->js token-symbol) "")
|
||||
(controlled-input/upper-limit input-state)
|
||||
(controlled-input/upper-limit-bn input-state)
|
||||
conversion-rate)
|
||||
(utils/prettify-balance currency-symbol
|
||||
(controlled-input/upper-limit
|
||||
(controlled-input/upper-limit-bn
|
||||
input-state)))})
|
||||
:status (when (controlled-input/input-error input-state) :error)}]}]
|
||||
[routes/view
|
||||
|
||||
@@ -9,6 +9,25 @@
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn other-account-item
|
||||
[{:keys [address color emoji network-preferences-names]
|
||||
account-name :name
|
||||
:as account}]
|
||||
(let [full-address (rf/sub [:wallet/account-address address network-preferences-names])]
|
||||
[quo/account-item
|
||||
{:account-props (assoc account
|
||||
:customization-color color
|
||||
:address full-address
|
||||
:full-address? true)
|
||||
: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])]
|
||||
@@ -20,22 +39,8 @@
|
||||
:container-style style/empty-container-style}]
|
||||
[rn/view {:style style/my-accounts-container}
|
||||
(doall
|
||||
(for [{:keys [color address] :as account} other-accounts]
|
||||
^{:key (str address)}
|
||||
(let [transformed-address (rf/sub [:wallet/account-address address
|
||||
(:network-preferences-names account)])]
|
||||
[quo/account-item
|
||||
{:account-props (assoc account
|
||||
:customization-color color
|
||||
:address transformed-address
|
||||
:full-address? true)
|
||||
:on-press #(rf/dispatch [:wallet/select-send-address
|
||||
{:address address
|
||||
:recipient {:recipient-type :account
|
||||
:label (:name account)
|
||||
:customization-color (:color account)
|
||||
:emoji (:emoji account)}
|
||||
:stack-id :screen/wallet.select-address}])}])))])))
|
||||
(for [{:keys [address] :as account} other-accounts]
|
||||
^{:key (str address)} [other-account-item account]))])))
|
||||
|
||||
(defn- recent-transactions
|
||||
[theme]
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
(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/wallet-send-recipient])
|
||||
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])]
|
||||
@@ -191,7 +191,7 @@
|
||||
[floating-button-page/view
|
||||
{:content-container-style {:flex 1}
|
||||
:footer-container-padding 0
|
||||
:keyboard-should-persist-taps true
|
||||
:keyboard-should-persist-taps :always
|
||||
:header [account-switcher/view
|
||||
{:on-press #(rf/dispatch [:navigate-back])
|
||||
:margin-top (safe-area/get-top)
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
:on-collectible-press (fn [{:keys [collectible]}]
|
||||
(rf/dispatch [:wallet/set-collectible-to-send
|
||||
{:collectible collectible
|
||||
:current-screen :screen/wallet.select-asset}]))}]))
|
||||
:current-screen :screen/wallet.select-asset
|
||||
:entry-point :account-send-button}]))}]))
|
||||
|
||||
(defn- tab-view
|
||||
[search-text selected-tab on-change-text]
|
||||
|
||||
@@ -11,23 +11,26 @@
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [on-close (rn/use-callback #(rf/dispatch [:navigate-back]))
|
||||
send-transaction-data (rf/sub [:wallet/wallet-send])
|
||||
collectible (:collectible send-transaction-data)
|
||||
balance (utils/collectible-balance collectible)
|
||||
[value set-value] (rn/use-state (-> controlled-input/init-state
|
||||
(controlled-input/set-numeric-value 1)
|
||||
(controlled-input/set-lower-limit 1)))
|
||||
preview-uri (get-in collectible [:preview-url :uri])
|
||||
incorrect-value? (controlled-input/input-error value)
|
||||
increase-value (rn/use-callback #(set-value controlled-input/increase))
|
||||
decrease-value (rn/use-callback #(set-value controlled-input/decrease))
|
||||
delete-character (rn/use-callback #(set-value controlled-input/delete-last))
|
||||
add-character (rn/use-callback (fn [c]
|
||||
(set-value #(controlled-input/add-character % c))))]
|
||||
(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)
|
||||
balance (utils/collectible-balance collectible)
|
||||
[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-value #(controlled-input/set-upper-limit % balance)))
|
||||
(set-input-state #(controlled-input/set-upper-limit % balance)))
|
||||
[balance])
|
||||
[rn/view
|
||||
[account-switcher/view
|
||||
@@ -45,9 +48,9 @@
|
||||
:status (if incorrect-value? :error :default)
|
||||
:container-style style/network-tags-container}]
|
||||
[quo/amount-input
|
||||
{:max-value (controlled-input/upper-limit value)
|
||||
:min-value (controlled-input/lower-limit value)
|
||||
:value (controlled-input/numeric-value value)
|
||||
{: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
|
||||
@@ -57,7 +60,7 @@
|
||||
:button-one-props {:on-press #(rf/dispatch
|
||||
[:wallet/set-collectible-amount-to-send
|
||||
{:stack-id :screen/wallet.select-collectible-amount
|
||||
:amount (controlled-input/numeric-value value)}])
|
||||
:amount (controlled-input/value-numeric input-state)}])
|
||||
:disabled? incorrect-value?}
|
||||
:button-one-label (i18n/label :t/confirm)}]
|
||||
[quo/numbered-keyboard
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
|
||||
(defn view
|
||||
[_]
|
||||
(let [on-close #(rf/dispatch [:navigate-back])]
|
||||
(let [on-close #(rf/dispatch [:wallet/transaction-confirmation-navigate-back])]
|
||||
(fn []
|
||||
(let [theme (quo.theme/use-theme)
|
||||
send-transaction-data (rf/sub [:wallet/wallet-send])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns status-im.contexts.wallet.signals
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[status-im.constants :as constants]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.transforms :as transforms]))
|
||||
@@ -8,9 +9,33 @@
|
||||
(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)]
|
||||
{:db (update-in db [:wallet :transactions tx-hash] assoc :status :confirmed :blocks 1)})))
|
||||
(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)
|
||||
swap-approval-transaction-id (get-in db [:wallet :ui :swap :approval-transaction-id])
|
||||
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
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
(ns status-im.contexts.wallet.swap.events
|
||||
(:require [re-frame.core :as rf]
|
||||
(:require [native-module.core :as native-module]
|
||||
[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.sheets.network-selection.view :as network-selection]
|
||||
[utils.number]))
|
||||
[taoensso.timbre :as log]
|
||||
[utils.address :as address]
|
||||
[utils.debounce :as debounce]
|
||||
[utils.hex :as hex]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.number]
|
||||
[utils.number :as number]))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/start
|
||||
(fn [{:keys [_db]}]
|
||||
@@ -14,7 +23,9 @@
|
||||
(assoc-in [:wallet :ui :swap :asset-to-pay] token)
|
||||
(assoc-in [:wallet :ui :swap :network] network))
|
||||
:fx (if network
|
||||
[[:dispatch [:navigate-to :screen/wallet.swap-propasal]]
|
||||
[[:dispatch
|
||||
[:navigate-to-within-stack
|
||||
[:screen/wallet.setup-swap :screen/wallet.swap-select-asset-to-pay]]]
|
||||
[:dispatch [:wallet.swap/set-default-slippage]]]
|
||||
[[:dispatch
|
||||
[:show-bottom-sheet
|
||||
@@ -30,14 +41,9 @@
|
||||
:stack-id
|
||||
:screen/wallet.swap-select-asset-to-pay}]))}])}]]])}))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/clean-asset-to-pay
|
||||
(fn [{:keys [db]}]
|
||||
{:db (update-in db [:wallet :ui :swap] dissoc :asset-to-pay)}))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/set-default-slippage
|
||||
(fn [{:keys [db]}]
|
||||
{:db
|
||||
(assoc-in db [:wallet :ui :swap :max-slippage] constants/default-slippage)}))
|
||||
{:db (assoc-in db [:wallet :ui :swap :max-slippage] constants/default-slippage)}))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/set-max-slippage
|
||||
(fn [{:keys [db]} [max-slippage]]
|
||||
@@ -47,18 +53,318 @@
|
||||
(fn [{:keys [db]} [{:keys [token]}]]
|
||||
{:db (assoc-in db [:wallet :ui :swap :asset-to-receive] token)}))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/set-pay-amount
|
||||
(fn [{:keys [db]} [amount]]
|
||||
{:db (assoc-in db [:wallet :ui :swap :pay-amount] amount)}))
|
||||
(rf/reg-event-fx :wallet/start-get-swap-proposal
|
||||
(fn [{:keys [db]} [{:keys [amount-in amount-out]}]]
|
||||
(let [wallet-address (get-in db [:wallet :current-viewing-account-address])
|
||||
{:keys [asset-to-pay asset-to-receive
|
||||
network]} (get-in db [:wallet :ui :swap])
|
||||
test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?])
|
||||
networks ((if test-networks-enabled? :test :prod)
|
||||
(get-in db [:wallet :networks]))
|
||||
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)
|
||||
receive-token-decimals (:decimals asset-to-receive)
|
||||
gas-rates constants/gas-rate-medium
|
||||
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
|
||||
request-uuid (str (random-uuid))
|
||||
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}
|
||||
amount-in (assoc :amountIn amount-in-hex)
|
||||
amount-out (assoc :amountOut amount-out-hex))]]
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/set-swap-proposal
|
||||
(when-let [amount (or amount-in amount-out)]
|
||||
{:db (update-in db
|
||||
[:wallet :ui :swap]
|
||||
#(-> %
|
||||
(assoc
|
||||
:last-request-uuid request-uuid
|
||||
:amount amount
|
||||
:loading-swap-proposal? true)
|
||||
(dissoc :error-response)))
|
||||
: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]]
|
||||
{:db (assoc-in db [:wallet :ui :swap :swap-proposal] swap-proposal)}))
|
||||
(let [last-request-uuid (get-in db [:wallet :ui :swap :last-request-uuid])
|
||||
view-id (:view-id db)
|
||||
request-uuid (:uuid swap-proposal)
|
||||
best-routes (:best swap-proposal)
|
||||
error-response (:error-response swap-proposal)]
|
||||
(when (= request-uuid last-request-uuid)
|
||||
(cond-> {:db (update-in db
|
||||
[:wallet :ui :swap]
|
||||
assoc
|
||||
:swap-proposal (first best-routes)
|
||||
:error-response (when (empty? best-routes) error-response)
|
||||
:loading-swap-proposal? false)}
|
||||
;; 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 [[:dismiss-modal :screen/wallet.swap-set-spending-cap]])
|
||||
(and (empty? best-routes) (= view-id :screen/wallet.swap-confirmation))
|
||||
(assoc :fx [[:navigate-back]]))))))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/set-provider
|
||||
(rf/reg-event-fx :wallet/swap-proposal-error
|
||||
(fn [{:keys [db]} [error-message]]
|
||||
{:db (-> db
|
||||
(update-in [:wallet :ui :swap] dissoc :route :swap-proposal)
|
||||
(assoc-in [:wallet :ui :swap :loading-swap-proposal?] false)
|
||||
(assoc-in [:wallet :ui :swap :error-response] error-message))
|
||||
:fx [[:dispatch
|
||||
[:toasts/upsert
|
||||
{:id :swap-proposal-error
|
||||
:type :negative
|
||||
:text error-message}]]]}))
|
||||
|
||||
(rf/reg-event-fx :wallet/stop-get-swap-proposal
|
||||
(fn []
|
||||
{: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]}]
|
||||
{:db (assoc-in db [:wallet :ui :swap :providers] [constants/swap-default-provider])}))
|
||||
{:db (update-in db
|
||||
[:wallet :ui :swap]
|
||||
dissoc
|
||||
:last-request-uuid
|
||||
:swap-proposal
|
||||
:error-response
|
||||
:loading-swap-proposal?
|
||||
:approval-transaction-id)}))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/recalculate-fees
|
||||
(fn [{:keys [db]} [loading-fees?]]
|
||||
{:db (assoc-in db [:wallet :ui :swap :loading-fees?] loading-fees?)}))
|
||||
(rf/reg-event-fx :wallet/clean-swap
|
||||
(fn [{:keys [db]}]
|
||||
{:db (update-in db [:wallet :ui] dissoc :swap)}))
|
||||
|
||||
(rf/reg-event-fx :wallet/swap-transaction
|
||||
(fn [{:keys [db]} [sha3-pwd]]
|
||||
(let [wallet-address (get-in db
|
||||
[:wallet
|
||||
:current-viewing-account-address])
|
||||
{:keys [asset-to-pay asset-to-receive
|
||||
swap-proposal network amount
|
||||
approval-transaction-id
|
||||
max-slippage]} (get-in db [:wallet :ui :swap])
|
||||
transactions (get-in db [:wallet :transactions])
|
||||
approval-transaction (when approval-transaction-id
|
||||
(get transactions approval-transaction-id))
|
||||
already-approved? (and approval-transaction
|
||||
(= (:status approval-transaction)
|
||||
:confirmed))
|
||||
approval-required? (and (:approval-required swap-proposal)
|
||||
(not already-approved?))
|
||||
multi-transaction-type constants/multi-transaction-type-swap
|
||||
swap-chain-id (:chain-id network)
|
||||
token-id-from (:symbol asset-to-pay)
|
||||
token-id-to (:symbol asset-to-receive)
|
||||
erc20-transfer? (and asset-to-pay (not= token-id-from "ETH"))
|
||||
eth-transfer? (and asset-to-pay (not erc20-transfer?))
|
||||
token-address (when erc20-transfer?
|
||||
(get-in asset-to-pay
|
||||
[:balances-per-chain swap-chain-id
|
||||
:address]))
|
||||
data (when erc20-transfer?
|
||||
(native-module/encode-transfer
|
||||
(address/normalized-hex wallet-address)
|
||||
(:amount-in swap-proposal)))
|
||||
transaction-paths (if approval-required?
|
||||
[(utils/approval-path
|
||||
{:route swap-proposal
|
||||
:token-address token-address
|
||||
:from-address wallet-address
|
||||
:to-address wallet-address})]
|
||||
[(utils/transaction-path
|
||||
{:to-address wallet-address
|
||||
:from-address wallet-address
|
||||
:route swap-proposal
|
||||
:token-address token-address
|
||||
:token-id-from token-id-from
|
||||
:token-id-to token-id-to
|
||||
:data data
|
||||
:slippage-percentage max-slippage
|
||||
:eth-transfer? eth-transfer?})])
|
||||
request-params [(utils/multi-transaction-command
|
||||
{:from-address wallet-address
|
||||
:to-address wallet-address
|
||||
:from-asset token-id-from
|
||||
:to-asset (if approval-required?
|
||||
token-id-from
|
||||
token-id-to)
|
||||
:amount-out (if eth-transfer?
|
||||
(:amount-out swap-proposal)
|
||||
"0x0")
|
||||
:multi-transaction-type
|
||||
multi-transaction-type})
|
||||
transaction-paths
|
||||
sha3-pwd]]
|
||||
(log/info "multi transaction called")
|
||||
{:json-rpc/call [{:method "wallet_createMultiTransaction"
|
||||
:params request-params
|
||||
:on-success (fn [result]
|
||||
(when result
|
||||
(let [receive-token-decimals (:decimals asset-to-receive)
|
||||
amount-out (:amount-out swap-proposal)
|
||||
decimals-to-display
|
||||
(min
|
||||
receive-token-decimals
|
||||
constants/min-token-decimals-to-display)
|
||||
receive-amount (when amount-out
|
||||
(number/remove-trailing-zeroes
|
||||
(.toFixed (number/convert-to-whole-number
|
||||
(native-module/hex-to-number
|
||||
(hex/normalize-hex
|
||||
amount-out))
|
||||
receive-token-decimals)
|
||||
decimals-to-display)))]
|
||||
(rf/dispatch [:wallet.swap/add-authorized-transaction
|
||||
(cond-> {:transaction result
|
||||
: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}))])
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(rf/dispatch [:dismiss-modal
|
||||
(if approval-required?
|
||||
:screen/wallet.swap-set-spending-cap
|
||||
:screen/wallet.swap-confirmation)])
|
||||
(when-not approval-required?
|
||||
(rf/dispatch [:wallet/select-account-tab :activity])
|
||||
(debounce/debounce-and-dispatch [:wallet/clean-swap] 1000)
|
||||
(debounce/debounce-and-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})}]
|
||||
500)))))
|
||||
:on-error (fn [error]
|
||||
(log/error "failed swap transaction"
|
||||
{:event :wallet/swap-transaction
|
||||
:error error
|
||||
:params request-params})
|
||||
(rf/dispatch [:toasts/upsert
|
||||
{:id :swap-transaction-error
|
||||
:type :negative
|
||||
:text (:message error)}]))}]})))
|
||||
|
||||
(rf/reg-event-fx :wallet.swap/add-authorized-transaction
|
||||
(fn [{:keys [db]} [{:keys [transaction swap-data approval-transaction?]}]]
|
||||
(let [transactions (get-in db [:wallet :transactions] {})
|
||||
transaction-batch-id (:id transaction)
|
||||
transaction-hashes (:hashes transaction)
|
||||
transaction-ids (flatten (vals transaction-hashes))
|
||||
transaction-id (first transaction-ids)
|
||||
transaction-details (cond-> (send-utils/map-multitransaction-by-ids transaction-batch-id
|
||||
transaction-hashes)
|
||||
: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 transactions transaction-details))
|
||||
:always (assoc-in [:wallet :ui :swap :transaction-ids] transaction-ids)
|
||||
approval-transaction? (assoc-in [:wallet :ui :swap :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]} (get-in db [:wallet :ui :swap])
|
||||
provider-name (:bridge-name swap-proposal)
|
||||
token-symbol (:symbol asset-to-pay)
|
||||
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
|
||||
[:toasts/upsert
|
||||
{:id :approve-transaction-update
|
||||
:type (if transaction-confirmed? :positive :negative)
|
||||
:text (if transaction-confirmed?
|
||||
(i18n/label :t/spending-cap-set
|
||||
{:amount amount
|
||||
:token-symbol token-symbol
|
||||
:provider-name provider-name
|
||||
:account-name account-name})
|
||||
(i18n/label :t/spending-cap-failed
|
||||
{:amount amount
|
||||
:token-symbol token-symbol
|
||||
:provider-name provider-name
|
||||
:account-name account-name}))}]]]}
|
||||
(not transaction-confirmed?)
|
||||
(assoc :db (update-in db [:wallet :ui :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]} (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
|
||||
[: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))}]]]}))))
|
||||
|
||||
@@ -18,36 +18,18 @@
|
||||
:value search-text
|
||||
:on-change-text on-change-text}]])
|
||||
|
||||
(def dummy-swap-proposal
|
||||
{:from {:chain-id 1
|
||||
:native-currency-symbol "ETH"}
|
||||
:to {:chain-id 1
|
||||
:native-currency-symbol "ETH"}
|
||||
:gas-amount "23487"
|
||||
:gas-fees {:base-fee "32.325296406"
|
||||
:max-priority-fee-per-gas "0.011000001"
|
||||
:eip1559-enabled true}
|
||||
:estimated-time 3
|
||||
:receive-amount "99.98"
|
||||
:pay-token {:symbol "SNT"
|
||||
:address "0x432492384728934239789"}
|
||||
:receive-token {:symbol "USDT"
|
||||
:address "0x432492384728934239789"}})
|
||||
|
||||
(defn- assets-view
|
||||
[search-text on-change-text]
|
||||
(let [on-token-press (fn [token]
|
||||
(let [token-networks (:networks token)
|
||||
asset-to-receive (rf/sub [:wallet/token-by-symbol "SNT"])]
|
||||
asset-to-receive (rf/sub [:wallet/token-by-symbol "DAI"])]
|
||||
(rf/dispatch [:wallet.swap/select-asset-to-pay
|
||||
{:token token
|
||||
:network (when (= (count token-networks) 1)
|
||||
(first token-networks))
|
||||
:stack-id :screen/wallet.swap-select-asset-to-pay}])
|
||||
(rf/dispatch [:wallet.swap/select-asset-to-receive {:token asset-to-receive}])
|
||||
(rf/dispatch [:wallet.swap/set-pay-amount 100])
|
||||
(rf/dispatch [:wallet.swap/set-swap-proposal dummy-swap-proposal])
|
||||
(rf/dispatch [:wallet.swap/set-provider])))]
|
||||
(rf/dispatch [:wallet.swap/select-asset-to-receive
|
||||
{:token asset-to-receive}])))]
|
||||
[:<>
|
||||
[search-input search-text on-change-text]
|
||||
[asset-list/view
|
||||
@@ -59,7 +41,7 @@
|
||||
(let [[search-text set-search-text] (rn/use-state "")
|
||||
on-change-text #(set-search-text %)
|
||||
on-close (fn []
|
||||
(rf/dispatch [:wallet.swap/clean-asset-to-pay])
|
||||
(rf/dispatch [:wallet/clean-swap])
|
||||
(rf/dispatch [:navigate-back]))]
|
||||
[rn/safe-area-view {:style style/container}
|
||||
[account-switcher/view
|
||||
|
||||
@@ -7,84 +7,102 @@
|
||||
[status-im.common.events-helper :as events-helper]
|
||||
[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.common.utils.external-links :as external-links]
|
||||
[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]))
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(defn- swap-title
|
||||
[{:keys [pay-token-symbol pay-amount account provider]}]
|
||||
[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)}]]])
|
||||
[]
|
||||
(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
|
||||
[{:keys [theme amount token-symbol]}]
|
||||
[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)]
|
||||
[quo/approval-info
|
||||
{:type :spending-cap
|
||||
:unlimited-icon? false
|
||||
:label (str amount " " token-symbol)
|
||||
:avatar-props {:token token-symbol}}]])
|
||||
[]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
asset-to-pay (rf/sub [:wallet/swap-asset-to-pay])
|
||||
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 :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
|
||||
[{:keys [theme account pay-token-symbol pay-token-amount]}]
|
||||
[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)]
|
||||
[quo/approval-info
|
||||
{:type :account
|
||||
:unlimited-icon? false
|
||||
:label (:name account)
|
||||
:description (address-utils/get-short-wallet-address (:address account))
|
||||
:tag-label (str pay-token-amount " " pay-token-symbol)
|
||||
:avatar-props {:emoji (:emoji account)
|
||||
:customization-color (:color account)}}]])
|
||||
[]
|
||||
(let [theme (quo.theme/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 [chain-id contract-address]}]
|
||||
@@ -103,42 +121,54 @@
|
||||
:right-icon :i/external}]]])}]))
|
||||
|
||||
(defn- token-section
|
||||
[{:keys [theme token-address token-symbol network-chain-id]}]
|
||||
[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)]
|
||||
[quo/approval-info
|
||||
{:type :token-contract
|
||||
:option-icon :i/options
|
||||
:on-option-press #(on-option-press {:chain-id network-chain-id
|
||||
:contract-address token-address})
|
||||
:unlimited-icon? false
|
||||
:label token-symbol
|
||||
:description (address-utils/get-short-wallet-address token-address)
|
||||
:avatar-props {:token token-symbol}}]])
|
||||
[]
|
||||
(let [theme (quo.theme/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 {:chain-id network-chain-id
|
||||
: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
|
||||
[{:keys [theme provider network-chain-id]}]
|
||||
[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)]
|
||||
[quo/approval-info
|
||||
{:type :token-contract
|
||||
:option-icon :i/options
|
||||
:on-option-press #(on-option-press {:chain-id network-chain-id
|
||||
:contract-address (:contract-address provider)})
|
||||
:unlimited-icon? false
|
||||
:label (:full-name provider)
|
||||
:description (address-utils/get-short-wallet-address (:contract-address provider))
|
||||
:avatar-props {:image (resources/get-network (:name provider))}}]])
|
||||
[]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
network (rf/sub [:wallet/swap-network])
|
||||
provider (rf/sub [:wallet/swap-proposal-provider])
|
||||
network-chain-id (:chain-id network)]
|
||||
[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 {:chain-id network-chain-id
|
||||
:contract-address (:contract-address provider)})
|
||||
:unlimited-icon? false
|
||||
:label (:full-name provider)
|
||||
:description (address-utils/get-short-wallet-address (:contract-address provider))
|
||||
:avatar-props {:image (resources/get-network (:name provider))}}])]))
|
||||
|
||||
(defn- data-item
|
||||
[{:keys [network-image title subtitle size loading?]}]
|
||||
@@ -154,56 +184,57 @@
|
||||
:size size}])
|
||||
|
||||
(defn- transaction-details
|
||||
[{:keys [estimated-time-min max-fees network loading-fees?]}]
|
||||
[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/est-time)
|
||||
:subtitle (i18n/label :t/time-in-mins {:minutes (str estimated-time-min)})}]
|
||||
[data-item
|
||||
{:title (i18n/label :t/max-fees)
|
||||
:subtitle max-fees
|
||||
:loading? loading-fees?
|
||||
:size :small}]]])
|
||||
[]
|
||||
(let [network (rf/sub [:wallet/swap-network])
|
||||
max-fees (rf/sub [:wallet/wallet-swap-proposal-fee-fiat-formatted
|
||||
constants/token-for-fees-symbol])
|
||||
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
|
||||
estimated-time (rf/sub [:wallet/swap-proposal-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 (and estimated-time max-fees) max-fees (i18n/label :t/unknown))
|
||||
:loading? loading-swap-proposal?
|
||||
:size :small}]
|
||||
[data-item
|
||||
{:title (i18n/label :t/est-time)
|
||||
:subtitle (if estimated-time
|
||||
(i18n/label :t/time-in-mins {:minutes (str estimated-time)})
|
||||
(i18n/label :t/unknown))
|
||||
:loading? loading-swap-proposal?
|
||||
:size :small}]]]))
|
||||
|
||||
(defn footer
|
||||
[{:keys [estimated-time-min native-currency-symbol network theme account-color loading-fees?]}]
|
||||
(let [fee-formatted (rf/sub [:wallet/wallet-send-fee-fiat-formatted native-currency-symbol])
|
||||
on-auth-success (rn/use-callback #(js/alert "Not implemented yet"))]
|
||||
[rn/view {:style {:margin-bottom -10}}
|
||||
[transaction-details
|
||||
{:estimated-time-min estimated-time-min
|
||||
:max-fees fee-formatted
|
||||
:network network
|
||||
:loading-fees? loading-fees?
|
||||
:theme theme}]
|
||||
[standard-auth/slide-button
|
||||
{:size :size-48
|
||||
:track-text (i18n/label :t/slide-to-swap)
|
||||
:container-style {:z-index 2}
|
||||
:customization-color account-color
|
||||
:disabled? loading-fees?
|
||||
:on-auth-success on-auth-success
|
||||
:auth-button-label (i18n/label :t/confirm)}]]))
|
||||
(defn- slide-button
|
||||
[]
|
||||
(let [loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
|
||||
swap-proposal (rf/sub [:wallet/swap-proposal])
|
||||
account (rf/sub [:wallet/current-viewing-account])
|
||||
on-auth-success (rn/use-callback #(rf/dispatch
|
||||
[:wallet/swap-transaction
|
||||
(security/safe-unmask-data %)]))]
|
||||
[standard-auth/slide-button
|
||||
{:size :size-48
|
||||
:track-text (i18n/label :t/slide-to-sign)
|
||||
:container-style {:z-index 2}
|
||||
:customization-color (:color account)
|
||||
:disabled? (or loading-swap-proposal? (not swap-proposal))
|
||||
:on-auth-success on-auth-success
|
||||
:auth-button-label (i18n/label :t/confirm)}]))
|
||||
|
||||
(defn- footer
|
||||
[]
|
||||
[rn/view {:style {:margin-bottom -10}}
|
||||
[transaction-details]
|
||||
[slide-button]])
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
swap-transaction-data (rf/sub [:wallet/swap])
|
||||
{:keys [asset-to-pay network pay-amount
|
||||
providers swap-proposal
|
||||
loading-fees?]} swap-transaction-data
|
||||
estimated-time-min (:estimated-time swap-proposal)
|
||||
pay-token-symbol (:symbol asset-to-pay)
|
||||
pay-token-address (:address asset-to-pay)
|
||||
native-currency-symbol (get-in swap-proposal [:from :native-currency-symbol])
|
||||
account (rf/sub [:wallet/current-viewing-account])
|
||||
account-color (:color account)
|
||||
provider (first providers)]
|
||||
(let [account (rf/sub [:wallet/current-viewing-account])]
|
||||
[rn/view {:style style/container}
|
||||
[floating-button-page/view
|
||||
{:footer-container-padding 0
|
||||
@@ -213,37 +244,12 @@
|
||||
:margin-top 8
|
||||
:background :blur
|
||||
:accessibility-label :top-bar}]
|
||||
:footer [footer
|
||||
{:estimated-time-min estimated-time-min
|
||||
:native-currency-symbol native-currency-symbol
|
||||
:network network
|
||||
:account-color account-color
|
||||
:provider provider
|
||||
:loading-fees? loading-fees?
|
||||
:theme theme}]
|
||||
:footer [footer]
|
||||
:gradient-cover? true
|
||||
:customization-color account-color}
|
||||
:customization-color (:color account)}
|
||||
[:<>
|
||||
[swap-title
|
||||
{:pay-token-symbol pay-token-symbol
|
||||
:pay-amount pay-amount
|
||||
:account account
|
||||
:provider provider}]
|
||||
[spending-cap-section
|
||||
{:token-symbol pay-token-symbol
|
||||
:amount pay-amount
|
||||
:theme theme}]
|
||||
[account-section
|
||||
{:account account
|
||||
:pay-token-symbol pay-token-symbol
|
||||
:pay-token-amount pay-amount
|
||||
:theme theme}]
|
||||
[token-section
|
||||
{:token-symbol pay-token-symbol
|
||||
:token-address pay-token-address
|
||||
:network-chain-id (:chain-id network)
|
||||
:theme theme}]
|
||||
[spender-contract-section
|
||||
{:provider provider
|
||||
:network-chain-id (:chain-id network)
|
||||
:theme theme}]]]]))
|
||||
[swap-title]
|
||||
[spending-cap-section]
|
||||
[account-section]
|
||||
[token-section]
|
||||
[spender-contract-section]]]]))
|
||||
|
||||
@@ -22,14 +22,19 @@
|
||||
:height 36
|
||||
:background-color :transparent})
|
||||
|
||||
(def swap-order-button
|
||||
{:margin-top -9
|
||||
(defn swap-order-button
|
||||
[approval-required?]
|
||||
{:margin-top (if approval-required? 3 -9)
|
||||
:z-index 2
|
||||
:align-self :center})
|
||||
|
||||
(def receive-token-swap-input-container
|
||||
{:margin-top -9})
|
||||
(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 40})
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
(ns status-im.contexts.wallet.swap.setup-swap.view
|
||||
(:require [quo.core :as quo]
|
||||
(:require [clojure.string :as string]
|
||||
[native-module.core :as native-module]
|
||||
[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.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.swap.setup-swap.style :as style]
|
||||
[utils.hex :as hex]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
[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.00")
|
||||
|
||||
(defn- on-close
|
||||
[]
|
||||
(rf/dispatch [:wallet/clean-swap-proposal])
|
||||
(events-helper/navigate-back))
|
||||
|
||||
(defn- fetch-swap-proposal
|
||||
[{:keys [amount valid-input?]}]
|
||||
(if valid-input?
|
||||
(rf/dispatch [:wallet/start-get-swap-proposal {:amount-in amount}])
|
||||
(rf/dispatch [:wallet/clean-swap-proposal])))
|
||||
|
||||
(defn- data-item
|
||||
[{:keys [title subtitle size subtitle-icon loading?]}]
|
||||
@@ -23,110 +45,300 @@
|
||||
:icon subtitle-icon}])
|
||||
|
||||
(defn- transaction-details
|
||||
[{:keys [max-slippage native-currency-symbol loading-fees?]}]
|
||||
(let [max-fees (rf/sub [:wallet/wallet-send-fee-fiat-formatted native-currency-symbol])]
|
||||
[]
|
||||
(let [max-fees (rf/sub [:wallet/wallet-swap-proposal-fee-fiat-formatted
|
||||
constants/token-for-fees-symbol])
|
||||
max-slippage (rf/sub [:wallet/swap-max-slippage])
|
||||
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])]
|
||||
[rn/view {:style style/details-container}
|
||||
[data-item
|
||||
{:title (i18n/label :t/max-fees)
|
||||
:subtitle max-fees
|
||||
:loading? loading-fees?
|
||||
:loading? loading-swap-proposal?
|
||||
:size :small}]
|
||||
[data-item
|
||||
{:title (i18n/label :t/max-slippage)
|
||||
:subtitle max-slippage
|
||||
:subtitle-icon :i/edit
|
||||
:loading? loading-fees?}]]))
|
||||
:size :small}]]))
|
||||
|
||||
(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])
|
||||
asset-to-pay (rf/sub [:wallet/swap-asset-to-pay])
|
||||
currency (rf/sub [:profile/currency])
|
||||
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
|
||||
swap-proposal (rf/sub [:wallet/swap-proposal])
|
||||
approval-required (rf/sub [:wallet/swap-proposal-approval-required])
|
||||
approval-amount-required (rf/sub [:wallet/swap-proposal-approval-amount-required])
|
||||
currency-symbol (rf/sub [:profile/currency-symbol])
|
||||
approval-transaction-status (rf/sub [:wallet/swap-approval-transaction-status])
|
||||
pay-input-num-value (controlled-input/value-numeric input-state)
|
||||
pay-input-amount (controlled-input/input-value input-state)
|
||||
pay-token-symbol (:symbol asset-to-pay)
|
||||
pay-token-decimals (:decimals asset-to-pay)
|
||||
pay-token-balance-selected-chain (get-in asset-to-pay
|
||||
[:balances-per-chain
|
||||
(:chain-id network) :balance]
|
||||
0)
|
||||
pay-token-fiat-value (str
|
||||
(utils/calculate-token-fiat-value
|
||||
{:currency currency
|
||||
:balance (or pay-input-num-value 0)
|
||||
:token asset-to-pay}))
|
||||
available-crypto-limit (number/remove-trailing-zeroes
|
||||
(.toFixed (money/bignumber
|
||||
pay-token-balance-selected-chain)
|
||||
(min pay-token-decimals
|
||||
constants/min-token-decimals-to-display)))
|
||||
approval-amount-required-num (when approval-amount-required
|
||||
(str (number/convert-to-whole-number
|
||||
(native-module/hex-to-number
|
||||
(hex/normalize-hex
|
||||
approval-amount-required))
|
||||
pay-token-decimals)))
|
||||
pay-input-error? (and (not (string/blank? pay-input-amount))
|
||||
(money/greater-than
|
||||
(money/bignumber pay-input-num-value)
|
||||
(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?))
|
||||
request-fetch-swap-proposal (rn/use-callback
|
||||
(fn []
|
||||
(fetch-swap-proposal
|
||||
{:amount pay-input-amount
|
||||
:valid-input? valid-pay-input?}))
|
||||
[pay-input-amount])]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(request-fetch-swap-proposal))
|
||||
[pay-input-amount])
|
||||
[quo/swap-input
|
||||
{:type :pay
|
||||
:error? pay-input-error?
|
||||
:token pay-token-symbol
|
||||
:customization-color :blue
|
||||
:show-approval-label? (and swap-proposal approval-required)
|
||||
:auto-focus? true
|
||||
:show-keyboard? false
|
||||
:status (cond
|
||||
(and loading-swap-proposal? (not input-focused?)) :loading
|
||||
input-focused? :typing
|
||||
:else :disabled)
|
||||
:currency-symbol currency-symbol
|
||||
:on-token-press on-token-press
|
||||
:on-max-press #(on-max-press available-crypto-limit)
|
||||
: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 available-crypto-limit
|
||||
: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-amount-required-num
|
||||
:button-props {:on-press on-approve-press}
|
||||
: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])
|
||||
receive-token-symbol (:symbol asset-to-receive)
|
||||
receive-token-decimals (:decimals asset-to-receive)
|
||||
amount-out-whole-number (when amount-out
|
||||
(number/convert-to-whole-number
|
||||
(native-module/hex-to-number
|
||||
(utils.hex/normalize-hex
|
||||
amount-out))
|
||||
receive-token-decimals))
|
||||
amount-out-num (if amount-out-whole-number
|
||||
(number/remove-trailing-zeroes
|
||||
(.toFixed amount-out-whole-number receive-token-decimals))
|
||||
default-text-for-unfocused-input)
|
||||
receive-token-fiat-value (str (utils/calculate-token-fiat-value
|
||||
{:currency currency
|
||||
:balance (or amount-out-whole-number 0)
|
||||
:token asset-to-receive}))]
|
||||
[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)
|
||||
:currency-symbol currency-symbol
|
||||
: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- action-button
|
||||
[{:keys [on-press]}]
|
||||
(let [account-color (rf/sub [:wallet/current-viewing-account-color])
|
||||
swap-proposal (rf/sub [:wallet/swap-proposal])
|
||||
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)
|
||||
(and approval-required?
|
||||
(not= approval-transaction-status :confirmed))
|
||||
loading-swap-proposal?)
|
||||
:customization-color account-color
|
||||
:on-press on-press}}]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [[pay-value set-pay-value] (rn/use-state "")
|
||||
{:keys [color]} (rf/sub [:wallet/current-viewing-account])
|
||||
{:keys [max-slippage swap-proposal loading-fees?
|
||||
receive-amount network]} (rf/sub [:wallet/swap])
|
||||
currency (rf/sub [:profile/currency])
|
||||
currency-symbol (rf/sub [:profile/currency-symbol])
|
||||
asset-to-pay (rf/sub [:wallet/swap-asset-to-pay])
|
||||
asset-to-receive (rf/sub [:wallet/swap-asset-to-receive])
|
||||
|
||||
pay-token-fiat-value (utils/calculate-token-fiat-value
|
||||
{:currency currency
|
||||
:balance (or pay-value 0)
|
||||
:token asset-to-pay})
|
||||
receive-token-fiat-value (utils/calculate-token-fiat-value
|
||||
{:currency currency
|
||||
:balance (or receive-amount 0)
|
||||
:token asset-to-receive})
|
||||
native-currency-symbol (get-in swap-proposal
|
||||
[:from :native-currency-symbol])
|
||||
pay-token-symbol (:symbol asset-to-pay)
|
||||
receive-token-symbol (:symbol asset-to-receive)
|
||||
on-press (fn [v] (set-pay-value (str pay-value v)))
|
||||
delete (fn []
|
||||
(set-pay-value #(subs % 0 (dec (count %)))))]
|
||||
(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)
|
||||
[refetch-interval set-refetch-interval] (rn/use-state nil)
|
||||
error-response (rf/sub [:wallet/swap-error-response])
|
||||
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
|
||||
swap-proposal (rf/sub [:wallet/swap-proposal])
|
||||
asset-to-pay (rf/sub [:wallet/swap-asset-to-pay])
|
||||
network (rf/sub [:wallet/swap-network])
|
||||
pay-input-amount (controlled-input/input-value pay-input-state)
|
||||
pay-token-decimals (:decimals asset-to-pay)
|
||||
pay-input-num-value (controlled-input/value-numeric pay-input-state)
|
||||
pay-token-balance-selected-chain (get-in asset-to-pay
|
||||
[:balances-per-chain
|
||||
(:chain-id network) :balance]
|
||||
0)
|
||||
pay-input-error? (and (not (string/blank? pay-input-amount))
|
||||
(money/greater-than
|
||||
(money/bignumber pay-input-num-value)
|
||||
(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 [:navigate-to-within-stack
|
||||
[:screen/wallet.swap-confirmation
|
||||
:screen/wallet.setup-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))))))
|
||||
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])))
|
||||
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)))))
|
||||
on-refresh-swap-proposal (rn/use-callback
|
||||
(fn []
|
||||
(let [bottom-sheets (rf/sub
|
||||
[:bottom-sheet-sheets])]
|
||||
(when-not valid-pay-input?
|
||||
(js/clearInterval refetch-interval)
|
||||
(set-refetch-interval nil))
|
||||
(when (and valid-pay-input?
|
||||
(not loading-swap-proposal?)
|
||||
(not bottom-sheets))
|
||||
(fetch-swap-proposal
|
||||
{:amount pay-input-amount
|
||||
:valid-input? valid-pay-input?}))))
|
||||
[valid-pay-input? loading-swap-proposal?
|
||||
pay-input-amount])]
|
||||
(rn/use-effect (fn []
|
||||
(when refetch-interval
|
||||
(js/clearInterval refetch-interval)
|
||||
(set-refetch-interval nil))
|
||||
(when (or swap-proposal error-response)
|
||||
(set-refetch-interval (js/setInterval
|
||||
on-refresh-swap-proposal
|
||||
constants/swap-proposal-refresh-interval-ms))))
|
||||
[swap-proposal error-response])
|
||||
(rn/use-unmount (fn []
|
||||
(when refetch-interval
|
||||
(js/clearInterval refetch-interval)
|
||||
(set-refetch-interval nil))))
|
||||
[rn/view {:style style/container}
|
||||
[account-switcher/view
|
||||
{:on-press events-helper/navigate-back
|
||||
{:on-press on-close
|
||||
:icon-name :i/arrow-left
|
||||
:margin-top (safe-area/get-top)
|
||||
:switcher-type :select-account}]
|
||||
[rn/view {:style style/inputs-container}
|
||||
[quo/swap-input
|
||||
{:type :pay
|
||||
:error? false
|
||||
:token pay-token-symbol
|
||||
:customization-color :blue
|
||||
:show-approval-label? false
|
||||
:status :default
|
||||
:currency-symbol currency-symbol
|
||||
:on-swap-press #(js/alert "Swap Pressed")
|
||||
:on-token-press #(js/alert "Token Pressed")
|
||||
:on-max-press #(js/alert "Max Pressed")
|
||||
:value pay-value
|
||||
:fiat-value pay-token-fiat-value
|
||||
:network-tag-props {:title (i18n/label :t/max-token
|
||||
{:number 200
|
||||
:token-symbol pay-token-symbol})
|
||||
:networks [{:source (:source network)}]}
|
||||
:approval-label-props {:status :approve
|
||||
:token-value pay-value
|
||||
:button-props {:on-press
|
||||
#(js/alert "Approve Pressed")}
|
||||
:customization-color color
|
||||
:token-symbol pay-token-symbol}}]
|
||||
[quo/swap-order-button
|
||||
{:container-style style/swap-order-button
|
||||
:on-press #(js/alert "Pressed")}]
|
||||
[quo/swap-input
|
||||
{:type :receive
|
||||
:error? false
|
||||
:token receive-token-symbol
|
||||
:customization-color color
|
||||
:show-approval-label? false
|
||||
:enable-swap? true
|
||||
:status :default
|
||||
:currency-symbol currency-symbol
|
||||
:on-swap-press #(js/alert "Swap Pressed")
|
||||
:on-token-press #(js/alert "Token Pressed")
|
||||
:on-max-press #(js/alert "Max Pressed")
|
||||
:value receive-amount
|
||||
:fiat-value receive-token-fiat-value
|
||||
:container-style style/receive-token-swap-input-container}]]
|
||||
[pay-token-input
|
||||
{:input-state pay-input-state
|
||||
:on-max-press on-max-press
|
||||
:input-focused? pay-input-focused?
|
||||
:on-token-press #(js/alert "Token Pressed")
|
||||
: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 #(js/alert "Swap Order Pressed")}]
|
||||
[receive-token-input
|
||||
{:input-focused? (not pay-input-focused?)
|
||||
:on-token-press #(js/alert "Token Pressed")
|
||||
:on-input-focus #(set-pay-input-focused? false)}]]
|
||||
[rn/view {:style style/footer-container}
|
||||
(when swap-proposal
|
||||
[transaction-details
|
||||
{:native-currency-symbol native-currency-symbol
|
||||
:max-slippage max-slippage
|
||||
:loading-fees? loading-fees?}])
|
||||
[quo/bottom-actions
|
||||
{:actions :one-action
|
||||
:button-one-label (i18n/label :t/review-swap)
|
||||
:button-one-props {:disabled? (or (not swap-proposal)
|
||||
loading-fees?)
|
||||
:customization-color color
|
||||
:on-press #(js/alert "Review swap pressed")}}]]
|
||||
(when error-response
|
||||
[quo/alert-banner
|
||||
{:container-style style/alert-banner
|
||||
:text (i18n/label :t/something-went-wrong-please-try-again-later)}])
|
||||
(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-delete delete
|
||||
:on-long-press on-long-press}]]))
|
||||
|
||||
@@ -37,3 +37,7 @@
|
||||
(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)})
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
(ns status-im.contexts.wallet.swap.swap-confirmation.view
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[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.constants :as constants]
|
||||
[status-im.contexts.wallet.swap.swap-confirmation.style :as style]
|
||||
[utils.address :as address-utils]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(defn- on-close-action
|
||||
[]
|
||||
(rf/dispatch [:navigate-back]))
|
||||
|
||||
(defn- swap-title
|
||||
[{:keys [pay-token-symbol pay-amount receive-token-symbol receive-amount account]}]
|
||||
[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)}]]])
|
||||
[]
|
||||
(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]}]
|
||||
@@ -74,6 +82,44 @@
|
||||
:address (address-utils/get-shortened-compressed-key token-address)
|
||||
:size 32}}]]))
|
||||
|
||||
(defn- pay-section
|
||||
[]
|
||||
(let [theme (quo.theme/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])
|
||||
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 pay-token-address
|
||||
:network network-name
|
||||
:theme theme}]))
|
||||
|
||||
(defn- receive-section
|
||||
[]
|
||||
(let [theme (quo.theme/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])
|
||||
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 receive-token-address
|
||||
:network network-name
|
||||
:theme theme}]))
|
||||
|
||||
(defn- data-item
|
||||
[{:keys [title subtitle loading?]}]
|
||||
[quo/data-item
|
||||
@@ -86,64 +132,64 @@
|
||||
:subtitle subtitle}])
|
||||
|
||||
(defn- transaction-details
|
||||
[{:keys [estimated-time-min max-fees max-slippage loading-fees?]}]
|
||||
[rn/view {:style style/details-container}
|
||||
[:<>
|
||||
[data-item
|
||||
{:title (i18n/label :t/est-time)
|
||||
:subtitle (i18n/label :t/time-in-mins {:minutes (str estimated-time-min)})}]
|
||||
[data-item
|
||||
{:title (i18n/label :t/max-fees)
|
||||
:subtitle max-fees
|
||||
:loading? loading-fees?}]
|
||||
[data-item
|
||||
{:title (i18n/label :t/max-slippage)
|
||||
:subtitle (str max-slippage "%")}]]])
|
||||
[]
|
||||
(let [max-fees (rf/sub [:wallet/wallet-swap-proposal-fee-fiat-formatted
|
||||
constants/token-for-fees-symbol])
|
||||
estimated-time (rf/sub [:wallet/swap-proposal-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}
|
||||
[:<>
|
||||
[data-item
|
||||
{:title (i18n/label :t/est-time)
|
||||
:subtitle (if estimated-time
|
||||
(i18n/label :t/time-in-mins {:minutes (str estimated-time)})
|
||||
(i18n/label :t/unknown))
|
||||
:loading? loading-swap-proposal?}]
|
||||
[data-item
|
||||
{:title (i18n/label :t/max-fees)
|
||||
:subtitle (if (and estimated-time max-fees) max-fees (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])
|
||||
account (rf/sub [:wallet/current-viewing-account])
|
||||
account-color (:color account)
|
||||
on-auth-success (rn/use-callback #(rf/dispatch
|
||||
[:wallet/swap-transaction
|
||||
(security/safe-unmask-data %)]))]
|
||||
[standard-auth/slide-button
|
||||
{:size :size-48
|
||||
:track-text (i18n/label :t/slide-to-swap)
|
||||
:container-style {:z-index 2}
|
||||
:customization-color account-color
|
||||
:disabled? (or loading-swap-proposal? (not swap-proposal))
|
||||
:auth-button-label (i18n/label :t/confirm)
|
||||
:on-auth-success on-auth-success}]))
|
||||
|
||||
(defn footer
|
||||
[{:keys [estimated-time-min native-currency-symbol max-slippage theme account-color provider
|
||||
loading-fees?]}]
|
||||
(let [fee-formatted (rf/sub [:wallet/wallet-send-fee-fiat-formatted native-currency-symbol])]
|
||||
[]
|
||||
(let [provider (rf/sub [:wallet/swap-proposal-provider])
|
||||
theme (quo.theme/use-theme)]
|
||||
[:<>
|
||||
[transaction-details
|
||||
{:estimated-time-min estimated-time-min
|
||||
:max-fees fee-formatted
|
||||
:max-slippage max-slippage
|
||||
:loading-fees? loading-fees?
|
||||
:theme theme}]
|
||||
[standard-auth/slide-button
|
||||
{:size :size-48
|
||||
:track-text (i18n/label :t/slide-to-swap)
|
||||
:container-style {:z-index 2}
|
||||
:customization-color account-color
|
||||
:disabled? loading-fees?
|
||||
:auth-button-label (i18n/label :t/confirm)}]
|
||||
[transaction-details]
|
||||
[slide-button]
|
||||
[rn/view {:style style/providers-container}
|
||||
[quo/text
|
||||
{:size :paragraph-2
|
||||
:style {:color (colors/theme-colors colors/neutral-80-opa-40
|
||||
colors/white-opa-70
|
||||
theme)}}
|
||||
(i18n/label :t/swaps-powered-by {:provider (:name provider)})]]]))
|
||||
:style (style/swaps-powered-by theme)}
|
||||
(i18n/label :t/swaps-powered-by
|
||||
{:provider (if provider (:full-name provider) (i18n/label :t/unknown))})]]]))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
swap-transaction-data (rf/sub [:wallet/swap])
|
||||
{:keys [asset-to-pay max-slippage network
|
||||
pay-amount providers swap-proposal
|
||||
loading-fees?]} swap-transaction-data
|
||||
receive-amount (:receive-amount swap-proposal)
|
||||
receive-token (:receive-token swap-proposal)
|
||||
receive-token-symbol (:symbol receive-token)
|
||||
receive-token-address (:address receive-token)
|
||||
estimated-time-min (:estimated-time swap-proposal)
|
||||
pay-token-symbol (:symbol asset-to-pay)
|
||||
pay-token-address (:address asset-to-pay)
|
||||
native-currency-symbol (get-in swap-proposal [:from :native-currency-symbol])
|
||||
account (rf/sub [:wallet/current-viewing-account])
|
||||
account-color (:color account)
|
||||
provider (first providers)]
|
||||
(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
|
||||
@@ -153,36 +199,10 @@
|
||||
:margin-top (safe-area/get-top)
|
||||
:background :blur
|
||||
:accessibility-label :top-bar}]
|
||||
:footer [footer
|
||||
{:estimated-time-min estimated-time-min
|
||||
:native-currency-symbol native-currency-symbol
|
||||
:max-slippage max-slippage
|
||||
:account-color account-color
|
||||
:provider provider
|
||||
:loading-fees? loading-fees?
|
||||
:theme theme}]
|
||||
:footer [footer]
|
||||
:gradient-cover? true
|
||||
:customization-color account-color}
|
||||
[rn/view
|
||||
[swap-title
|
||||
{:pay-token-symbol pay-token-symbol
|
||||
:pay-amount pay-amount
|
||||
:receive-token-symbol receive-token-symbol
|
||||
:receive-amount receive-amount
|
||||
:account account}]
|
||||
[summary-section
|
||||
{:title-accessibility-label :summary-section-pay
|
||||
:label (i18n/label :t/pay)
|
||||
:token-symbol pay-token-symbol
|
||||
:amount pay-amount
|
||||
:token-address pay-token-address
|
||||
:network (:network-name network)
|
||||
:theme theme}]
|
||||
[summary-section
|
||||
{:title-accessibility-label :summary-section-receive
|
||||
:label (i18n/label :t/receive)
|
||||
:token-symbol receive-token-symbol
|
||||
:amount receive-amount
|
||||
:token-address receive-token-address
|
||||
:network (:network-name network)
|
||||
:theme theme}]]]]))
|
||||
[swap-title]
|
||||
[pay-section]
|
||||
[receive-section]]]]))
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
(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 [address] :as token}]
|
||||
(if (get-in token [:communityData :id])
|
||||
address
|
||||
(:symbol token))))))
|
||||
@@ -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)))))
|
||||
@@ -0,0 +1,144 @@
|
||||
(ns status-im.contexts.wallet.tokens.events
|
||||
(:require [re-frame.core :as rf]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.contexts.wallet.tokens.data :as tokens-data]
|
||||
[status-im.contexts.wallet.tokens.effects]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.address]
|
||||
[utils.ethereum.chain :as chain]))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:wallet.tokens/get-token-list
|
||||
(fn [{:keys [db]}]
|
||||
{: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 (chain/chain-ids db)
|
||||
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)
|
||||
symbols (->> tokens
|
||||
:by-symbol
|
||||
keys
|
||||
(remove utils.address/address?))]
|
||||
{:fx [[:effects.wallet.tokens/fetch-market-values
|
||||
{:symbols symbols
|
||||
:currency constants/profile-default-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]
|
||||
:on-success [:wallet.tokens/store-prices]
|
||||
:on-error [:wallet.tokens/fetch-prices-failed]}]]
|
||||
:db (-> db
|
||||
(assoc-in [:wallet :tokens]
|
||||
{:sources (:sources tokens)
|
||||
:by-address (-> tokens :by-address vals)
|
||||
:by-symbol (-> tokens :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
|
||||
(assoc-in [:wallet :tokens :market-values-per-token] 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
|
||||
(assoc-in [:wallet :tokens :prices-per-token] 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)}))
|
||||
@@ -0,0 +1,275 @@
|
||||
(ns status-im.contexts.wallet.tokens.events-test
|
||||
(:require
|
||||
[cljs.test :refer [deftest is testing]]
|
||||
[status-im.contexts.wallet.tokens.events :as tokens.events]))
|
||||
|
||||
(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 {:wallet {:networks {:prod [{:chain-id 1}]}}}}
|
||||
data [{:name "native"
|
||||
:source "native"
|
||||
:version "1.0.0"
|
||||
:tokens [eth snt]}]]
|
||||
(is
|
||||
(match?
|
||||
(:db (tokens.events/store-token-list cofx [{:data data}]))
|
||||
{:wallet {:networks {:prod [{:chain-id 1}]}
|
||||
: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 "ETH"
|
||||
:community-id nil
|
||||
:symbol "ETH"
|
||||
:sources ["native"]
|
||||
:name "Ether"
|
||||
:type :erc20
|
||||
:verified? true
|
||||
:chain-id 1
|
||||
:image nil}
|
||||
{:address "0x0000000000000000000000000000000000000001"
|
||||
:decimals 18
|
||||
:key "SNT"
|
||||
:community-id nil
|
||||
:symbol "SNT"
|
||||
:sources ["native"]
|
||||
:name "Status Network Token"
|
||||
:type :erc20
|
||||
:verified? true
|
||||
:chain-id 1
|
||||
:image nil})}
|
||||
:ui {:loading {:token-list false
|
||||
:market-values true
|
||||
:details true
|
||||
:prices true}}}}))))
|
||||
(testing "response contains one list, and `chain-id` is NOT equal to the one that's in the database"
|
||||
(let [cofx {:db {:wallet {:networks {:prod [{:chain-id 2}]}}}}
|
||||
data [{:name "native"
|
||||
:source "native"
|
||||
:version "1.0.0"
|
||||
:tokens [eth snt]}]]
|
||||
(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}]
|
||||
:by-address nil
|
||||
:by-symbol nil}))))
|
||||
(testing "response contains two lists"
|
||||
(let [cofx {:db {:wallet {:networks {:prod [{:chain-id 1}]}}}}
|
||||
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 "ETH"
|
||||
:community-id nil
|
||||
:symbol "ETH"
|
||||
:sources ["native"]
|
||||
:name "Ether"
|
||||
:type :erc20
|
||||
:verified? true
|
||||
:chain-id 1
|
||||
:image nil}
|
||||
{:address "0x0000000000000000000000000000000000000001"
|
||||
:decimals 18
|
||||
:key "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 "ANOTHER1"
|
||||
:community-id nil
|
||||
:symbol "ANOTHER1"
|
||||
:sources ["second"]
|
||||
:name "Another Token 1"
|
||||
:type :erc20
|
||||
:verified? false
|
||||
:chain-id 1
|
||||
:image nil})}))))
|
||||
(testing "second list contains a token that replaces the one that was added before"
|
||||
(let [cofx {:db {:wallet {:networks {:prod [{:chain-id 1}]}}}}
|
||||
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 "ETH"
|
||||
:community-id nil
|
||||
:symbol "ETH"
|
||||
:sources ["native"]
|
||||
:name "Ether"
|
||||
:type :erc20
|
||||
:verified? true
|
||||
:chain-id 1
|
||||
:image nil}
|
||||
{:address "0x0000000000000000000000000000000000000000"
|
||||
:decimals 18
|
||||
:key "ANOTHER1"
|
||||
:community-id nil
|
||||
:symbol "ANOTHER1"
|
||||
:sources ["native" "second"]
|
||||
:name "Another Token 2"
|
||||
:type :erc20
|
||||
:verified? false
|
||||
:chain-id 1
|
||||
:image nil})})))))
|
||||