Compare commits

...
Author SHA1 Message Date
Ulises M b778f62e6d Nashorn attempt 2024-03-13 16:49:58 -06:00
Ulises M ac6e3cdb2c Reimplement avatar animations using new worklet approach 2024-03-13 13:44:32 -06:00
Ulises M 691c6b67ce Add worklet translation to shadow-cljs 2024-03-13 13:42:12 -06:00
flexsurfer e26d3a856d migrate reagent part 3 (#19165) 2024-03-13 16:46:44 +01:00
Mohsen 98cdf00276 [#19132] feat: block contact with remove contact checkbox (#19213) 2024-03-13 18:33:19 +03:00
flexsurfer eb379791c9 migrate reagent part4 (#19167) 2024-03-13 15:43:39 +01:00
Parvesh Monu 6bda54266b Refactor navigation files (#19068) 2024-03-13 19:09:09 +05:30
flexsurfer 1a8bca56e0 migrate reagent record audio component part5 (#19212) 2024-03-13 14:27:22 +01:00
codemaster 296d868797 fix: remove biometric button in the login screen when disabled biometrics (#19112) 2024-03-13 06:13:20 -07:00
Andrea Maria Piana e002fab376 cache processed description 2024-03-13 12:38:47 +00:00
Icaro Motta 4fa9a27cd9 Rewrite contract tests to use new promise-based API (#19208) 2024-03-13 09:38:32 -03:00
mmilad75 dbcdb18818 The wrong validation is applied for entered amount within send assets page #18751 (#18937) 2024-03-13 14:23:19 +03:30
Ibrahem Khalil 2009199c25 feat: Add internal link preview for communities (#18484) 2024-03-13 00:49:19 +02:00
Sean Hagstrom 103c4b4653 chore: decode emoji-hash and bio from contact payload (#19030) 2024-03-12 17:42:11 +00:00
Flavio Fraschetti 0828e9aa55 Fraschetti/18343 - Add contract test for wallet_getDerivedAddresses and move legacy code
Add contract test for wallet_getDerivedAddresses and move legacy code
2024-03-12 13:21:23 -03:00
Sean Hagstrom 0efdea6962 Feature: Ignore / Dismiss contact requests from contact profile (#19166)
* chore: add english translation for "contact request ignored"

* feature: integrate action to ignore contact request and display toast

* wip: default to showing "send contact request" after ignoring contact request
2024-03-12 13:48:21 +00:00
Sean Hagstrom d4c8e48441 Add action to remove contact inside contact action menu (#19168)
* chore: add english translation for "Remove contact"

* feature: integrate remove-contact acton in contact-profile action menu
2024-03-12 13:34:59 +00:00
Jamie Caprani 2931ae84ee chore(navigation): qualify wallet screen keywords (#18778) 2024-03-12 11:22:04 +00:00
Lungu Cristian 637efa24cf Clean up biometrics and standard-auth and add tests (#18756)
* ref(biometric): events using reg-event-fx & other

* test(biometric-events): show-message

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix(biometrics): removed unnecessary event

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test(biometrics): tests & schemas for events

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref(standard-auth): refactored authorize

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test(biometrics): fixed event tests

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref(standard-auth): authorize using events

* feat(standard-auth): schemas and removed old auth

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test(standard-auth): added integration tests

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* feat(biometric): added missing schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref(biometrics): moved schemas to a separate file

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix(biometric): pressing biometric quickly errors

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: removed namespaced keywords for errors

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: removed the use of vector cb for biometric fx

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: removed :maybe from fx schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: remove event-fx schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* feat: added nested biometrics db keys

* fix: using ExceptionInfo for the error schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test: removed clearing fixture and small refactor

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: reset db password inside standard-authentication

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: use naming convention for effects

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: renamed not-canceled? to success?

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* rev: removed biometrics schemas :(

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: removed set-in & fix on-close bug

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: onboarding biometric not triggered

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: invalid props bug on onboarding welcome

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: moved side effect cb call into effect

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test: moved integration test to new location

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: password input theme

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: disabled biometric not triggering fail when checking biometric

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

---------

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>
2024-03-12 12:14:56 +02:00
Jamie Caprani c57e5cd6db chore: improve share wallet test (#19107) 2024-03-12 08:44:59 +00:00
Parvesh Monu 604e232bda Implement alert banner (#19011) 2024-03-12 14:03:37 +05:30
Volodymyr Kozieiev e4997a2088 Root keys named :wallet/... moved as nested under :wallet root key (#19152) 2024-03-11 21:19:48 +00:00
Sean Hagstrom 459bf1621a Show toast after sending contact request (#19144)
* chore: add english translations for "Contact request sent"

* feature: add toast notification when sending a contact request
2024-03-11 10:10:50 +00:00
Sean Hagstrom ca9346a931 Implement reviewal of contact requests from contact profile (#19119)
* chore: add english translation for contact-request-review

* feature: integrate contact-request-review button

* tweak: render the locked-input label when provided

* tweak: allow locked-input component to grow in height

* chore: add english translation for "ignore"

* feature: add subscription for retrieving a pending contact-request by contact-id

* feature: integrate initial contact request review flow

* tweak: display send-message button on contact profile when contact-request-state is mutual

* tidy: refactor button-wrapper view to be button container-styles

* feature: add toast for accepting contact request
2024-03-11 09:43:18 +00:00
Mohsen 7065f4abd2 [#19101] feat: share contact profile (#19154) 2024-03-10 18:56:45 +03:00
Mohsen e283a72454 [#19048] feat: show contact QR code (#19139) 2024-03-09 00:36:24 +03:00
Ulises Manuel 3c6923963d [#18736] add address to watch using an ENS (#19043)
* Add capability to register an address based on an ENS

* update tests

* Check for existing addresses using an ENS
2024-03-08 12:20:41 -06:00
190 changed files with 9425 additions and 5975 deletions
+31 -31
View File
@@ -22,37 +22,37 @@
:multi-lhs-hang]
:fn-map
{"reg-sub" :arg1-pair
"h/describe" :arg1-body
"h/describe-skip" :arg1-body
"h/describe-only" :arg1-body
"h/test" :arg1-body
"h/test-skip" :arg1-body
"h/test-only" :arg1-body
"test/async" :arg1-body
"test/use-fixtures" :arg1-body
"global.describe" :arg1-body
"global.test" :arg1-body
"list-comp" :binding
"defview" :arg1-body
"letsubs" :binding
"with-let" "let"
"reg-event-fx" :arg1-pair
"reg-fx" :arg1-pair
"testing" :arg1-body
"deftest-sub" :arg1-body
"h/integration-test" :arg1-body
"wait-for" :arg1-body
"with-deps-check" :arg1-body
"schema/=>" :arg1-body
"->" [:noarg1-body
{:list {:constant-pair? false :force-nl? false}
:next-inner-restore [[:list :constant-pair?]]}]
"set!" "reset!"
"assoc-when" "assoc"
"assoc-some" "assoc"
"conj-when" "conj"
"conj-some" "conj"}
{"reg-sub" :arg1-pair
"h/describe" :arg1-body
"h/describe-skip" :arg1-body
"h/describe-only" :arg1-body
"h/test" :arg1-body
"h/test-skip" :arg1-body
"h/test-only" :arg1-body
"test/async" :arg1-body
"test/use-fixtures" :arg1-body
"global.describe" :arg1-body
"global.test" :arg1-body
"list-comp" :binding
"defview" :arg1-body
"letsubs" :binding
"with-let" "let"
"reg-event-fx" :arg1-pair
"reg-fx" :arg1-pair
"testing" :arg1-body
"deftest-sub" :arg1-body
"test-async" :arg1-body
"wait-for" :arg1-body
"with-deps-check" :arg1-body
"schema/=>" :arg1-body
"->" [:noarg1-body
{:list {:constant-pair? false :force-nl? false}
:next-inner-restore [[:list :constant-pair?]]}]
"set!" "reset!"
"assoc-when" "assoc"
"assoc-some" "assoc"
"conj-when" "conj"
"conj-some" "conj"}
:style-map
{:no-comma {:map {:comma? false}}
+6 -3
View File
@@ -1,6 +1,9 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['react-native-reanimated/plugin', '@babel/plugin-transform-named-capturing-groups-regex'],
presets: [
"@babel/preset-env"
// 'module:metro-react-native-babel-preset'
],
/*plugins: ['react-native-reanimated/plugin', '@babel/plugin-transform-named-capturing-groups-regex'],
env: {
test: {
presets: [
@@ -15,5 +18,5 @@ module.exports = {
],
],
},
},
},*/
};
@@ -1888,6 +1888,36 @@ void _InitLogging(const FunctionCallbackInfo<Value>& args) {
delete c;
}
void _RestoreAccountAndLogin(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.Length() != 1) {
// Throw an Error that is passed back to JavaScript
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong number of arguments for RestoreAccountAndLogin")));
return;
}
// Check the argument types
if (!args[0]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'RestoreAccountAndLogin'")));
return;
}
String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked());
char *arg0 = *arg0Obj;
// Call exported Go function, which returns a C string
char *c = RestoreAccountAndLogin(arg0);
Local<String> ret = String::NewFromUtf8(isolate, c).ToLocalChecked();
args.GetReturnValue().Set(ret);
delete c;
}
void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "multiAccountGenerateAndDeriveAddresses", _MultiAccountGenerateAndDeriveAddresses);
@@ -1945,6 +1975,7 @@ void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "connectionChange", _ConnectionChange);
NODE_SET_METHOD(exports, "pollSignal", _PollSignal);
NODE_SET_METHOD(exports, "initLogging", _InitLogging);
NODE_SET_METHOD(exports, "restoreAccountAndLogin", _RestoreAccountAndLogin);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
+1
View File
@@ -78,6 +78,7 @@
"@babel/helper-builder-react-jsx": "^7.20.0",
"@babel/plugin-transform-block-scoping": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/preset-es2015": "^7.0.0-beta.53",
"@babel/preset-react": "^7.18.6",
"@babel/register": "7.0.0",
"@jest/globals": "^25.1.0",
+1
View File
@@ -46,6 +46,7 @@
;; To match the folder created by Nix build of JSBundle.
:output-dir "result"
:init-fn status-im.core/init
:build-hooks [(status-im.setup.build-hooks.worklets/transform-output)]
;; When false, the Shadow-CLJS watcher won't automatically refresh
;; the target files (a.k.a hot reload). When false, you can manually
;; reload by calling `shadow.cljs.devtools.api/watch-compile-all!`.
@@ -29,37 +29,39 @@
(testing "transforms messages from RPC response"
(is
(= {:last-message {:quoted-message nil
:outgoing-status nil
:command-parameters nil
:link-previews []
:content {:sticker nil
:rtl? nil
:ens-name nil
:parsed-text nil
:response-to nil
:chat-id nil
:image nil
:line-count nil
:links nil
:text nil}
:outgoing false}
(= {:last-message {:quoted-message nil
:outgoing-status nil
:command-parameters nil
:link-previews []
:status-link-previews []
:content {:sticker nil
:rtl? nil
:ens-name nil
:parsed-text nil
:response-to nil
:chat-id nil
:image nil
:line-count nil
:links nil
:text nil}
:outgoing false}
:message nil
:reply-message {:quoted-message nil
:outgoing-status nil
:command-parameters nil
:link-previews []
:content {:sticker nil
:rtl? nil
:ens-name nil
:parsed-text nil
:response-to nil
:chat-id nil
:image nil
:line-count nil
:links nil
:text nil}
:outgoing false}}
:reply-message {:quoted-message nil
:outgoing-status nil
:command-parameters nil
:link-previews []
:status-link-previews []
:content {:sticker nil
:rtl? nil
:ens-name nil
:parsed-text nil
:response-to nil
:chat-id nil
:image nil
:line-count nil
:links nil
:text nil}
:outgoing false}}
(-> raw-notification
store/<-rpc
(select-keys [:last-message :message :reply-message])))))
@@ -4,13 +4,6 @@
[clojure.walk :as walk]
[status-im.constants :as constants]))
(defn rpc->channel-permissions
[rpc-channels-permissions]
(update-vals rpc-channels-permissions
(fn [{:keys [viewAndPostPermissions viewOnlyPermissions]}]
{:view-only (set/rename-keys viewOnlyPermissions {:satisfied :satisfied?})
:view-and-post (set/rename-keys viewAndPostPermissions {:satisfied :satisfied?})})))
(defn <-revealed-accounts-rpc
[accounts]
(mapv
@@ -34,8 +27,9 @@
(assoc acc
(name k)
(-> v
(assoc :can-post? (:canPost v))
(dissoc :canPost)
(assoc :token-gated? (:tokenGated v)
:can-post? (:canPost v))
(dissoc :canPost :tokenGated)
(update :members walk/stringify-keys))))
{}
chats))
@@ -1,23 +0,0 @@
(ns legacy.status-im.data-store.communities-test
(:require
[cljs.test :refer [deftest is]]
[legacy.status-im.data-store.communities :as sut]))
(def permissions
{"community-id-chat-1"
{:viewOnlyPermissions {:satisfied false
:permissions {:token-permission-id-01 {:criteria [false]}}}
:viewAndPostPermissions {:satisfied true :permissions {}}}
"community-id-chat-2"
{:viewOnlyPermissions {:satisfied true :permissions {}}
:viewAndPostPermissions {:satisfied true :permissions {}}}})
(deftest rpc->channel-permissions-test
(is (= {"community-id-chat-1"
{:view-only {:satisfied? false
:permissions {:token-permission-id-01 {:criteria [false]}}}
:view-and-post {:satisfied? true :permissions {}}}
"community-id-chat-2"
{:view-only {:satisfied? true :permissions {}}
:view-and-post {:satisfied? true :permissions {}}}}
(sut/rpc->channel-permissions permissions))))
+33 -3
View File
@@ -16,13 +16,41 @@
:community-id :communityId
:clock-value :clock})))
(defn- <-status-link-previews-rpc
[preview]
(-> preview
(update :community
set/rename-keys
{:communityId :community-id
:displayName :display-name
:membersCount :members-count
:activeMembersCount :active-members-count})
(update-in [:community :banner] set/rename-keys {:dataUri :data-uri})
(update-in [:community :icon] set/rename-keys {:dataUri :data-uri})))
(defn ->status-link-previews-rpc
[preview]
(-> preview
(update :community
set/rename-keys
{:community-id :communityId
:display-name :displayName
:members-count :membersCount
:active-members-count :activeMembersCount})
(update-in [:community :banner] set/rename-keys {:data-uri :dataUri})
(update-in [:community :icon] set/rename-keys {:data-uri :dataUri})))
(defn- <-link-preview-rpc
[preview]
(update preview :thumbnail set/rename-keys {:dataUri :data-uri}))
(-> preview
(update :thumbnail set/rename-keys {:dataUri :data-uri})
(update :favicon set/rename-keys {:dataUri :data-uri})))
(defn ->link-preview-rpc
[preview]
(update preview :thumbnail set/rename-keys {:data-uri :dataUri}))
(-> preview
(update :thumbnail set/rename-keys {:data-uri :dataUri})
(update :favicon set/rename-keys {:data-uri :dataUri})))
(defn <-rpc
[message]
@@ -53,8 +81,10 @@
:new :new?
:albumImagesCount :album-images-count
:displayName :display-name
:linkPreviews :link-previews})
:linkPreviews :link-previews
:statusLinkPreviews :status-link-previews})
(update :link-previews #(map <-link-preview-rpc %))
(update :status-link-previews #(map <-status-link-previews-rpc %))
(update :quoted-message
set/rename-keys
{:parsedText :parsed-text
@@ -37,7 +37,19 @@
:compressed-key "c"
:timestamp 3
:link-previews [{:thumbnail {:url "http://localhost"
:data-uri "data:image/png"}}]}
:data-uri "data:image/png"}
:favicon nil}]
:status-link-previews [{:url "http://localhost"
:community {:community-id "0x01"
:display-name "Test Comm"
:members-count 12
:active-members-count 12
:banner
{:data-uri
"data:image/png"}
:icon
{:data-uri
"data:image/png"}}}]}
message {:id message-id
:whisperTimestamp 1
:parsedText "parsed-text"
@@ -61,5 +73,17 @@
:timestamp 3
:outgoingStatus "sending"
:linkPreviews [{:thumbnail {:url "http://localhost"
:dataUri "data:image/png"}}]}]
:dataUri "data:image/png"}
:favicon nil}]
:statusLinkPreviews [{:url "http://localhost"
:community {:communityId "0x01"
:displayName "Test Comm"
:membersCount 12
:activeMembersCount 12
:banner
{:dataUri
"data:image/png"}
:icon
{:dataUri
"data:image/png"}}}]}]
(is (= expected (m/<-rpc message))))))
+2 -3
View File
@@ -37,7 +37,6 @@
[react-native.core :as rn]
[react-native.permissions :as permissions]
[react-native.platform :as platform]
[status-im.common.biometric.events :as biometric]
status-im.common.serialization
status-im.common.standard-authentication.events
[status-im.common.theme.core :as theme]
@@ -120,7 +119,7 @@
:content (i18n/label :t/biometric-auth-confirm-message)
:confirm-button-text (i18n/label :t/biometric-auth-confirm-try-again)
:cancel-button-text (i18n/label :t/biometric-auth-confirm-logout)
:on-accept #(biometric/authenticate nil {:on-fail on-biometric-auth-fail})
:on-accept #(rf/dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}])
:on-cancel #(re-frame/dispatch [:multiaccounts.logout.ui/logout-confirmed])})))
(rf/defn on-return-from-background
@@ -142,7 +141,7 @@
#(when-let [chat-id (:current-chat-id db)]
{:dispatch [:chat/mark-all-as-read chat-id]})
#(when requires-bio-auth
(biometric/authenticate % {:on-fail on-biometric-auth-fail})))))
{:dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}]}))))
(rf/defn on-going-in-background
[{:keys [db now]}]
@@ -13,15 +13,14 @@
(native-module/logout)))
(rf/defn initialize-app-db
[{{:keys [keycard initials-avatar-font-file]
:biometric/keys [supported-type]
:network/keys [type]}
[{{:keys [keycard initials-avatar-font-file biometrics]
:network/keys [type]}
:db}]
{:db (assoc db/app-db
:network/type type
:initials-avatar-font-file initials-avatar-font-file
:keycard (dissoc keycard :secrets :pin :application-info)
:biometric/supported-type supported-type
:biometrics biometrics
:syncing nil)})
(rf/defn logout-method
@@ -29,8 +28,7 @@
[{:keys [db] :as cofx} {:keys [auth-method logout?]}]
(let [key-uid (get-in db [:profile/profile :key-uid])]
(rf/merge cofx
{:set-root :progress
:effects.shell/reset-state nil
{:effects.shell/reset-state nil
:hide-popover nil
::logout nil
:profile.settings/webview-debug-changed false
@@ -46,6 +44,7 @@
[_]
;; we need to disable notifications before starting the logout process
{:effects/push-notifications-disable nil
:dispatch [:alert-banners/remove-all]
:dispatch-later [{:ms 100
:dispatch [::logout-method
{:auth-method keychain/auth-method-none
+2
View File
@@ -22,6 +22,7 @@
(reg-root-key-sub :peers-count :peers-count)
(reg-root-key-sub :peers-summary :peers-summary)
(reg-root-key-sub :web3-node-version :web3-node-version)
(reg-root-key-sub :alert-banners :alert-banners)
;;keycard
(reg-root-key-sub :keycard :keycard)
@@ -99,6 +100,7 @@
;;; Link previews
(reg-root-key-sub :link-previews-whitelist :link-previews-whitelist)
(reg-root-key-sub :chat/link-previews :chat/link-previews)
(reg-root-key-sub :chat/status-link-previews :chat/status-link-previews)
;;commands
(reg-root-key-sub :commands/select-account :commands/select-account)
@@ -85,9 +85,7 @@
:accessibility-label :share-community-link
:type :secondary
:on-press #(debounce/throttle-and-dispatch
[(if can-invite?
::communities/invite-people-confirmation-pressed
::communities/share-community-confirmation-pressed) @user-pk
[::communities/share-community-confirmation-pressed @user-pk
selected]
3000)}
(i18n/label (if can-invite? :t/invite :t/share))]}]]))))
+47 -40
View File
@@ -1,7 +1,6 @@
(ns legacy.status-im.ui.screens.screens
(:require
[legacy.status-im.ui.components.colors :as colors]
[legacy.status-im.ui.components.icons.icons :as icons]
[legacy.status-im.ui.screens.about-app.views :as about-app]
[legacy.status-im.ui.screens.advanced-settings.views :as advanced-settings]
[legacy.status-im.ui.screens.appearance.views :as appearance]
@@ -48,10 +47,18 @@
[status-im.contexts.chat.group-details.view :as group-details]
[utils.i18n :as i18n]))
(defn right-button-options
[id icon]
{:id id
:icon (icons/icon-source icon)})
(defn topbar-options
[title]
{:elevation 0
:title {:color (if (colors/dark?) colors/white colors/black)
:text (i18n/label title)}
:rightButtonColor (if (colors/dark?) colors/white colors/black)
:background {:color (if (colors/dark?) colors/black colors/white)}
:backButton {:color (if (colors/dark?) colors/white colors/black)
:id :legacy-back-button
:testID :back-button
:visible true
:popStackOnPress false}})
(defn screens
[]
@@ -77,7 +84,7 @@
{:name :stickers
:options {:insets {:top? true}
:topBar {:title {:text (i18n/label :t/sticker-market)}}}
:topBar (topbar-options :t/sticker-market)}
:component stickers/packs}
{:name :stickers-pack
@@ -90,7 +97,7 @@
:component group-chat/new-group}
{:name :currency-settings
:options {:topBar {:title {:text (i18n/label :t/main-currency)}}
:options {:topBar (topbar-options :t/main-currency)
:insets {:top? true}}
:component currency-settings/currency-settings}
@@ -100,27 +107,27 @@
:options {:topBar {:visible false}}
:component profile.user/my-profile}
{:name :contacts-list
:options {:topBar {:title {:text (i18n/label :t/contacts)}}
:options {:topBar (topbar-options :t/contacts)
:insets {:top? true}}
:component contacts-list/contacts-list}
{:name :ens-main
:options {:topBar {:title {:text (i18n/label :t/ens-usernames)}}
:options {:topBar (topbar-options :t/ens-usernames)
:insets {:top? true}}
:component ens/main}
{:name :ens-search
:options {:topBar {:title {:text (i18n/label :t/ens-your-username)}}
:options {:topBar (topbar-options :t/ens-your-username)
:insets {:top? true}}
:component ens/search}
{:name :ens-checkout
:options {:topBar {:title {:text (i18n/label :t/ens-your-username)}}
:options {:topBar (topbar-options :t/ens-your-username)
:insets {:top? true}}
:component ens/checkout}
{:name :ens-confirmation
:options {:topBar {:title {:text (i18n/label :t/ens-your-username)}}
:options {:topBar (topbar-options :t/ens-your-username)
:insets {:top? true}}
:component ens/confirmation}
{:name :ens-terms
:options {:topBar {:title {:text (i18n/label :t/ens-terms-registration)}}
:options {:topBar (topbar-options :t/ens-terms-registration)
:insets {:top? true}}
:component ens/terms}
{:name :ens-name-details
@@ -128,7 +135,7 @@
:options {:insets {:top? true}}
:component ens/name-details}
{:name :blocked-users-list
:options {:topBar {:title {:text (i18n/label :t/blocked-users)}}
:options {:topBar (topbar-options :t/blocked-users)
:insets {:top? true}}
:component contacts-list/blocked-users-list}
{:name :wakuv2-settings
@@ -144,7 +151,7 @@
:options {:insets {:top? true}}
:component bootnodes-settings/bootnodes-settings}
{:name :installations
:options {:topBar {:title {:text (i18n/label :t/devices)}}
:options {:topBar (topbar-options (i18n/label :t/devices))
:insets {:top? true}}
:component pairing/installations}
{:name :edit-bootnode
@@ -160,63 +167,63 @@
:options {:insets {:top? true}}
:component edit-mailserver/edit-mailserver}
{:name :dapps-permissions
:options {:topBar {:title {:text (i18n/label :t/dapps-permissions)}}
:options {:topBar (topbar-options :t/dapps-permissions)
:insets {:top? true}}
:component dapps-permissions/dapps-permissions}
{:name :privacy-and-security
:options {:topBar {:title {:text (i18n/label :t/privacy-and-security)}}
:options {:topBar (topbar-options :t/privacy-and-security)
:insets {:top? true}}
:component privacy-and-security/privacy-and-security}
{:name :messages-from-contacts-only
:options {:topBar {:title {:text (i18n/label :t/accept-new-chats-from)}}
:options {:topBar (topbar-options :t/accept-new-chats-from)
:insets {:top? true}}
:component messages-from-contacts-only/messages-from-contacts-only-view}
{:name :appearance
:options {:topBar {:title {:text (i18n/label :t/appearance)}}
:options {:topBar (topbar-options :t/appearance)
:insets {:top? true}}
:component appearance/appearance-view}
{:name :privacy-and-security-profile-pic-show-to
:options {:topbar {:title {:text (i18n/label :t/show-profile-pictures-to)}}
:options {:topbar (topbar-options :t/show-profile-pictures-to)
:insets {:top? true}}
:component privacy-and-security/profile-pic-show-to}
{:name :privacy-and-security-profile-pic
:options {:topBar {:title {:text (i18n/label :t/show-profile-pictures)}}
:options {:topBar (topbar-options :t/show-profile-pictures)
:insets {:top? true}}
:component privacy-and-security/profile-pic}
{:name :notifications
:options {:topBar {:title {:text (i18n/label :t/notification-settings)}}
:options {:topBar (topbar-options :t/notification-settings)
:insets {:top? true}}
:component notifications-settings/notifications-settings}
{:name :sync-settings
:options {:topBar {:title {:text (i18n/label :t/sync-settings)}}
:options {:topBar (topbar-options :t/sync-settings)
:insets {:top? true}}
:component sync-settings/sync-settings}
{:name :advanced-settings
:options {:topBar {:title {:text (i18n/label :t/advanced)}}
:options {:topBar (topbar-options :t/advanced)
:insets {:top? true}}
:component advanced-settings/advanced-settings}
{:name :help-center
:options {:topBar {:title {:text (i18n/label :t/need-help)}}
:options {:topBar (topbar-options :t/need-help)
:insets {:top? true}}
:component help-center/help-center}
{:name :glossary
:options {:topBar {:title {:text (i18n/label :t/glossary)}}
:options {:topBar (topbar-options :t/glossary)
:insets {:top? true}}
:component glossary/glossary}
{:name :about-app
:options {:topBar {:title {:text (i18n/label :t/about-app)}}
:options {:topBar (topbar-options :t/about-app)
:insets {:top? true}}
:component about-app/about-app}
{:name :privacy-policy
:options {:topBar {:title {:text (i18n/label :t/privacy-policy)}}
:options {:topBar (topbar-options :t/privacy-policy)
:insets {:top? true}}
:component about-app/privacy-policy}
{:name :terms-of-service
:options {:topBar {:title {:text (i18n/label :t/terms-of-service)}}
:options {:topBar (topbar-options :t/terms-of-service)
:insets {:top? true}}
:component about-app/tos}
{:name :principles
:options {:topBar {:title {:text (i18n/label :t/principles)}}
:options {:topBar (topbar-options :t/principles)
:insets {:top? true}}
:component about-app/principles}
{:name :manage-dapps-permissions
@@ -224,27 +231,27 @@
:options {:insets {:top? true}}
:component dapps-permissions/manage}
{:name :rpc-usage-info
:options {:topBar {:title {:text (i18n/label :t/rpc-usage-info)}}
:options {:topBar (topbar-options :t/rpc-usage-info)
:insets {:top? true}}
:component rpc-usage-info/usage-info}
{:name :peers-stats
:options {:topBar {:title {:text (i18n/label :t/peers-stats)}}
:options {:topBar (topbar-options :t/peers-stats)
:insets {:top? true}}
:component peers-stats/peers-stats}
{:name :log-level-settings
:options {:topBar {:title {:text (i18n/label :t/log-level-settings)}}
:options {:topBar (topbar-options :t/log-level-settings)
:insets {:top? true}}
:component log-level-settings/log-level-settings}
{:name :fleet-settings
:options {:topBar {:title {:text (i18n/label :t/fleet-settings)}}
:options {:topBar (topbar-options :t/fleet-settings)
:insets {:top? true}}
:component fleet-settings/fleet-settings}
{:name :mobile-network-settings
:options {:topBar {:title {:text (i18n/label :t/mobile-network-settings)}}
:options {:topBar (topbar-options :t/mobile-network-settings)
:insets {:top? true}}
:component mobile-network-settings/mobile-network-settings}
{:name :backup-settings
:options {:topBar {:title {:text (i18n/label :t/backup-settings)}}
:options {:topBar (topbar-options :t/backup-settings)
:insets {:top? true}}
:component backup-settings/backup-settings}
{:name :backup-seed
@@ -252,14 +259,14 @@
:options {:insets {:top? true}}
:component profile.seed/backup-seed}
{:name :reset-password
:options {:topBar {:title {:text (i18n/label :t/reset-password)}}
:options {:topBar (topbar-options :t/reset-password)
:insets {:top? true}}
:component reset-password/reset-password}
{:name :delete-profile
:insets {:bottom? true}
:component delete-profile/delete-profile}
{:name :default-sync-period-settings
:options {:topBar {:title {:text (i18n/label :t/default-sync-period)}}
:options {:topBar (topbar-options :t/default-sync-period)
:insets {:top? true}}
:component default-sync-period-settings/default-sync-period-settings}
@@ -268,7 +275,7 @@
;[Chat] Link preview settings
{:name :link-previews-settings
:options {:topBar {:title {:text (i18n/label :t/chat-link-previews)}}
:options {:topBar (topbar-options :t/chat-link-previews)
:insets {:top? true}}
:component link-previews-settings/link-previews-settings}
@@ -308,7 +315,7 @@
;;TODO WHY MODAL?
;[Profile] Notifications settings
{:name :notifications-settings
:options {:topBar {:title {:text (i18n/label :t/notification-settings)}}
:options {:topBar (topbar-options :t/notification-settings)
:popGesture false
:hardwareBackButton {:dismissModalOnPress false
:popStackOnPress false}
+1 -1
View File
@@ -2,7 +2,7 @@
(:require-macros [legacy.status-im.utils.slurp :refer [slurp]])
(:require
[legacy.status-im.fleet.default-fleet :refer (default-fleets)])
(:require [legacy.status-im.utils.test :as utils.test]))
(:require [tests.test-utils :as utils.test]))
;; to generate a js Proxy at js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ that accept any (.xxx) call and
;; return itself
@@ -1,7 +1,6 @@
(ns quo.components.colors.color-picker.component-spec
(:require
[quo.components.colors.color-picker.view :as color-picker]
[reagent.core :as reagent]
[test-helpers.component :as h]))
(h/describe "color-picker"
@@ -12,7 +11,7 @@
(-> (h/expect event)
(.toHaveBeenCalled))))
(h/test "color picker color changed"
(let [selected (reagent/atom nil)]
(let [selected (atom nil)]
(h/render [color-picker/view {:on-change #(reset! selected %)}])
(h/fire-event :press (get (h/get-all-by-label-text :color-picker-item) 0))
(-> (h/expect @selected)
+76 -76
View File
@@ -6,8 +6,7 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]))
[react-native.reanimated :as reanimated]))
(def header-height 56)
@@ -57,7 +56,7 @@
:justify-content :center
:align-items :flex-end})
(defn title-style
(defn get-title-style
[{:keys [left right]} title-align]
(merge
absolute-fill
@@ -133,76 +132,77 @@
:size :large}
title])])
(defn- header-internal
[{:keys [left-width right-width]}]
(let [layout (reagent/atom {:left {:width (or left-width 8)
:height header-height}
:right {:width (or right-width 8)
:height header-height}
:title {:width 0
:height header-height}})
handle-layout (fn [el get-layout]
(fn [evt]
(let [width (oget evt "nativeEvent" "layout" "width")
height (oget evt "nativeEvent" "layout" "height")]
(when get-layout
(get-layout el
{:width width
:height height}))
(swap! layout assoc
el
{:width width
:height height}))))]
(fn
[{:keys [left-accessories left-component border-bottom
right-accessories right-component insets get-layout
title subtitle title-component style title-align
background theme]
:or {title-align :center
border-bottom false}}]
(let [status-bar-height (get insets :top 0)
height (+ header-height status-bar-height)]
[reanimated/view
{:style (header-wrapper-style {:height height
:background background
:border-bottom border-bottom
:theme theme})}
[rn/view
{:pointer-events :box-none
:height status-bar-height}]
[rn/view
{:style (merge {:height header-height}
style)
:pointer-events :box-none}
[rn/view
{:style absolute-fill
:pointer-events :box-none}
[rn/view
{:style content
:pointer-events :box-none}
[rn/view
{:style left-style
:on-layout (handle-layout :left get-layout)
:pointer-events :box-none}
[header-actions
{:accessories left-accessories
:component left-component}]]
[rn/view
{:style (title-style @layout title-align)
:on-layout (handle-layout :title get-layout)
:pointer-events :box-none}
[header-title
{:title title
:subtitle subtitle
:title-align title-align
:component title-component}]]
[rn/view
{:style right-style
:on-layout (handle-layout :right get-layout)
:pointer-events :box-none}
[header-actions
{:accessories right-accessories
:component right-component}]]]]]]))))
(def header (quo.theme/with-theme header-internal))
(defn header
[{:keys [left-accessories left-component border-bottom left-width right-width
right-accessories right-component insets get-layout
title subtitle title-component style title-align
background]
:or {title-align :center
border-bottom false}}]
(let [theme (quo.theme/use-theme-value)
[layout set-layout] (rn/use-state {:left {:width (or left-width 8)
:height header-height}
:right {:width (or right-width 8)
:height header-height}
:title {:width 0
:height header-height}})
handle-layout (rn/use-callback
(fn [el get-layout]
(fn [evt]
(let [width (oget evt "nativeEvent" "layout" "width")
height (oget evt "nativeEvent" "layout" "height")]
(when get-layout
(get-layout el
{:width width
:height height}))
(set-layout
(assoc layout
el
{:width width
:height height})))))
[layout])
status-bar-height (get insets :top 0)
height (+ header-height status-bar-height)
title-style (rn/use-memo (fn [] (get-title-style layout title-align))
[layout title-align])]
[reanimated/view
{:style (header-wrapper-style {:height height
:background background
:border-bottom border-bottom
:theme theme})}
[rn/view
{:pointer-events :box-none
:height status-bar-height}]
[rn/view
{:style (merge {:height header-height}
style)
:pointer-events :box-none}
[rn/view
{:style absolute-fill
:pointer-events :box-none}
[rn/view
{:style content
:pointer-events :box-none}
[rn/view
{:style left-style
:on-layout (handle-layout :left get-layout)
:pointer-events :box-none}
[header-actions
{:accessories left-accessories
:component left-component}]]
[rn/view
{:style title-style
:on-layout (handle-layout :title get-layout)
:pointer-events :box-none}
[header-title
{:title title
:subtitle subtitle
:title-align title-align
:component title-component}]]
[rn/view
{:style right-style
:on-layout (handle-layout :right get-layout)
:pointer-events :box-none}
[header-actions
{:accessories right-accessories
:component right-component}]]]]]]))
@@ -3,7 +3,7 @@
[quo.foundations.colors :as colors]))
(def ^:private themes
{:light {:default {:bg colors/white
{:light {:default {:bg colors/neutral-5
:border colors/neutral-20
:icon colors/neutral-50
:text colors/neutral-100}
@@ -10,7 +10,7 @@
:background-color (colors/theme-colors colors/neutral-10
colors/neutral-80-opa-80
theme)
:height 40
:min-height 40
:padding-horizontal 12
:padding-vertical 9
:margin-top 4})
@@ -22,11 +22,12 @@
(defn- locked-input-internal
[{:keys [label icon container-style theme]} value]
[rn/view {:style container-style}
[text/text
{:size :paragraph-2
:weight :medium
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}}
label]
(when label
[text/text
{:size :paragraph-2
:weight :medium
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}}
label])
[info-box
{:theme theme
:icon icon
@@ -60,8 +60,8 @@
:padding-horizontal 12
:padding-top 10
:padding-bottom 12
:height (if (= :message size) 245 266)
:width (if (= :message size) 295 335)})
:width (if (= :message size) 295 335)
:flex 1})
(def header-container
{:flex-direction :row
@@ -7,7 +7,6 @@
{:title "Some title"
:description "Some description"
:link "status.im"
:logo "data:image/png,logo-x"
:thumbnail "data:image/png,whatever"})
(h/describe "Links - Link Preview"
@@ -21,17 +20,12 @@
(h/is-truthy (h/query-by-text (:title props)))
(h/is-truthy (h/query-by-text (:description props)))
(h/is-truthy (h/query-by-text (:link props)))
(h/is-truthy (h/query-by-label-text :logo))
(h/is-truthy (h/query-by-label-text :thumbnail)))
(h/test "does not render thumbnail if prop is not present"
(h/render [view/view (dissoc props :thumbnail)])
(h/is-null (h/query-by-label-text :thumbnail)))
(h/test "does not render logo if prop is not present"
(h/render [view/view (dissoc props :logo)])
(h/is-null (h/query-by-label-text :logo)))
(h/test "shows button to enable preview when preview is disabled"
(h/render [view/view
(assoc props
@@ -1,9 +1,14 @@
(ns quo.components.links.link-preview.view
(:require
["react-native-blob-util" :default ReactNativeBlobUtil]
[oops.core :as oops]
[quo.components.buttons.button.view :as button]
[quo.components.links.link-preview.style :as style]
[quo.components.markdown.text :as text]
[react-native.core :as rn]))
[react-native.core :as rn]
[react-native.platform :as platform]
[react-native.svg :as svg]
[taoensso.timbre :as log]))
(defn- button-disabled
[disabled-text on-enable]
@@ -51,14 +56,30 @@
thumbnail)
:accessibility-label :thumbnail}])
(defn- get-image-data
[logo set-is-svg on-success]
(-> (.config ReactNativeBlobUtil (clj->js {:trusty platform/ios?}))
(.fetch "GET" logo)
(.then (fn [imgObj]
(set-is-svg (= "image/svg"
(oops/oget imgObj
["respInfo" "headers" "Content-Type"])))
(on-success (oops/oget imgObj "data"))))
(.catch #(log/error "could not fetch favicon " logo))))
(defn- logo-comp
[logo]
[rn/image
{:accessibility-label :logo
:source (if (string? logo)
{:uri logo}
logo)
:style style/logo}])
(let [[image-data set-image-data] (rn/use-state nil)
[is-svg? set-is-svg] (rn/use-state nil)
on-success (fn [data-uri]
(set-image-data data-uri))
_get-image-data (get-image-data logo set-is-svg on-success)]
(if is-svg?
[svg/svg-xml (merge style/logo {:xml image-data})]
[rn/image
{:accessibility-label :logo
:source {:uri (str "data:image/png;base64," image-data)}
:style style/logo}])))
(defn view
[{:keys [title logo description link thumbnail
@@ -21,6 +21,7 @@
:border-radius 12
:border-style :dashed
:align-items :center
:flex-direction :row
:justify-content :center
:align-self :stretch
:padding horizontal-padding
+27 -8
View File
@@ -1,19 +1,31 @@
(ns quo.components.links.url-preview.view
(:require
[clojure.string :as string]
[quo.components.icon :as icon]
[quo.components.links.url-preview.style :as style]
[quo.components.markdown.text :as text]
[quo.foundations.colors :as colors]
[react-native.core :as rn]))
[react-native.core :as rn]
[react-native.svg :as svg]))
(def base64-svg-prefix "data:image/svg;base64,")
(defn- logo-comp
[{:keys [logo]}]
[rn/image
{:accessibility-label :logo
:source (if (string? logo)
{:uri logo}
logo)
:style style/logo}])
(if (and (string? logo)
(string/starts-with? logo base64-svg-prefix))
[svg/svg-xml
(merge style/logo
{:xml (-> logo
(string/replace base64-svg-prefix "")
js/atob)})]
[rn/image
{:accessibility-label :logo
:source (if (string? logo)
{:uri logo}
logo)
:style style/logo
:resize-mode :cover}]))
(defn- content
[{:keys [title body]}]
@@ -50,6 +62,10 @@
[rn/view
{:accessibility-label :url-preview-loading
:style (merge (style/loading-container) container-style)}
[icon/icon :i/loading
{:size 12
:color (colors/theme-colors colors/neutral-50 colors/neutral-40)
:container-style {:margin-right 8}}]
[rn/text
{:size :paragraph-2
:weight :medium
@@ -60,6 +76,9 @@
{:accessibility-label :url-preview
:style (merge (style/container) container-style)}
(when logo
[logo-comp {:logo logo}])
[logo-comp
{:logo (if (map? logo)
(:data-uri logo)
logo)}])
[content {:title title :body body}]
[clear-button {:on-press on-clear}]]))
@@ -32,7 +32,7 @@
[rn/view {:style style/url-preview-separator}])
(defn- item-component
[{:keys [title body loading? logo]} _ _
[{:keys [title body loading? logo url]} _ _
{:keys [width on-clear loading-message container-style]}]
[url-preview/view
{:logo logo
@@ -41,7 +41,8 @@
:loading? loading?
:loading-message loading-message
:on-clear on-clear
:container-style (merge container-style {:width width})}])
:container-style (merge container-style {:width width})
:url url}])
(defn- f-view
[]
@@ -27,7 +27,6 @@
[:blur? {:optional true} [:maybe :boolean]]
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
[:on-press {:optional true} [:maybe fn?]]
[:theme :schema.common/theme]
[:title-icon {:optional true} [:maybe :keyword]]
[:account-props
[:map
+33 -35
View File
@@ -9,7 +9,6 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]))
(defn- account-view
@@ -97,41 +96,40 @@
(colors/resolve-color customization-color theme))}]])
(defn- internal-view
[_]
(let [pressed? (reagent/atom false)
on-press-in #(reset! pressed? true)
on-press-out #(reset! pressed? false)]
(fn [{:keys [type state blur? customization-color on-press]
:or {customization-color :blue
type :default
state :default
blur? false}
:as props}]
[rn/pressable
{:style (style/container
{:state state
:blur? blur?
:customization-color customization-color
:pressed? @pressed?})
:on-press-in on-press-in
:on-press on-press
:on-press-out on-press-out
:accessibility-label :container}
[account-view props]
[rn/view {:style (when (= type :tag) style/token-tag-container)}
(cond
(#{:balance-neutral :balance-negative :balance-positive} type)
[balance-view props]
[{:keys [type state blur? customization-color on-press]
:or {customization-color :blue
type :default
state :default
blur? false}
:as props}]
(let [theme (quo.theme/use-theme-value)
[pressed? set-pressed] (rn/use-state false)
on-press-in (rn/use-callback #(set-pressed true))
on-press-out (rn/use-callback #(set-pressed false))
props (assoc props :theme theme)]
[rn/pressable
{:style (style/container
{:state state
:blur? blur?
:customization-color customization-color
:pressed? pressed?})
:on-press-in on-press-in
:on-press on-press
:on-press-out on-press-out
:accessibility-label :container}
[account-view props]
[rn/view {:style (when (= type :tag) style/token-tag-container)}
(cond
(#{:balance-neutral :balance-negative :balance-positive} type)
[balance-view props]
(= type :tag)
[token-tag props]
(= type :tag)
[token-tag props]
(= type :action)
[options-button props]
(= type :action)
[options-button props]
(and (= type :default) (= state :selected))
[check-icon props])]])))
(and (= type :default) (= state :selected))
[check-icon props])]]))
(def view
(quo.theme/with-theme
(schema/instrument #'internal-view component-schema/?schema)))
(def view (schema/instrument #'internal-view component-schema/?schema))
@@ -5,7 +5,6 @@
[:action {:optional true} [:enum :icon :none]]
[:blur? {:optional true} [:maybe :boolean]]
[:on-press {:optional true} [:maybe fn?]]
[:theme :schema.common/theme]
[:account-props
[:map {:closed true}
[:type [:enum :default :watch-only]]
@@ -9,38 +9,37 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]))
(defn- internal-view
[]
(let [state (reagent/atom :default)]
(fn [{:keys [action blur? account-props networks on-press on-options-press theme]}]
[rn/pressable
{:style (style/container {:state @state :blur? blur? :theme theme})
:on-press-in #(reset! state :pressed)
:on-press-out #(reset! state :default)
:on-press on-press
:accessibility-label :container}
[rn/view {:style style/left-container}
[account-avatar/view account-props]
[rn/view {:style {:margin-left 8}}
[text/text
{:weight :semi-bold
:size :paragraph-2}
(:name account-props)]
[address-text/view
{:networks networks
:address (:address account-props)
:format :short}]]]
(when (= action :icon)
[rn/pressable {:on-press on-options-press}
[icon/icon :i/options
{:color (if blur?
colors/white-opa-70
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme))
:accessibility-label :icon}]])])))
[{:keys [action blur? account-props networks on-press on-options-press]}]
(let [theme (quo.theme/use-theme-value)
[state set-state] (rn/use-state :default)
on-press-in (rn/use-callback #(set-state :pressed))
on-press-out (rn/use-callback #(set-state :default))]
[rn/pressable
{:style (style/container {:state state :blur? blur? :theme theme})
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :container}
[rn/view {:style style/left-container}
[account-avatar/view account-props]
[rn/view {:style {:margin-left 8}}
[text/text
{:weight :semi-bold
:size :paragraph-2}
(:name account-props)]
[address-text/view
{:networks networks
:address (:address account-props)
:format :short}]]]
(when (= action :icon)
[rn/pressable {:on-press on-options-press}
[icon/icon :i/options
{:color (if blur?
colors/white-opa-70
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme))
:accessibility-label :icon}]])]))
(def view
(quo.theme/with-theme
(schema/instrument #'internal-view component-schema/?schema)))
(def view (schema/instrument #'internal-view component-schema/?schema))
@@ -9,6 +9,5 @@
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
[:on-press {:optional true} [:maybe fn?]]
[:blur? {:optional true} [:maybe :boolean]]
[:theme :schema.common/theme]
[:active-state? {:optional true} [:maybe :boolean]]]]
:any])
+32 -32
View File
@@ -1,13 +1,12 @@
(ns quo.components.list-items.address.view
(:require
[quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar]
(quo.components.list-items.address.schema :as component-schema)
[quo.components.list-items.address.schema :as component-schema]
[quo.components.list-items.address.style :as style]
[quo.components.markdown.text :as text]
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]
[utils.address :as address]))
@@ -39,34 +38,35 @@
(address/get-shortened-key address)]]]])
(defn- internal-view
[]
(let [state (reagent/atom :default)
active? (atom false)
timer (atom nil)
on-press-in (fn []
(when-not (= @state :selected)
(reset! timer (js/setTimeout #(reset! state :pressed) 100))))]
(fn [{:keys [networks address customization-color on-press active-state? blur? theme]
:or {customization-color :blue}}]
(let [on-press-out (fn []
(let [new-state (if (or (not active-state?) @active?) :default :active)]
(when @timer (js/clearTimeout @timer))
(reset! timer nil)
(reset! active? (= new-state :active))
(reset! state new-state)))]
[rn/pressable
{:style (style/container @state customization-color blur?)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :container}
[left-container
{:theme theme
:networks networks
:address address
:blur? blur?}]]))))
(def view
(quo.theme/with-theme
(schema/instrument #'internal-view component-schema/?schema)))
[{:keys [networks address customization-color on-press active-state? blur?]
:or {customization-color :blue}}]
(let [theme (quo.theme/use-theme-value)
[state set-state] (rn/use-state :default)
active? (rn/use-ref-atom false)
timer (rn/use-ref-atom nil)
on-press-in (rn/use-callback
(fn []
(when-not (= state :selected)
(reset! timer (js/setTimeout #(set-state :pressed) 100))))
[state])
on-press-out (rn/use-callback
(fn []
(let [new-state (if (or (not active-state?) @active?) :default :active)]
(when @timer (js/clearTimeout @timer))
(reset! timer nil)
(reset! active? (= new-state :active))
(set-state new-state)))
[active-state?])]
[rn/pressable
{:style (style/container state customization-color blur?)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :container}
[left-container
{:theme theme
:networks networks
:address address
:blur? blur?}]]))
(def view (schema/instrument #'internal-view component-schema/?schema))
+37 -39
View File
@@ -5,11 +5,10 @@
[quo.components.icon :as quo.icons]
[quo.components.list-items.channel.style :as style]
[quo.components.markdown.text :as quo.text]
[quo.theme :as theme]
[react-native.core :as rn]
[reagent.core :as reagent]))
[quo.theme]
[react-native.core :as rn]))
(defn- view-internal
(defn view
"Options:
- notification - (nil/:notification/:mention/:mute, default: nil):
- :notification - Display a grey dot.
@@ -27,38 +26,37 @@
- on-press - (function, default: nil) - Function called when the component is pressed.
- on-long-press - (function, default: nil) - Function called when the component is long pressed.
- theme - Theme value from with-theme HOC"
[]
(let [pressed? (reagent/atom false)]
(fn [{:keys [notification locked? mentions-count customization-color emoji name on-press
on-long-press theme]}]
[rn/pressable
{:style (style/container @pressed? customization-color theme)
:accessibility-label :channel-list-item
:on-press on-press
:on-long-press on-long-press
:on-press-in #(reset! pressed? true)
:on-press-out #(reset! pressed? false)}
[channel-avatar/view
{:size :size-32
:locked? locked?
:full-name name
:customization-color customization-color
:emoji emoji}]
[quo.text/text
{:style (style/label notification theme)
:weight :medium
:size :paragraph-1} (str "# " name)]
(when-not locked?
(condp = notification
:mute [quo.icons/icon :i/muted
{:color (style/mute-notification-icon-color theme)}]
:mention [counter/view
{:customization-color customization-color
:container-style (style/counter mentions-count)}
mentions-count]
:notification [quo.icons/icon :i/notification
{:color (style/mute-notification-icon-color theme)
:accessibility-label :unviewed-messages-public}]
nil))])))
(def view (theme/with-theme view-internal))
[{:keys [notification locked? mentions-count customization-color emoji name on-press on-long-press]}]
(let [theme (quo.theme/use-theme-value)
[pressed? set-pressed] (rn/use-state false)
on-press-in (rn/use-callback #(set-pressed true))
on-press-out (rn/use-callback #(set-pressed false))]
[rn/pressable
{:style (style/container pressed? customization-color theme)
:accessibility-label :channel-list-item
:on-press on-press
:on-long-press on-long-press
:on-press-in on-press-in
:on-press-out on-press-out}
[channel-avatar/view
{:size :size-32
:locked? locked?
:full-name name
:customization-color customization-color
:emoji emoji}]
[quo.text/text
{:style (style/label notification theme)
:weight :medium
:size :paragraph-1} (str "# " name)]
(when-not locked?
(condp = notification
:mute [quo.icons/icon :i/muted
{:color (style/mute-notification-icon-color theme)}]
:mention [counter/view
{:customization-color customization-color
:container-style (style/counter mentions-count)}
mentions-count]
:notification [quo.icons/icon :i/notification
{:color (style/mute-notification-icon-color theme)
:accessibility-label :unviewed-messages-public}]
nil))]))
@@ -7,8 +7,7 @@
[quo.components.markdown.text :as text]
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]))
[react-native.core :as rn]))
(defn- logo-component
[logo]
@@ -83,7 +82,7 @@
:style {:margin-left 10}}
component])))
(defn- view-internal
(defn view
"Options:
:type - :discover/engage/share
@@ -116,49 +115,48 @@
:unread-count - number - When the info is :mention, it will be used to show
the number of unread mentions.
"
[]
(let [pressed? (reagent/atom false)]
(fn [{:keys [members type info tokens locked? title subtitle
logo blur? customization-color
on-press on-long-press on-press-info
container-style unread-count theme]}]
[rn/pressable
{:accessibility-label :container
:on-press-in (fn [] (reset! pressed? true))
:on-press on-press
:on-long-press on-long-press
:on-press-out (fn [] (reset! pressed? false))
:style (merge (style/container {:blur? blur?
:customization-color customization-color
:info info
:type type
:pressed? @pressed?
:theme theme})
container-style)}
[logo-component logo]
[rn/view {:style {:flex 1}}
[title-component
{:blur? blur?
:info info
:theme theme
:title title
:type type}]
(when (and (= type :share) subtitle)
[subtitle-component subtitle blur? theme])
(when (and members (= type :discover))
[community-view/community-stats-column
{:type :list-view
:members-count (:members-count members)
:active-count (:active-count members)}])]
[info-component
{:blur? blur?
:customization-color customization-color
:info info
:type type
:locked? locked?
:on-press-info on-press-info
:theme theme
:tokens tokens
:unread-count unread-count}]])))
[{:keys [members type info tokens locked? title subtitle logo blur? customization-color
on-press on-long-press on-press-info container-style unread-count]}]
(let [theme (quo.theme/use-theme-value)
[pressed? set-pressed] (rn/use-state false)
on-press-in (rn/use-callback #(set-pressed true))
on-press-out (rn/use-callback #(set-pressed false))]
[rn/pressable
{:accessibility-label :container
(def view (quo.theme/with-theme view-internal))
:on-press on-press
:on-long-press on-long-press
:on-press-in on-press-in
:on-press-out on-press-out
:style (merge (style/container {:blur? blur?
:customization-color customization-color
:info info
:type type
:pressed? pressed?
:theme theme})
container-style)}
[logo-component logo]
[rn/view {:style {:flex 1}}
[title-component
{:blur? blur?
:info info
:theme theme
:title title
:type type}]
(when (and (= type :share) subtitle)
[subtitle-component subtitle blur? theme])
(when (and members (= type :discover))
[community-view/community-stats-column
{:type :list-view
:members-count (:members-count members)
:active-count (:active-count members)}])]
[info-component
{:blur? blur?
:customization-color customization-color
:info info
:type type
:locked? locked?
:on-press-info on-press-info
:theme theme
:tokens tokens
:unread-count unread-count}]]))
+37 -39
View File
@@ -6,43 +6,41 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[react-native.fast-image :as fast-image]
[reagent.core :as reagent]))
[react-native.fast-image :as fast-image]))
(defn- view-internal
[]
(let [pressed? (reagent/atom false)]
(fn [{:keys [dapp action on-press on-press-icon theme] :as props}]
[rn/pressable
{:style (style/container (assoc props :pressed? @pressed?))
:on-press on-press
:on-press-in #(reset! pressed? true)
:on-press-out #(reset! pressed? false)}
[rn/view {:style style/container-info}
[fast-image/fast-image
{:source (:avatar dapp)
:style {:width 32 :height 32}}]
[rn/view {:style style/user-info}
[text/text
{:weight :semi-bold
:size :paragraph-1
:style (style/style-text-name theme)}
(:name dapp)]
[text/text
{:weight :regular
:size :paragraph-2
:style (style/style-text-value theme)}
(:value dapp)]]]
(when (= action :icon)
[rn/pressable
{:on-press on-press-icon
:testID "dapp-component-icon"}
[icons/icon :i/options
{:color (colors/theme-colors
colors/neutral-50
colors/neutral-40
theme)
:accessibility-label :icon}]])])))
(def view
(quo.theme/with-theme view-internal))
(defn view
[{:keys [dapp action on-press on-press-icon] :as props}]
(let [theme (quo.theme/use-theme-value)
[pressed? set-pressed] (rn/use-state false)
on-press-in (rn/use-callback #(set-pressed true))
on-press-out (rn/use-callback #(set-pressed false))]
[rn/pressable
{:style (style/container (assoc props :pressed? pressed?))
:on-press on-press
:on-press-in on-press-in
:on-press-out on-press-out}
[rn/view {:style style/container-info}
[fast-image/fast-image
{:source (:avatar dapp)
:style {:width 32 :height 32}}]
[rn/view {:style style/user-info}
[text/text
{:weight :semi-bold
:size :paragraph-1
:style (style/style-text-name theme)}
(:name dapp)]
[text/text
{:weight :regular
:size :paragraph-2
:style (style/style-text-value theme)}
(:value dapp)]]]
(when (= action :icon)
[rn/pressable
{:on-press on-press-icon
:testID "dapp-component-icon"}
[icons/icon :i/options
{:color (colors/theme-colors
colors/neutral-50
colors/neutral-40
theme)
:accessibility-label :icon}]])]))
@@ -5,7 +5,6 @@
[quo.components.markdown.text :as text]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]))
(defn- info
@@ -22,18 +21,19 @@
(if (string/blank? label) "-" label)]]])
(defn- values
[{:keys [token-value fiat-value theme]}]
[rn/view {:style style/values-container}
[text/text
{:weight :medium
:size :paragraph-2
:number-of-lines 1}
token-value]
[text/text
{:style (style/fiat-value theme)
:size :paragraph-2
:number-of-lines 1}
fiat-value]])
[{:keys [token-value fiat-value]}]
(let [theme (quo.theme/use-theme-value)]
[rn/view {:style style/values-container}
[text/text
{:weight :medium
:size :paragraph-2
:number-of-lines 1}
token-value]
[text/text
{:style (style/fiat-value theme)
:size :paragraph-2
:number-of-lines 1}
fiat-value]]))
(def ?schema
[:=>
@@ -51,26 +51,21 @@
:any])
(defn- view-internal
[]
(let [pressed? (reagent/atom false)
on-press-in #(reset! pressed? true)
on-press-out #(reset! pressed? false)]
(fn [{:keys [on-press state customization-color theme]
:as props
:or {customization-color :blue}}]
(let [internal-state (if @pressed?
:pressed
state)]
[{:keys [on-press state customization-color]
:as props
:or {customization-color :blue}}]
(let [theme (quo.theme/use-theme-value)
[pressed? set-pressed] (rn/use-state false)
on-press-in (rn/use-callback #(set-pressed true))
on-press-out (rn/use-callback #(set-pressed false))
internal-state (if pressed? :pressed state)]
[rn/pressable
{:style (style/container internal-state customization-color theme)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :network-list}
[info props]
[values props]]))
[rn/pressable
{:style (style/container internal-state customization-color theme)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :network-list}
[info props]
[values props]]))))
(def view
(quo.theme/with-theme
(schema/instrument #'view-internal ?schema)))
(def view (schema/instrument #'view-internal ?schema))
@@ -7,70 +7,69 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[utils.address :as address]))
(defn- left-container
[{:keys [blur? theme name ens address customization-color]}]
[rn/view {:style style/left-container}
[wallet-user-avatar/wallet-user-avatar
{:size :size-32
:full-name name
:customization-color customization-color}]
[rn/view {:style style/account-container}
[text/text
{:weight :semi-bold
:size :paragraph-1
:style style/name-text}
name]
[text/text {:size :paragraph-2}
[text/text
{:size :paragraph-2
:weight :monospace
:style (style/account-address blur? theme)}
(or ens (address/get-shortened-key address))]]]])
[{:keys [blur? name ens address customization-color]}]
(let [theme (quo.theme/use-theme-value)]
[rn/view {:style style/left-container}
[wallet-user-avatar/wallet-user-avatar
{:size :size-32
:full-name name
:customization-color customization-color}]
[rn/view {:style style/account-container}
[text/text
{:weight :semi-bold
:size :paragraph-1
:style style/name-text}
name]
[text/text {:size :paragraph-2}
[text/text
{:size :paragraph-2
:weight :monospace
:style (style/account-address blur? theme)}
(or ens (address/get-shortened-key address))]]]]))
(defn- internal-view
[]
(let [state (reagent/atom :default)
active? (atom false)
timer (atom nil)
on-press-in (fn []
(when-not (= @state :selected)
(reset! timer (js/setTimeout #(reset! state :pressed) 100))))]
(fn [{:keys [blur? user-props active-state? customization-color
on-press
on-options-press
theme]
:or {customization-color :blue
blur? false}}]
(let [on-press-out (fn []
(let [new-state (if (or (not active-state?) @active?) :default :active)]
(when @timer (js/clearTimeout @timer))
(reset! timer nil)
(reset! active? (= new-state :active))
(reset! state new-state)))]
[rn/pressable
{:style (style/container
{:state @state :blur? blur? :customization-color customization-color})
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :container}
[left-container
{:blur? blur?
:theme theme
:name (:name user-props)
:ens (:ens user-props)
:address (:address user-props)
:customization-color (or (:customization-color user-props) :blue)}]
(when on-options-press
[rn/pressable
{:accessibility-label :options-button
:on-press on-options-press}
[icon/icon :i/options
{:color (if blur?
colors/white-opa-70
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}]])]))))
(def view (quo.theme/with-theme internal-view))
(defn view
[{:keys [blur? user-props active-state? customization-color on-press on-options-press]
:or {customization-color :blue
blur? false}}]
(let [theme (quo.theme/use-theme-value)
[state set-state] (rn/use-state :default)
active? (rn/use-ref-atom false)
timer (rn/use-ref-atom nil)
on-press-in (rn/use-callback
(fn []
(when-not (= state :selected)
(reset! timer (js/setTimeout #(set-state :pressed) 100))))
[state])
on-press-out (rn/use-callback
(fn []
(let [new-state (if (or (not active-state?) @active?) :default :active)]
(when @timer (js/clearTimeout @timer))
(reset! timer nil)
(reset! active? (= new-state :active))
(set-state new-state)))
[active-state?])]
[rn/pressable
{:style (style/container
{:state state :blur? blur? :customization-color customization-color})
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :container}
[left-container
{:blur? blur?
:theme theme
:name (:name user-props)
:ens (:ens user-props)
:address (:address user-props)
:customization-color (or (:customization-color user-props) :blue)}]
(when on-options-press
[rn/pressable
{:accessibility-label :options-button
:on-press on-options-press}
[icon/icon :i/options
{:color (if blur?
colors/white-opa-70
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}]])]))
@@ -23,7 +23,6 @@
[:contact-props ?contact]
[:accounts {:optional true} ?accounts]
[:on-press {:optional true} [:maybe fn?]]
[:theme :schema.common/theme]
[:active-state? {:optional true} [:maybe :boolean]]
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]]]
:any])
@@ -9,91 +9,89 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]
[utils.address :as address]
[utils.i18n :as i18n]))
(defn- account
[{:keys [emoji name address customization-color theme]}]
[rn/view
{:accessibility-label :account-container
:style style/account-container}
[account-avatar/view
{:emoji emoji
:size 16
:customization-color customization-color}]
[text/text
{:size :paragraph-2
:weight :monospace
:style (style/account-name theme)}
name]
[rn/view {:style (style/dot-divider theme)}]
[text/text
{:size :paragraph-2
:weight :monospace
:style (style/account-address theme)}
(address/get-shortened-key address)]])
[{:keys [emoji name address customization-color]}]
(let [theme (quo.theme/use-theme-value)]
[rn/view
{:accessibility-label :account-container
:style style/account-container}
[account-avatar/view
{:emoji emoji
:size 16
:customization-color customization-color}]
[text/text
{:size :paragraph-2
:weight :monospace
:style (style/account-name theme)}
name]
[rn/view {:style (style/dot-divider theme)}]
[text/text
{:size :paragraph-2
:weight :monospace
:style (style/account-address theme)}
(address/get-shortened-key address)]]))
(defn- internal-view
[]
(let [state (reagent/atom :default)
active? (atom false)
timer (atom nil)
on-press-in (fn []
(when-not (= @state :selected)
(reset! timer (js/setTimeout #(reset! state :pressed) 100))))]
(fn [{:keys [contact-props accounts active-state? customization-color on-press theme]
:or {customization-color :blue
accounts []
active-state? true}}]
(let [accounts-count (count accounts)
account-props (when (= accounts-count 1)
(first accounts))
on-press-out (fn []
[{:keys [contact-props accounts active-state? customization-color on-press]
:or {customization-color :blue
accounts []
active-state? true}}]
(let [theme (quo.theme/use-theme-value)
[state set-state] (rn/use-state :default)
active? (rn/use-ref-atom false)
timer (rn/use-ref-atom nil)
on-press (rn/use-callback #(when on-press (on-press)))
on-press-in (rn/use-callback
(fn []
(when-not (= state :selected)
(reset! timer (js/setTimeout #(set-state :pressed) 100))))
[state])
accounts-count (count accounts)
account-props (when (= accounts-count 1) (first accounts))
on-press-out (rn/use-callback
(fn []
(let [new-state (if (or (not active-state?) @active?) :default :active)]
(when @timer (js/clearTimeout @timer))
(reset! timer nil)
(reset! active? (= new-state :active))
(reset! state new-state)))]
[rn/pressable
{:style (style/container
{:state @state :customization-color customization-color})
:on-press-in on-press-in
:on-press-out on-press-out
:on-press #(when on-press
(on-press))
:accessibility-label :container}
[rn/view {:style style/left-container}
[user-avatar/user-avatar (assoc contact-props :size :small)]
[rn/view {:style style/saved-contact-container}
[rn/view
{:style style/account-title-container}
[text/text
{:weight :semi-bold
:size :paragraph-1}
(:full-name contact-props)]
[icon/icon :i/contact
{:container-style style/contact-icon-container
:accessibility-label :contact-icon
:size 12
:color (colors/theme-colors colors/primary-50 colors/primary-60 theme)
:color-2 colors/white}]]
(if account-props
[account (assoc account-props :theme theme)]
[text/text
{:accessibility-label :accounts-count
:size :paragraph-2
:style (style/accounts-count theme)}
(i18n/label :t/accounts-count {:count accounts-count})])]]
(when (> accounts-count 1)
[icon/icon :i/chevron-right
{:accessibility-label :check-icon
:size 20
:color (colors/theme-colors colors/neutral-50
colors/neutral-40
theme)}])]))))
(set-state new-state)))
[active-state?])]
[rn/pressable
{:style (style/container {:state state :customization-color customization-color})
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :container}
[rn/view {:style style/left-container}
[user-avatar/user-avatar (assoc contact-props :size :small)]
[rn/view {:style style/saved-contact-container}
[rn/view
{:style style/account-title-container}
[text/text
{:weight :semi-bold
:size :paragraph-1}
(:full-name contact-props)]
[icon/icon :i/contact
{:container-style style/contact-icon-container
:accessibility-label :contact-icon
:size 12
:color (colors/theme-colors colors/primary-50 colors/primary-60 theme)
:color-2 colors/white}]]
(if account-props
[account (assoc account-props :theme theme)]
[text/text
{:accessibility-label :accounts-count
:size :paragraph-2
:style (style/accounts-count theme)}
(i18n/label :t/accounts-count {:count accounts-count})])]]
(when (> accounts-count 1)
[icon/icon :i/chevron-right
{:accessibility-label :check-icon
:size 20
:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}])]))
(def view
(quo.theme/with-theme
(schema/instrument #'internal-view component-schema/?schema)))
(def view (schema/instrument #'internal-view component-schema/?schema))
@@ -11,6 +11,5 @@
[:networks [:* [:map [:source :schema.common/image-source]]]]
[:on-press {:optional true} [:maybe fn?]]
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
[:state {:optional true} [:enum :default :active :selected]]
[:theme :schema.common/theme]]]
[:state {:optional true} [:enum :default :active :selected]]]]
:any])
@@ -8,7 +8,6 @@
[quo.components.utilities.token.view :as token]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]))
(defn- info
@@ -31,44 +30,40 @@
networks]]])
(defn- values
[{:keys [state token-value fiat-value customization-color theme]}]
(if (= state :selected)
[icon/icon :i/check
{:color (style/check-color customization-color theme)
:accessibility-label :check-icon}]
[rn/view {:style style/values-container}
[text/text
{:weight :medium
:size :paragraph-2
:number-of-lines 1}
token-value]
[text/text
{:style (style/fiat-value theme)
:size :paragraph-2
:number-of-lines 1}
fiat-value]]))
[{:keys [state token-value fiat-value customization-color]}]
(let [theme (quo.theme/use-theme-value)]
(if (= state :selected)
[icon/icon :i/check
{:color (style/check-color customization-color theme)
:accessibility-label :check-icon}]
[rn/view {:style style/values-container}
[text/text
{:weight :medium
:size :paragraph-2
:number-of-lines 1}
token-value]
[text/text
{:style (style/fiat-value theme)
:size :paragraph-2
:number-of-lines 1}
fiat-value]])))
(defn- view-internal
[]
(let [pressed? (reagent/atom false)
on-press-in #(reset! pressed? true)
on-press-out #(reset! pressed? false)]
(fn [{:keys [on-press state customization-color
_token _networks _token-value _fiat-value theme]
:as props
:or {customization-color :blue}}]
(let [internal-state (if @pressed?
:pressed
state)]
[rn/pressable
{:style (style/container internal-state customization-color theme)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :token-network}
[info props]
[values props]]))))
[{:keys [on-press state customization-color _token _networks _token-value _fiat-value]
:as props
:or {customization-color :blue}}]
(let [theme (quo.theme/use-theme-value)
[pressed? set-pressed] (rn/use-state false)
on-press-in (rn/use-callback #(set-pressed true))
on-press-out (rn/use-callback #(set-pressed false))
internal-state (if pressed? :pressed state)]
[rn/pressable
{:style (style/container internal-state customization-color theme)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :token-network}
[info props]
[values props]]))
(def view
(quo.theme/with-theme
(schema/instrument #'view-internal component-schema/?schema)))
(def view (schema/instrument #'view-internal component-schema/?schema))
@@ -17,7 +17,6 @@
[:values ?values]
[:on-press {:optional true} [:maybe fn?]]
[:on-long-press {:optional true} [:maybe fn?]]
[:theme :schema.common/theme]
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
[:metrics? {:optional true} [:maybe :boolean]]]]
:any])
@@ -9,64 +9,67 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]))
(defn- internal-view
[]
(let [state (reagent/atom :default)]
(fn [{:keys [theme customization-color status token metrics? values on-press on-long-press
token-name]}]
(let [bg-opacity (case @state
:active 10
:pressed 5
0)
{:keys [crypto-value fiat-value percentage-change fiat-change]} values]
[rn/pressable
{:style (style/container customization-color bg-opacity theme)
:on-press-in #(reset! state :pressed)
:on-press-out #(reset! state :default)
:on-press (fn []
(reset! state :active)
(js/setTimeout #(reset! state :default) 300)
on-press)
:on-long-press on-long-press
:accessibility-label :container}
[rn/view
{:style {:flex-direction :row
:align-items :center
:flex 1}}
[token/view {:token token :size :size-32}]
[rn/view {:style {:margin-left 8}}
[text/text {:weight :semi-bold} token-name]
[text/text
{:size :paragraph-2
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}}
(str crypto-value " " (if token (string/upper-case (clj->js token)) ""))]]]
[rn/view
{:style {:align-items :flex-end
:justify-content :space-between}}
[text/text
{:weight :medium
:size :paragraph-2} fiat-value]
(when metrics?
[rn/view
{:style {:flex-direction :row
:align-items :center}}
[text/text
{:size :paragraph-2
:style (style/metric-text status theme)} (str percentage-change "%")]
[rn/view {:style (style/dot-divider status theme)}]
[text/text
{:size :paragraph-2
:style (style/metric-text status theme)} fiat-change]
(when (not= status :empty)
[rn/view
{:style {:margin-left 4}
:accessibility-label :arrow-icon}
[icon/icon (if (= status :positive) :i/positive :i/negative)
(style/arrow-icon status theme)]])])]]))))
[{:keys [customization-color status token metrics? values on-press on-long-press token-name]}]
(let [theme (quo.theme/use-theme-value)
[state set-state] (rn/use-state :default)
bg-opacity (case state
:active 10
:pressed 5
0)
{:keys [crypto-value
fiat-value
percentage-change
fiat-change]} values
on-press-in (rn/use-callback #(set-state :pressed))
on-press-out (rn/use-callback #(set-state :default))
on-press (rn/use-callback
(fn []
(set-state :active)
(js/setTimeout #(set-state :default) 300)
on-press))]
[rn/pressable
{:style (style/container customization-color bg-opacity theme)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:on-long-press on-long-press
:accessibility-label :container}
[rn/view
{:style {:flex-direction :row
:align-items :center
:flex 1}}
[token/view {:token token :size :size-32}]
[rn/view {:style {:margin-left 8}}
[text/text {:weight :semi-bold} token-name]
[text/text
{:size :paragraph-2
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}}
(str crypto-value " " (if token (string/upper-case (clj->js token)) ""))]]]
[rn/view
{:style {:align-items :flex-end
:justify-content :space-between}}
[text/text
{:weight :medium
:size :paragraph-2} fiat-value]
(when metrics?
[rn/view
{:style {:flex-direction :row
:align-items :center}}
[text/text
{:size :paragraph-2
:style (style/metric-text status theme)} (str percentage-change "%")]
[rn/view {:style (style/dot-divider status theme)}]
[text/text
{:size :paragraph-2
:style (style/metric-text status theme)} fiat-change]
(when (not= status :empty)
[rn/view
{:style {:margin-left 4}
:accessibility-label :arrow-icon}
[icon/icon (if (= status :positive) :i/positive :i/negative)
(style/arrow-icon status theme)]])])]]))
(def view
(quo.theme/with-theme
(schema/instrument #'internal-view component-schema/?schema)))
(def view (schema/instrument #'internal-view component-schema/?schema))
+22 -22
View File
@@ -5,8 +5,7 @@
[quo.components.markdown.text :as text]
[quo.foundations.colors :as colors]
[quo.theme :as theme]
[react-native.core :as rn]
[reagent.core :as reagent]))
[react-native.core :as rn]))
;;; helpers
(def themes
@@ -137,24 +136,25 @@
style
on-press
warning-label]}]
(let [body-height (reagent/atom nil)]
(fn []
[rn/view
{:on-layout #(reset! body-height (oget % "nativeEvent.layout.height"))
:overflow :hidden
:flex 1}
[hborder {:type :top}]
[hborder {:type :bottom}]
[rn/view
(merge {:width "100%"
:background-color (get-color :background)
:flex-direction :row
:padding 20
:padding-left 31
:margin-vertical 4}
style)
(let [[body-height set-body-height] (rn/use-state nil)
on-layout (rn/use-callback #(set-body-height
(oget % "nativeEvent.layout.height")))]
[rn/view
{:on-layout on-layout
:overflow :hidden
:flex 1}
[hborder {:type :top}]
[hborder {:type :bottom}]
[rn/view
(merge {:width "100%"
:background-color (get-color :background)
:flex-direction :row
:padding 20
:padding-left 31
:margin-vertical 4}
style)
[timeline]
[body timestamp-far timestamp-near on-info-button-pressed on-press warning-label]]
[vborder :left body-height]
[vborder :right body-height]])))
[timeline]
[body timestamp-far timestamp-near on-info-button-pressed on-press warning-label]]
[vborder :left body-height]
[vborder :right body-height]]))
@@ -4,50 +4,48 @@
[quo.components.markdown.text :as text]
[quo.components.numbered-keyboard.keyboard-key.style :as style]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]))
[react-native.core :as rn]))
(defn- label->accessibility-label
[label]
(let [label-name (if (keyword? label) (name label) label)]
(keyword (str "keyboard-key-" label-name))))
(defn- view-internal
[]
(let [pressed? (reagent/atom false)]
(fn [{:keys [disabled? theme blur? on-press on-long-press type]} label]
(let [label-color (style/get-label-color disabled? theme blur?)
background-color (style/toggle-background-color @pressed? blur? theme)]
[rn/pressable
{:accessibility-label (label->accessibility-label label)
:disabled (or disabled? (not label))
:on-press (fn []
(when on-press
(on-press label)))
:on-long-press (fn []
(when (fn? on-long-press)
(on-long-press label)))
:allow-multiple-presses? true
:on-press-in #(reset! pressed? true)
:on-press-out #(reset! pressed? false)
:hit-slop {:top 8 :bottom 8 :left 25 :right 25}
:style (style/container background-color)}
(case type
:key [icons/icon
label
{:color label-color
:accessibility-label :icon-label}]
:digit [text/text
{:accessibility-label :text-label
:weight :regular
:size :heading-1
:style {:color label-color}}
label]
:derivation-path [icons/icon
:i/derivation-path
{:color label-color
:size 32
:accessibility-label :derivation-path-label}]
nil)]))))
(def view (quo.theme/with-theme view-internal))
(defn view
[{:keys [disabled? blur? on-press on-long-press type]} label]
(let [theme (quo.theme/use-theme-value)
[pressed? set-pressed?] (rn/use-state false)
on-press (rn/use-callback #(when on-press (on-press label)) [label])
on-long-press (rn/use-callback #(when (fn? on-long-press) (on-long-press label))
[label])
on-press-in (rn/use-callback #(set-pressed? true))
on-press-out (rn/use-callback #(set-pressed? false))
label-color (style/get-label-color disabled? theme blur?)
background-color (style/toggle-background-color pressed? blur? theme)]
[rn/pressable
{:accessibility-label (label->accessibility-label label)
:disabled (or disabled? (not label))
:on-press on-press
:on-long-press on-long-press
:allow-multiple-presses? true
:on-press-in on-press-in
:on-press-out on-press-out
:hit-slop {:top 8 :bottom 8 :left 25 :right 25}
:style (style/container background-color)}
(case type
:key [icons/icon
label
{:color label-color
:accessibility-label :icon-label}]
:digit [text/text
{:accessibility-label :text-label
:weight :regular
:size :heading-1
:style {:color label-color}}
label]
:derivation-path [icons/icon
:i/derivation-path
{:color label-color
:size 32
:accessibility-label :derivation-path-label}]
nil)]))
@@ -3,48 +3,33 @@
[quo.components.avatars.user-avatar.view :as user-avatar]
[quo.components.markdown.text :as text]
[quo.components.profile.select-profile.style :as style]
[react-native.core :as rn]
[reagent.core :as reagent]))
(defn- on-change-handler
[selected? on-change]
(swap! selected? not)
(when on-change (on-change @selected?)))
[react-native.core :as rn]))
(defn view
"Options
- `default-selected?` The default selected state. Use when the component is not controlled.
- `selected?` Selected state. Use when the component is controlled.
- `name` Full name
- `profile-picture` Profile picture
- `customization-color` Customization color
- `(on-change selected?)` On change event handler
"
[{:keys [default-selected?]}]
(let [internal-selected? (reagent/atom (or default-selected? false))]
(fn [{:keys [selected?
profile-picture
name
customization-color
on-change]
:or {customization-color :turquoise}}]
(when (and (not (nil? selected?)) (not= @internal-selected? selected?))
(reset! internal-selected? selected?))
[rn/touchable-opacity
{:style (style/container customization-color @internal-selected?)
:on-press #(on-change-handler internal-selected? on-change)
:active-opacity 1
:accessibility-label :select-profile}
[rn/view {:style style/header}
[user-avatar/user-avatar
{:full-name name
:status-indicator? false
:size :medium
:profile-picture profile-picture}]
[rn/view {:style (style/select-radio @internal-selected?)}
(when @internal-selected? [rn/view {:style style/select-radio-inner}])]]
[text/text
{:size :heading-2
:weight :semi-bold
:style style/profile-name}
name]])))
[{:keys [selected? profile-picture name customization-color on-change]
:or {customization-color :turquoise}}]
[rn/touchable-opacity
{:style (style/container customization-color selected?)
:on-press #(when on-change (on-change (not selected?)))
:accessibility-label :select-profile}
[rn/view {:style style/header}
[user-avatar/user-avatar
{:full-name name
:status-indicator? false
:size :medium
:profile-picture profile-picture}]
[rn/view {:style (style/select-radio selected?)}
(when selected?
[rn/view {:style style/select-radio-inner}])]]
[text/text
{:size :heading-2
:weight :semi-bold
:style style/profile-name}
name]])
@@ -1,7 +1,6 @@
(ns quo.components.profile.showcase-nav.component-spec
(:require
[quo.components.profile.showcase-nav.view :as view]
[reagent.core :as reagent]
[test-helpers.component :as h]))
(def nav-data
@@ -35,7 +34,7 @@
(.toHaveBeenCalled))))
(h/test "active id updated"
(let [active-id (reagent/atom :recent)]
(let [active-id (atom :recent)]
(h/render [view/view
{:data nav-data
:on-press #(reset! active-id %)}])
@@ -4,10 +4,10 @@
[quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors]
[react-native.core :refer [use-effect]]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]))
(defn f-delete-button
(defn delete-button
[recording? ready-to-delete? reviewing-audio? force-show-controls?]
(let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0))
translate-x (reanimated/use-shared-value (if force-show-controls? 35 20))
@@ -17,62 +17,68 @@
connector-height (reanimated/use-shared-value 12)
border-radius-first-half (reanimated/use-shared-value 8)
border-radius-second-half (reanimated/use-shared-value 8)
start-x-animation (fn []
(helpers/animate-linear-with-delay translate-x 12 50 133.33)
(helpers/animate-easing-with-delay connector-opacity 1 0 93.33)
(helpers/animate-easing-with-delay connector-width 56 83.33 80)
(helpers/animate-easing-with-delay connector-height 56 83.33 80)
(helpers/animate-easing-with-delay border-radius-first-half
28
83.33
80)
(helpers/animate-easing-with-delay border-radius-second-half
28
83.33
80))
reset-x-animation (fn []
(helpers/animate-linear translate-x 0 100)
(helpers/set-value connector-opacity 0)
(helpers/set-value connector-width 24)
(helpers/set-value connector-height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))
fade-in-animation (fn []
(helpers/animate-linear translate-x 0 200)
(helpers/animate-linear opacity 1 200))
fade-out-animation (fn []
(helpers/animate-linear
translate-x
(if @reviewing-audio? 35 20)
200)
(if @reviewing-audio?
(helpers/animate-linear scale 0.75 200)
(helpers/animate-linear opacity 0 200))
(helpers/set-value connector-opacity 0)
(helpers/set-value connector-width 24)
(helpers/set-value connector-height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))
fade-out-reset-animation (fn []
(helpers/animate-linear opacity 0 200)
(helpers/animate-linear-with-delay translate-x 20 0 200)
(helpers/animate-linear-with-delay scale 1 0 200))]
(use-effect (fn []
(if @recording?
(fade-in-animation)
(fade-out-animation)))
[@recording?])
(use-effect (fn []
(when-not @reviewing-audio?
(fade-out-reset-animation)))
[@reviewing-audio?])
(use-effect (fn []
(cond
@ready-to-delete?
(start-x-animation)
@recording?
(reset-x-animation)))
[@ready-to-delete?])
start-x-animation (rn/use-callback
(fn []
(helpers/animate-linear-with-delay translate-x 12 50 133.33)
(helpers/animate-easing-with-delay connector-opacity 1 0 93.33)
(helpers/animate-easing-with-delay connector-width 56 83.33 80)
(helpers/animate-easing-with-delay connector-height 56 83.33 80)
(helpers/animate-easing-with-delay border-radius-first-half
28
83.33
80)
(helpers/animate-easing-with-delay border-radius-second-half
28
83.33
80)))
reset-x-animation (rn/use-callback
(fn []
(helpers/animate-linear translate-x 0 100)
(helpers/set-value connector-opacity 0)
(helpers/set-value connector-width 24)
(helpers/set-value connector-height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16)))
fade-in-animation (rn/use-callback
(fn []
(helpers/animate-linear translate-x 0 200)
(helpers/animate-linear opacity 1 200)))
fade-out-animation (rn/use-callback
(fn []
(helpers/animate-linear
translate-x
(if reviewing-audio? 35 20)
200)
(if reviewing-audio?
(helpers/animate-linear scale 0.75 200)
(helpers/animate-linear opacity 0 200))
(helpers/set-value connector-opacity 0)
(helpers/set-value connector-width 24)
(helpers/set-value connector-height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))
[reviewing-audio?])
fade-out-reset-animation (rn/use-callback
(fn []
(helpers/animate-linear opacity 0 200)
(helpers/animate-linear-with-delay translate-x 20 0 200)
(helpers/animate-linear-with-delay scale 1 0 200)))]
(rn/use-effect (fn []
(if recording?
(fade-in-animation)
(fade-out-animation)))
[recording?])
(rn/use-effect (fn []
(when-not reviewing-audio?
(fade-out-reset-animation)))
[reviewing-audio?])
(rn/use-effect (fn []
(cond
ready-to-delete?
(start-x-animation)
recording?
(reset-x-animation)))
[ready-to-delete?])
[:<>
[reanimated/view {:style (style/delete-button-container opacity)}
[reanimated/view
@@ -5,10 +5,10 @@
[quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :refer [use-effect]]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]))
(defn f-lock-button
(defn lock-button
[recording? ready-to-lock? locked?]
(let [theme (quo.theme/use-theme-value)
translate-x-y (reanimated/use-shared-value 20)
@@ -18,54 +18,58 @@
height (reanimated/use-shared-value 12)
border-radius-first-half (reanimated/use-shared-value 8)
border-radius-second-half (reanimated/use-shared-value 8)
start-x-y-animation (fn []
(helpers/animate-linear-with-delay translate-x-y 8 50 116.66)
(helpers/animate-easing-with-delay connector-opacity 1 0 80)
(helpers/animate-easing-with-delay width 56 83.33 63.33)
(helpers/animate-easing-with-delay height 56 83.33 63.33)
(helpers/animate-easing-with-delay border-radius-first-half
28
83.33
63.33)
(helpers/animate-easing-with-delay border-radius-second-half
28
83.33
63.33))
reset-x-y-animation (fn []
(helpers/animate-linear translate-x-y 0 100)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))
fade-in-animation (fn []
(helpers/animate-linear translate-x-y 0 220)
(helpers/animate-linear opacity 1 220))
fade-out-animation (fn []
(helpers/animate-linear translate-x-y 20 200)
(helpers/animate-linear opacity 0 200)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))]
(use-effect (fn []
(if @recording?
(fade-in-animation)
(fade-out-animation)))
[@recording?])
(use-effect (fn []
(cond
@ready-to-lock?
(start-x-y-animation)
(and @recording? (not @locked?))
(reset-x-y-animation)))
[@ready-to-lock?])
(use-effect (fn []
(if @locked?
(fade-out-animation)
(reset-x-y-animation)))
[@locked?])
start-x-y-animation (rn/use-callback
(fn []
(helpers/animate-linear-with-delay translate-x-y 8 50 116.66)
(helpers/animate-easing-with-delay connector-opacity 1 0 80)
(helpers/animate-easing-with-delay width 56 83.33 63.33)
(helpers/animate-easing-with-delay height 56 83.33 63.33)
(helpers/animate-easing-with-delay border-radius-first-half
28
83.33
63.33)
(helpers/animate-easing-with-delay border-radius-second-half
28
83.33
63.33)))
reset-x-y-animation (rn/use-callback
(fn []
(helpers/animate-linear translate-x-y 0 100)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16)))
fade-in-animation (rn/use-callback
(fn []
(helpers/animate-linear translate-x-y 0 220)
(helpers/animate-linear opacity 1 220)))
fade-out-animation (rn/use-callback
(fn []
(helpers/animate-linear translate-x-y 20 200)
(helpers/animate-linear opacity 0 200)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16)))]
(rn/use-effect (fn []
(if recording?
(fade-in-animation)
(fade-out-animation)))
[recording?])
(rn/use-effect (fn []
(cond
ready-to-lock?
(start-x-y-animation)
(and recording? (not locked?))
(reset-x-y-animation)))
[ready-to-lock?])
(rn/use-effect (fn []
(if locked?
(fade-out-animation)
(reset-x-y-animation)))
[locked?])
[:<>
[reanimated/view {:style (style/lock-button-container opacity)}
[reanimated/view
@@ -77,6 +81,6 @@
[reanimated/view
{:style (style/lock-button translate-x-y opacity)
:pointer-events :none}
[icons/icon (if @ready-to-lock? :i/locked :i/unlocked)
[icons/icon (if ready-to-lock? :i/locked :i/unlocked)
{:color (colors/theme-colors colors/black colors/white theme)
:size 20}]]]))
@@ -3,19 +3,17 @@
[quo.components.buttons.button.view :as button]
[quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style]
[react-native.core :as rn :refer [use-effect]]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]))
(defn f-record-button
(defn record-button
[recording? reviewing-audio?]
(let [opacity (reanimated/use-shared-value 1)
show-animation #(helpers/set-value opacity 1)
hide-animation #(helpers/set-value opacity 0)]
(use-effect (fn []
(if (or @recording? @reviewing-audio?)
(hide-animation)
(show-animation)))
[@recording? @reviewing-audio?])
(let [opacity (reanimated/use-shared-value 1)]
(rn/use-effect (fn []
(if (or recording? reviewing-audio?)
(helpers/set-value opacity 0)
(helpers/set-value opacity 1)))
[recording? reviewing-audio?])
[reanimated/view {:style (style/record-button-container opacity)}
[button/button
{:type :outline
@@ -5,8 +5,9 @@
[quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors]
[quo.theme]
[react-native.audio-toolkit :as audio]
[react-native.core :as rn :refer [use-effect]]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[taoensso.timbre :as log]
@@ -28,13 +29,16 @@
(reagent/as-element
[reanimated/view {:style (style/animated-circle scale opacity color)}]))))))
(defn f-record-button-big
[{:keys [recording? ready-to-send? ready-to-lock? ready-to-delete? record-button-is-animating?
record-button-at-initial-position? locked? reviewing-audio? recording-length-ms
clear-timeout touch-active? recorder-ref reload-recorder-fn idle? on-send on-cancel theme]}]
(let [scale (reanimated/use-shared-value 1)
(defn record-button-big
[{:keys [recording? set-recording ready-to-send? set-ready-to-send ready-to-lock? set-ready-to-lock
ready-to-delete? set-ready-to-delete record-button-is-animating?
record-button-at-initial-position? locked? set-locked reviewing-audio? recording-length-ms
set-recording-length-ms
clear-timeout touch-active? recorder-ref reload-recorder-fn idle? on-send on-cancel]}]
(let [theme (quo.theme/use-theme-value)
scale (reanimated/use-shared-value 1)
opacity (reanimated/use-shared-value 0)
opacity-from (if @ready-to-lock? opacity-from-lock opacity-from-default)
opacity-from (if ready-to-lock? opacity-from-lock opacity-from-default)
animations (map
(fn [index]
(let [ring-scale (worklets.record-audio/ring-scale scale
@@ -46,141 +50,155 @@
[opacity-from 0])}))
(range 0 5))
rings-color (cond
@ready-to-lock? (colors/theme-colors colors/neutral-80-opa-5-opaque
colors/neutral-80
theme)
@ready-to-delete? colors/danger-50
:else colors/primary-50)
ready-to-lock? (colors/theme-colors colors/neutral-80-opa-5-opaque
colors/neutral-80
theme)
ready-to-delete? colors/danger-50
:else colors/primary-50)
translate-y (reanimated/use-shared-value 0)
translate-x (reanimated/use-shared-value 0)
button-color colors/primary-50
icon-color (if (and (not (= :dark theme)) @ready-to-lock?) colors/black colors/white)
icon-color (if (and (not (= :dark theme)) ready-to-lock?) colors/black colors/white)
icon-opacity (reanimated/use-shared-value 1)
red-overlay-opacity (reanimated/use-shared-value 0)
gray-overlay-opacity (reanimated/use-shared-value 0)
complete-animation
(fn []
(cond
(and @ready-to-lock? (not @record-button-is-animating?))
(do
(reset! locked? true)
(reset! ready-to-lock? false))
(and (not @locked?) (not @reviewing-audio?))
(audio/stop-recording
@recorder-ref
(fn []
(cond
@ready-to-send?
(when on-send
(on-send {:file-path (audio/get-recorder-file-path @recorder-ref)
:duration @recording-length-ms}))
@ready-to-delete?
(when on-cancel
(on-cancel)))
(reload-recorder-fn)
(reset! recording? false)
(reset! ready-to-send? false)
(reset! ready-to-delete? false)
(reset! ready-to-lock? false)
(reset! idle? true)
(js/setTimeout #(reset! idle? false) 1000)
(reset! recording-length-ms 0)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))))
start-animation (fn []
(helpers/set-value opacity 1)
(helpers/animate-linear scale 2.6 signal-anim-duration)
;; TODO: Research if we can implement this with withSequence method
;; from Reanimated 2
;; GitHub issue [#14561]:
;; https://github.com/status-im/status-mobile/issues/14561
(reset! clear-timeout
(js/setTimeout
(rn/use-callback
(fn []
(cond
(and ready-to-lock? (not @record-button-is-animating?))
(do
(set-locked true)
(set-ready-to-lock false))
(and (not locked?) (not reviewing-audio?))
(audio/stop-recording
@recorder-ref
(fn []
(cond
ready-to-send?
(when on-send
(on-send {:file-path (audio/get-recorder-file-path @recorder-ref)
:duration recording-length-ms}))
ready-to-delete?
(when on-cancel
(on-cancel)))
(reload-recorder-fn)
(set-recording false)
(set-ready-to-send false)
(set-ready-to-delete false)
(set-ready-to-lock false)
(reset! idle? true)
(js/setTimeout #(reset! idle? false) 1000)
(set-recording-length-ms 0)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))))
[ready-to-lock? locked? reviewing-audio? ready-to-send? recording-length-ms ready-to-delete?
idle?])
start-animation (rn/use-callback
(fn []
(helpers/set-value opacity 1)
(helpers/animate-linear scale 2.6 signal-anim-duration)
;; TODO: Research if we can implement this with withSequence method
;; from Reanimated 2
;; GitHub issue [#14561]:
;; https://github.com/status-im/status-mobile/issues/14561
(reset! clear-timeout
(js/setTimeout
(fn []
(helpers/set-value scale scale-to-each)
(helpers/animate-linear-with-delay-loop scale
scale-to-total
signal-anim-duration-2
0))
signal-anim-duration))))
stop-animation (rn/use-callback
(fn []
(helpers/set-value opacity 0)
(reanimated/cancel-animation scale)
(helpers/set-value scale 1)
(when @clear-timeout (js/clearTimeout @clear-timeout))))
start-y-animation (rn/use-callback
(fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-y -64 250)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
[complete-animation])
reset-y-animation (rn/use-callback
(fn []
(helpers/animate-easing translate-y 0 300)
(helpers/animate-linear icon-opacity 1 500)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500)))
start-x-animation (rn/use-callback
(fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-x -64 250)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66)
(helpers/animate-linear red-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
[complete-animation])
reset-x-animation (rn/use-callback
(fn []
(helpers/animate-easing translate-x 0 300)
(helpers/animate-linear icon-opacity 1 500)
(helpers/animate-linear red-overlay-opacity 0 100)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500)))
start-x-y-animation (rn/use-callback
(fn []
(helpers/set-value scale scale-to-each)
(helpers/animate-linear-with-delay-loop scale
scale-to-total
signal-anim-duration-2
0))
signal-anim-duration)))
stop-animation (fn []
(helpers/set-value opacity 0)
(reanimated/cancel-animation scale)
(helpers/set-value scale 1)
(when @clear-timeout (js/clearTimeout @clear-timeout)))
start-y-animation (fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-y -64 250)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
reset-y-animation (fn []
(helpers/animate-easing translate-y 0 300)
(helpers/animate-linear icon-opacity 1 500)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500))
start-x-animation (fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-x -64 250)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66)
(helpers/animate-linear red-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
reset-x-animation (fn []
(helpers/animate-easing translate-x 0 300)
(helpers/animate-linear icon-opacity 1 500)
(helpers/animate-linear red-overlay-opacity 0 100)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500))
start-x-y-animation (fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-y -44 200)
(helpers/animate-easing translate-x -44 200)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 33.33)
(helpers/animate-linear gray-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
200))
reset-x-y-animation (fn []
(helpers/animate-easing translate-y 0 300)
(helpers/animate-easing translate-x 0 300)
(helpers/animate-linear icon-opacity 1 500)
(helpers/animate-linear gray-overlay-opacity 0 800)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
800))]
(use-effect (fn []
(cond
@recording?
(start-animation)
(not @ready-to-lock?)
(stop-animation)))
[@recording?])
(use-effect (fn []
(if @ready-to-lock?
(start-x-y-animation)
(reset-x-y-animation)))
[@ready-to-lock?])
(use-effect (fn []
(if @ready-to-send?
(start-y-animation)
(reset-y-animation)))
[@ready-to-send?])
(use-effect (fn []
(if @ready-to-delete?
(start-x-animation)
(reset-x-animation)))
[@ready-to-delete?])
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-y -44 200)
(helpers/animate-easing translate-x -44 200)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 33.33)
(helpers/animate-linear gray-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
200))
[complete-animation])
reset-x-y-animation (rn/use-callback
(fn []
(helpers/animate-easing translate-y 0 300)
(helpers/animate-easing translate-x 0 300)
(helpers/animate-linear icon-opacity 1 500)
(helpers/animate-linear gray-overlay-opacity 0 800)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
800)))]
(rn/use-effect (fn []
(cond
recording?
(start-animation)
(not ready-to-lock?)
(stop-animation)))
[recording?])
(rn/use-effect (fn []
(if ready-to-lock?
(start-x-y-animation)
(reset-x-y-animation)))
[ready-to-lock?])
(rn/use-effect (fn []
(if ready-to-send?
(start-y-animation)
(reset-y-animation)))
[ready-to-send?])
(rn/use-effect (fn []
(if ready-to-delete?
(start-x-animation)
(reset-x-animation)))
[ready-to-delete?])
[reanimated/view
{:style (style/record-button-big-container translate-x translate-y opacity)
:pointer-events :none}
@@ -197,6 +215,6 @@
[reanimated/view {:style (style/record-button-big-red-overlay red-overlay-opacity)}]
[reanimated/view {:style (style/record-button-big-gray-overlay gray-overlay-opacity)}]
[reanimated/view {:style (style/record-button-big-icon-container icon-opacity)}
(if @locked?
(if locked?
[rn/view {:style style/stop-icon}]
[icons/icon :i/audio {:color icon-color}])]]]))
@@ -4,10 +4,10 @@
[quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors]
[react-native.core :refer [use-effect]]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]))
(defn f-send-button
(defn send-button
[recording? ready-to-send? reviewing-audio? force-show-controls?]
(let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0))
translate-y (reanimated/use-shared-value (if force-show-controls? 76 20))
@@ -16,65 +16,71 @@
height (reanimated/use-shared-value 24)
border-radius-first-half (reanimated/use-shared-value 16)
border-radius-second-half (reanimated/use-shared-value 8)
start-y-animation (fn []
(helpers/animate-linear-with-delay translate-y 12 50 133.33)
(helpers/animate-easing-with-delay connector-opacity 1 0 93.33)
(helpers/animate-easing-with-delay width 56 83.33 80)
(helpers/animate-easing-with-delay height 56 83.33 80)
(helpers/animate-easing-with-delay border-radius-first-half
28
83.33
80)
(helpers/animate-easing-with-delay border-radius-second-half
28
83.33
80))
reset-y-animation (fn []
(helpers/animate-linear translate-y 0 100)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 12)
(helpers/set-value height 24)
(helpers/set-value border-radius-first-half 16)
(helpers/set-value border-radius-second-half 8))
fade-in-animation (fn []
(helpers/animate-linear translate-y 0 200)
(helpers/animate-linear opacity 1 200))
fade-out-animation (fn []
(when-not force-show-controls?
(helpers/animate-linear
translate-y
(if @reviewing-audio? 76 20)
200))
(when-not @reviewing-audio?
(helpers/animate-linear opacity 0 200))
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))
fade-out-reset-animation (fn []
(helpers/animate-linear opacity 0 200)
(helpers/animate-linear-with-delay translate-y 20 0 200)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))]
(use-effect (fn []
(if @recording?
(fade-in-animation)
(fade-out-animation)))
[@recording?])
(use-effect (fn []
(when-not @reviewing-audio?
(fade-out-reset-animation)))
[@reviewing-audio?])
(use-effect (fn []
(cond
@ready-to-send?
(start-y-animation)
@recording? (reset-y-animation)))
[@ready-to-send?])
start-y-animation (rn/use-callback
(fn []
(helpers/animate-linear-with-delay translate-y 12 50 133.33)
(helpers/animate-easing-with-delay connector-opacity 1 0 93.33)
(helpers/animate-easing-with-delay width 56 83.33 80)
(helpers/animate-easing-with-delay height 56 83.33 80)
(helpers/animate-easing-with-delay border-radius-first-half
28
83.33
80)
(helpers/animate-easing-with-delay border-radius-second-half
28
83.33
80)))
reset-y-animation (rn/use-callback
(fn []
(helpers/animate-linear translate-y 0 100)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 12)
(helpers/set-value height 24)
(helpers/set-value border-radius-first-half 16)
(helpers/set-value border-radius-second-half 8)))
fade-in-animation (rn/use-callback
(fn []
(helpers/animate-linear translate-y 0 200)
(helpers/animate-linear opacity 1 200)))
fade-out-animation (rn/use-callback
(fn []
(when-not force-show-controls?
(helpers/animate-linear
translate-y
(if reviewing-audio? 76 20)
200))
(when-not reviewing-audio?
(helpers/animate-linear opacity 0 200))
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16))
[reviewing-audio? force-show-controls?])
fade-out-reset-animation (rn/use-callback
(fn []
(helpers/animate-linear opacity 0 200)
(helpers/animate-linear-with-delay translate-y 20 0 200)
(helpers/set-value connector-opacity 0)
(helpers/set-value width 24)
(helpers/set-value height 12)
(helpers/set-value border-radius-first-half 8)
(helpers/set-value border-radius-second-half 16)))]
(rn/use-effect (fn []
(if recording?
(fade-in-animation)
(fade-out-animation)))
[recording?])
(rn/use-effect (fn []
(when-not reviewing-audio?
(fade-out-reset-animation)))
[reviewing-audio?])
(rn/use-effect (fn []
(cond
ready-to-send?
(start-y-animation)
recording? (reset-y-animation)))
[ready-to-send?])
[:<>
[reanimated/view {:style (style/send-button-container opacity)}
[reanimated/view
@@ -2,7 +2,6 @@
(:require
[quo.components.record-audio.record-audio.view :as record-audio]
[react-native.audio-toolkit :as audio]
[reagent.core :as reagent]
[test-helpers.component :as h]
[utils.datetime :as datetime]))
@@ -11,8 +10,10 @@
(h/test "renders record-audio"
(h/render [record-audio/record-audio])
(-> (h/expect (h/get-by-test-id "record-audio"))
(.toBeTruthy)))
(-> (h/wait-for #(h/get-by-test-id "record-audio"))
(.then (fn []
(-> (h/expect (h/get-by-test-id "record-audio"))
(.toBeTruthy))))))
(h/test "record-audio on-start-recording works"
(let [event (js/jest.fn)]
@@ -31,7 +32,7 @@
(h/test "record-audio on-reviewing-audio works"
(let [event (js/jest.fn)
on-meter (reagent/atom nil)]
on-meter (atom nil)]
(h/render [record-audio/record-audio
{:on-reviewing-audio event
:record-audio-permission-granted true}])
@@ -39,7 +40,7 @@
(reset! on-meter on-meter-fn))
audio/start-recording (fn [_ on-start _]
(on-start)
(js/setInterval #(@on-meter) 100))
(js/setInterval @on-meter 100))
audio/get-recorder-file-path (fn [] "file-path")]
(h/fire-event
:on-start-should-set-responder
@@ -48,6 +49,7 @@
:locationY 70
:timestamp 0
:identifier 0}})
(with-redefs [datetime/timestamp (fn [] (+ (.now js/Date) 1000))]
(h/advance-timers-by-time 100)
(h/fire-event
@@ -63,7 +65,7 @@
(h/test "record-audio on-send works after reviewing audio"
(let [event (js/jest.fn)
on-meter (reagent/atom nil)]
on-meter (atom nil)]
(h/render [record-audio/record-audio
{:on-send event
:record-audio-permission-granted true}])
@@ -114,14 +116,15 @@
(h/test "record-audio on-send works after sliding to the send button"
(let [event (js/jest.fn)
on-meter (reagent/atom nil)
on-meter (atom nil)
last-now-ms (atom nil)
duration-ms (atom nil)]
(h/render [record-audio/record-audio
{:on-send event
:record-audio-permission-granted true}])
(with-redefs [audio/new-recorder (fn [_ on-meter-fn _]
(reset! on-meter on-meter-fn))
(reset! on-meter on-meter-fn)
#js {:destroy #()})
audio/start-recording (fn [_ on-start _]
(on-start)
(js/setInterval #(@on-meter) 100))
@@ -170,15 +173,16 @@
(h/test "record-audio on-cancel works after reviewing audio"
(let [event (js/jest.fn)
on-meter (reagent/atom nil)]
on-meter (atom nil)]
(h/render [record-audio/record-audio
{:on-cancel event
:record-audio-permission-granted true}])
(with-redefs [audio/new-recorder (fn [_ on-meter-fn _]
(reset! on-meter on-meter-fn))
(reset! on-meter on-meter-fn)
#js {:destroy #()})
audio/start-recording (fn [_ on-start _]
(on-start)
(js/setInterval #(@on-meter) 100))]
(js/setInterval @on-meter 100))]
(h/fire-event
:on-start-should-set-responder
(h/get-by-test-id "record-audio")
@@ -195,6 +199,7 @@
:locationY 70
:timestamp 200
:identifier 0}})
(h/advance-timers-by-time 250)
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
@@ -202,13 +207,12 @@
:locationY 80
:timestamp 200
:identifier 0}})
(h/advance-timers-by-time 250)
(-> (js/expect event)
(.toHaveBeenCalledTimes 1))))))
(h/test "record-audio on-cancel works after sliding to the cancel button"
(let [event (js/jest.fn)
on-meter (reagent/atom nil)]
on-meter (atom nil)]
(h/render [record-audio/record-audio
{:on-cancel event
:record-audio-permission-granted true}])
@@ -0,0 +1,81 @@
(ns quo.components.record-audio.record-audio.constants
(:require [react-native.audio-toolkit :as audio]
[react-native.platform :as platform]
[utils.datetime :as datetime]))
(def min-audio-duration-ms 1000)
(def max-audio-duration-ms (if platform/ios? 120800 120500))
(def metering-interval 25)
(def base-filename "am")
(def default-format ".aac")
(def min-touch-duration 150)
(def record-button-area-big
{:width 56
:height 56
:x 64
:y 64})
(def record-button-area
{:width 48
:height 48
:x 68
:y 68})
(defn delete-button-area
[{:keys [active? reviewing-audio?]}]
{:width (cond
active? 72
reviewing-audio? 32
:else 82)
:height (if reviewing-audio? 32 56)
:x (cond
active? -16
reviewing-audio? 36
:else -32)
:y (cond
active? 64
reviewing-audio? 76
:else 70)})
(defn lock-button-area
[{:keys [active?]}]
{:width (if active? 72 100)
:height (if active? 72 102)
:x -32
:y -32})
(defn send-button-area
[{:keys [active? reviewing-audio?]}]
{:width (if reviewing-audio? 32 56)
:height (cond
active? 72
reviewing-audio? 47
:else 92)
:x (if reviewing-audio? 76 32)
:y (cond
active? -16
reviewing-audio? 76
:else -32)})
(defn touch-inside-area?
[{:keys [location-x location-y ignore-min-y? ignore-max-y? ignore-min-x? ignore-max-x?]}
{:keys [width height x y]}]
(let [max-x (+ x width)
max-y (+ y height)]
(and
(and
(or ignore-min-x? (>= location-x x))
(or ignore-max-x? (<= location-x max-x)))
(and
(or ignore-min-y? (>= location-y y))
(or ignore-max-y? (<= location-y max-y))))))
(def rec-options
(merge
audio/default-recorder-options
{:filename (str base-filename
(datetime/timestamp)
default-format)
:meteringInterval metering-interval}))
@@ -0,0 +1,345 @@
(ns quo.components.record-audio.record-audio.handlers
(:require [oops.core :as oops]
[quo.components.record-audio.record-audio.constants :as record-audio.constants]
[react-native.audio-toolkit :as audio]
[taoensso.timbre :as log]
[utils.datetime :as datetime]))
(def get-reload-player
(memoize
(fn
[[player-ref destroy-player set-playing-audio playing-timer set-audio-current-time-ms
set-seeking-audio]]
(fn [audio-file]
(when @player-ref
(destroy-player))
(reset! player-ref
(audio/new-player
(or audio-file (:filename record-audio.constants/rec-options))
{:autoDestroy false
:continuesToPlayInBackground false
:category audio/PLAYBACK}
(fn []
(set-playing-audio false)
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil)
(set-audio-current-time-ms 0)
(set-seeking-audio false)))))
(audio/prepare-player
@player-ref
#(log/debug "[record-audio] prepare player - success")
#(log/error "[record-audio] prepare player - error: " %))))))
(def get-recorder-on-meter
(memoize
(fn
[[recording-start-ms set-recording-length-ms reached-max-duration? locked? set-locked
set-reviewing-audio
idle? set-recording
set-ready-to-lock set-ready-to-send set-ready-to-delete recorder-ref output-file reload-player
on-reviewing-audio]]
(fn []
(when @recording-start-ms
(let [now-ms (datetime/timestamp)
recording-duration (- now-ms @recording-start-ms)]
(set-recording-length-ms recording-duration)
(when (>= recording-duration record-audio.constants/max-audio-duration-ms)
(reset! reached-max-duration? (not locked?))
(set-reviewing-audio true)
(reset! idle? false)
(set-locked false)
(set-recording false)
(set-ready-to-lock false)
(set-ready-to-send false)
(set-ready-to-delete false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path @recorder-ref))
(reload-player nil)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(set-recording-length-ms 0)
(reset! recording-start-ms nil)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path @recorder-ref))))
(log/debug "[record-audio] new recorder - on meter")))))))
(def get-reload-recorder
(memoize
(fn
[[recorder-ref recorder-on-meter]]
(fn []
(when @recorder-ref
(audio/destroy-recorder @recorder-ref)
(reset! recorder-ref nil))
(reset! recorder-ref (audio/new-recorder
record-audio.constants/rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended")))))))
(def get-on-start-should-set-responder
(memoize
(fn
[[locked? idle? disabled? recorder-on-meter touch-timestamp touch-identifier
reviewing-audio? record-audio-permission-granted set-recording set-playing-audio output-file
recorder-ref recording-start-ms set-audio-current-time-ms on-start-recording
on-request-record-audio-permission touch-active?]]
(fn [e]
(when-not (or locked? @idle? (nil? e) @disabled?)
(let [pressed-record-button? (record-audio.constants/touch-inside-area?
{:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-audio.constants/record-button-area)
new-recorder (audio/new-recorder
record-audio.constants/rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended"))]
(reset! touch-timestamp (oops/oget e "nativeEvent.timestamp"))
(reset! touch-identifier (oops/oget e "nativeEvent.identifier"))
(when-not reviewing-audio?
(if record-audio-permission-granted
(do
(when (not @idle?)
(set-recording pressed-record-button?))
(when pressed-record-button?
(set-playing-audio false)
(reset! output-file nil)
(reset! recorder-ref new-recorder)
(audio/start-recording
new-recorder
(fn []
(reset! recording-start-ms (datetime/timestamp))
(set-audio-current-time-ms 0)
(log/debug "[record-audio] start recording - success"))
#(log/error "[record-audio] start recording - error: " %))
(when on-start-recording
(on-start-recording))))
(when on-request-record-audio-permission
(on-request-record-audio-permission))))
(when record-audio-permission-granted
(reset! touch-active? true))))
(and (not @idle?) (not @disabled?))))))
(def get-on-responder-move
(memoize
(fn
[[locked? ready-to-send? set-ready-to-send ready-to-delete? set-ready-to-delete ready-to-lock?
set-ready-to-lock touch-identifier
record-button-at-initial-position? recording?]]
(fn [^js e]
(when-not locked?
(let [location-x (oops/oget e "nativeEvent.locationX")
location-y (oops/oget e "nativeEvent.locationY")
page-x (oops/oget e "nativeEvent.pageX")
page-y (oops/oget e "nativeEvent.pageY")
identifier (oops/oget e "nativeEvent.identifier")
moved-to-send-button? (record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? true
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? true}
(record-audio.constants/send-button-area
{:active? ready-to-send?
:reviewing-audio? false}))
moved-to-delete-button? (record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? true
:ignore-min-x? true
:ignore-max-x? false}
(record-audio.constants/delete-button-area
{:active? ready-to-delete?
:reviewing-audio? false}))
moved-to-lock-button? (record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
(record-audio.constants/lock-button-area {:active?
ready-to-lock?}))
moved-to-record-button? (and
(record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-audio.constants/record-button-area-big)
(not= location-x page-x)
(not= location-y page-y))]
(when (= identifier @touch-identifier)
(cond
(and
(or
(and moved-to-record-button? ready-to-lock?)
(and (not locked?) moved-to-lock-button? @record-button-at-initial-position?))
(not ready-to-delete?)
(not ready-to-send?)
recording?)
(set-ready-to-lock moved-to-lock-button?)
(and
(or
(and moved-to-record-button? ready-to-delete?)
(and moved-to-delete-button? @record-button-at-initial-position?))
(not ready-to-lock?)
(not ready-to-send?)
recording?)
(set-ready-to-delete moved-to-delete-button?)
(and
(or
(and moved-to-record-button? ready-to-send?)
(and moved-to-send-button? @record-button-at-initial-position?))
(not ready-to-lock?)
(not ready-to-delete?)
recording?)
(set-ready-to-send moved-to-send-button?)))))))))
(def get-on-responder-release
(memoize
(fn
[[idle? reached-max-duration? touch-timestamp recording-length-ms set-recording-length-ms
reviewing-audio? set-reviewing-audio
set-audio-current-time-ms set-force-show-controls on-send output-file player-ref
destroy-player on-cancel record-button-is-animating? ready-to-lock? set-ready-to-lock
locked? set-locked on-lock ready-to-delete? set-ready-to-delete ready-to-send? set-ready-to-send
disabled? on-reviewing-audio recorder-ref set-recording reload-player recording-start-ms
touch-active?]]
(fn [^js e]
(when (and
(not @idle?)
(not @reached-max-duration?))
(let [touch-area {:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
on-record-button? (record-audio.constants/touch-inside-area?
touch-area
record-audio.constants/record-button-area-big)
on-send-button? (record-audio.constants/touch-inside-area?
touch-area
(record-audio.constants/send-button-area
{:active? false
:reviewing-audio? true}))
on-delete-button? (record-audio.constants/touch-inside-area?
touch-area
(record-audio.constants/delete-button-area
{:active? false
:reviewing-audio? true}))
release-touch-timestamp (oops/oget e "nativeEvent.timestamp")
touch-timestamp-diff (- release-touch-timestamp @touch-timestamp)
audio-length recording-length-ms]
(cond
(and reviewing-audio? on-send-button?)
(do
(set-reviewing-audio false)
(set-audio-current-time-ms 0)
(set-force-show-controls false)
(when on-send
(on-send {:file-path @output-file
:duration (min record-audio.constants/max-audio-duration-ms
(int (audio/get-player-duration @player-ref)))}))
(when @player-ref
(audio/stop-playing
@player-ref
(fn []
(destroy-player)
(log/debug "[record-audio] stop playing - success"))
#(log/error "[record-audio] stop playing - error: " %))))
(and reviewing-audio? on-delete-button?)
(do
(set-reviewing-audio false)
(set-audio-current-time-ms 0)
(set-force-show-controls false)
(destroy-player)
(when on-cancel
(on-cancel)))
(and ready-to-lock? (not @record-button-is-animating?))
(do
(set-locked true)
(set-ready-to-lock false)
(when on-lock
(on-lock)))
(and (not reviewing-audio?)
(or on-record-button?
(and (not ready-to-delete?)
(not ready-to-lock?)
(not ready-to-send?))))
(do
(reset! disabled? (<= touch-timestamp-diff record-audio.constants/min-touch-duration))
(js/setTimeout
(fn []
(if (>= recording-length-ms record-audio.constants/min-audio-duration-ms)
(do (set-reviewing-audio true)
(reset! idle? false)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path @recorder-ref))))
(do (when on-cancel
(on-cancel))
(reset! idle? true)))
(set-locked false)
(set-recording false)
(set-ready-to-lock false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path @recorder-ref))
(when (>= audio-length record-audio.constants/min-audio-duration-ms)
(reload-player nil))
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(set-recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false))
(if (> touch-timestamp-diff record-audio.constants/min-touch-duration) 0 250)))
(and (not locked?) (not reviewing-audio?) (not @record-button-is-animating?))
(do
(reset! disabled? (<= touch-timestamp-diff record-audio.constants/min-touch-duration))
(js/setTimeout
(fn []
(audio/stop-recording
@recorder-ref
(fn []
(cond
ready-to-send?
(when on-send
(on-send {:file-path (audio/get-recorder-file-path @recorder-ref)
:duration recording-length-ms}))
ready-to-delete?
(when on-cancel
(on-cancel)))
(set-recording false)
(set-ready-to-send false)
(set-ready-to-delete false)
(set-ready-to-lock false)
(reset! idle? true)
(js/setTimeout #(reset! idle? false) 1000)
(set-recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %)))
(if (> touch-timestamp-diff record-audio.constants/min-touch-duration) 0 250)))))
(reset! touch-active? false))
(when @reached-max-duration?
(reset! reached-max-duration? false))
(reset! touch-timestamp nil)))))
@@ -2,7 +2,6 @@
(:require
[clojure.string :as string]
[goog.string :as gstring]
[oops.core :as oops]
[quo.components.icon :as icons]
[quo.components.markdown.text :as text]
[quo.components.record-audio.record-audio.buttons.delete-button :as delete-button]
@@ -10,93 +9,23 @@
[quo.components.record-audio.record-audio.buttons.record-button :as record-button]
[quo.components.record-audio.record-audio.buttons.record-button-big :as record-button-big]
[quo.components.record-audio.record-audio.buttons.send-button :as send-button]
[quo.components.record-audio.record-audio.constants :as record-audio.constants]
[quo.components.record-audio.record-audio.handlers :as handlers]
[quo.components.record-audio.record-audio.style :as style]
[quo.components.record-audio.soundtrack.view :as soundtrack]
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.audio-toolkit :as audio]
[react-native.core :as rn]
[react-native.platform :as platform]
[reagent.core :as reagent]
[taoensso.timbre :as log]
[utils.datetime :as datetime]))
(def ^:private min-audio-duration-ms 1000)
(def ^:private max-audio-duration-ms (if platform/ios? 120800 120500))
(def ^:private metering-interval 25)
(def ^:private base-filename "am")
(def ^:private default-format ".aac")
(def min-touch-duration 150)
(def ^:private record-button-area-big
{:width 56
:height 56
:x 64
:y 64})
(def ^:private record-button-area
{:width 48
:height 48
:x 68
:y 68})
(defn- delete-button-area
[{:keys [active? reviewing-audio?]}]
{:width (cond
active? 72
reviewing-audio? 32
:else 82)
:height (if reviewing-audio? 32 56)
:x (cond
active? -16
reviewing-audio? 36
:else -32)
:y (cond
active? 64
reviewing-audio? 76
:else 70)})
(defn- lock-button-area
[{:keys [active?]}]
{:width (if active? 72 100)
:height (if active? 72 102)
:x -32
:y -32})
(defn- send-button-area
[{:keys [active? reviewing-audio?]}]
{:width (if reviewing-audio? 32 56)
:height (cond
active? 72
reviewing-audio? 47
:else 92)
:x (if reviewing-audio? 76 32)
:y (cond
active? -16
reviewing-audio? 76
:else -32)})
(defn touch-inside-area?
[{:keys [location-x location-y ignore-min-y? ignore-max-y? ignore-min-x? ignore-max-x?]}
{:keys [width height x y]}]
(let [max-x (+ x width)
max-y (+ y height)]
(and
(and
(or ignore-min-x? (>= location-x x))
(or ignore-max-x? (<= location-x max-x)))
(and
(or ignore-min-y? (>= location-y y))
(or ignore-max-y? (<= location-y max-y))))))
(defn- f-recording-bar
(defn- recording-bar
[recording-length-ms ready-to-delete?]
(let [fill-percentage (/ (* recording-length-ms 100) max-audio-duration-ms)]
(let [fill-percentage (/ (* recording-length-ms 100) record-audio.constants/max-audio-duration-ms)]
[rn/view {:style (style/recording-bar-container)}
[rn/view {:style (style/recording-bar fill-percentage ready-to-delete?)}]]))
(defn- f-time-counter
(defn- time-counter
[recording? recording-length-ms ready-to-delete? reviewing-audio? audio-current-time-ms]
(let [s (quot (if recording? recording-length-ms audio-current-time-ms) 1000)
time-str (gstring/format "%02d:%02d" (quot s 60) (mod s 60))]
@@ -111,10 +40,12 @@
{:style (style/timer-text)}))
time-str]]))
(defn- f-play-button
[playing-audio? player-ref playing-timer audio-current-time-ms seeking-audio? max-duration-ms]
(defn- play-button
[playing-audio? set-playing-audio player-ref playing-timer set-audio-current-time-ms seeking-audio?
set-seeking-audio
max-duration-ms]
(let [on-play (fn []
(reset! playing-audio? true)
(set-playing-audio true)
(reset! playing-timer
(js/setInterval
(fn []
@@ -122,24 +53,24 @@
player-state (audio/get-state @player-ref)
playing? (= player-state audio/PLAYING)]
(when (and playing?
(not @seeking-audio?)
(not seeking-audio?)
(> current-time 0)
(< current-time max-duration-ms))
(reset! audio-current-time-ms current-time))
(set-audio-current-time-ms current-time))
(when (>= current-time max-duration-ms)
(audio/stop-playing
@player-ref
(fn []
(reset! playing-audio? false)
(set-playing-audio false)
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil)
(reset! audio-current-time-ms 0)
(reset! seeking-audio? false)))
(set-audio-current-time-ms 0)
(set-seeking-audio false)))
#(log/error "[record-audio] stop player - error: " %)))))
100)))
on-pause (fn []
(reset! playing-audio? false)
(set-playing-audio false)
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil))
@@ -154,445 +85,186 @@
{:style (style/play-button)
:on-press on-press}
[icons/icon
(if @playing-audio? :i/pause :i/play)
(if playing-audio? :i/pause :i/play)
{:color (colors/theme-colors colors/neutral-100 colors/white)}]]))
(defn- record-audio-internal
[{:keys [on-init on-start-recording on-send on-cancel on-reviewing-audio
record-audio-permission-granted
on-request-record-audio-permission on-check-audio-permissions
audio-file on-lock max-duration-ms theme]}]
[:f>
;; TODO we need to refactor this, and use :f> with defined function, currenly state is reseted each
;; time parent component
;; is re-rendered
(fn []
(let [recording? (reagent/atom false)
locked? (reagent/atom false)
ready-to-send? (reagent/atom false)
ready-to-lock? (reagent/atom false)
ready-to-delete? (reagent/atom false)
reviewing-audio? (reagent/atom (some? audio-file))
playing-audio? (reagent/atom false)
recording-length-ms (reagent/atom 0)
audio-current-time-ms (reagent/atom 0)
seeking-audio? (reagent/atom false)
force-show-controls? (reagent/atom (some? audio-file))
recording-start-ms (atom (datetime/timestamp))
clear-timeout (atom nil)
record-button-at-initial-position? (atom true)
record-button-is-animating? (atom false)
idle? (atom false)
recorder-ref (atom nil)
player-ref (atom nil)
touch-active? (atom false)
playing-timer (atom nil)
output-file (atom audio-file)
reached-max-duration? (atom false)
touch-timestamp (atom nil)
touch-identifier (atom nil)
disabled? (atom false)
app-state-listener (atom nil)
rec-options
(merge
audio/default-recorder-options
{:filename (str base-filename (datetime/timestamp) default-format)
:meteringInterval metering-interval})
destroy-player
(fn []
(audio/destroy-player @player-ref)
(reset! player-ref nil))
reload-player
(fn [audio-file]
(when @player-ref
(destroy-player))
(reset! player-ref
(audio/new-player
(or audio-file (:filename rec-options))
{:autoDestroy false
:continuesToPlayInBackground false
:category audio/PLAYBACK}
(fn []
(reset! playing-audio? false)
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil)
(reset! audio-current-time-ms 0)
(reset! seeking-audio? false)))))
(audio/prepare-player
@player-ref
#(log/debug "[record-audio] prepare player - success")
#(log/error "[record-audio] prepare player - error: " %)))
destroy-recorder
(fn []
(audio/destroy-recorder @recorder-ref)
(reset! recorder-ref nil))
recorder-on-meter
(fn []
(when @recording-start-ms
(let [now-ms (datetime/timestamp)
recording-duration (- now-ms
@recording-start-ms)]
(reset! recording-length-ms recording-duration)
(when (>= recording-duration max-audio-duration-ms)
(reset! reached-max-duration? (not @locked?))
(reset! reviewing-audio? true)
(reset! idle? false)
(reset! locked? false)
(reset! recording? false)
(reset! ready-to-lock? false)
(reset! ready-to-send? false)
(reset! ready-to-delete? false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path
@recorder-ref))
(reload-player nil)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(reset! recording-length-ms 0)
(reset! recording-start-ms nil)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path
@recorder-ref))))
(log/debug "[record-audio] new recorder - on meter"))))
reload-recorder
(fn []
(when @recorder-ref
(destroy-recorder))
(reset! recorder-ref (audio/new-recorder
rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended"))))
reset-recorder
(fn []
(reset! recording? false)
(reset! reviewing-audio? false)
(reset! locked? false)
(reset! ready-to-send? false)
(reset! ready-to-lock? false)
(reset! ready-to-delete? false)
(reset! audio-current-time-ms 0)
(reset! recording-length-ms 0)
(reset! seeking-audio? false)
(reset! playing-audio? false)
(reset! touch-active? false)
(reset! reached-max-duration? false)
(reset! output-file nil)
(reset! idle? false)
(reset! record-button-is-animating? false)
(reset! record-button-at-initial-position? true)
(when @clear-timeout
(js/clearTimeout @clear-timeout)
(reset! clear-timeout nil))
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil))
(reload-recorder))
on-start-should-set-responder
(fn [^js e]
(when-not (or @locked? @idle? (nil? e) @disabled?)
(let [pressed-record-button? (touch-inside-area?
{:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-button-area)
new-recorder (audio/new-recorder
rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended"))]
(reset! touch-timestamp (oops/oget e "nativeEvent.timestamp"))
(reset! touch-identifier (oops/oget e "nativeEvent.identifier"))
(when-not @reviewing-audio?
(if record-audio-permission-granted
(do
(when (not @idle?)
(reset! recording? pressed-record-button?))
(when pressed-record-button?
(reset! playing-audio? false)
(reset! output-file nil)
(reset! recorder-ref new-recorder)
(audio/start-recording
new-recorder
(fn []
(reset! recording-start-ms (datetime/timestamp))
(reset! audio-current-time-ms 0)
(log/debug "[record-audio] start recording - success"))
#(log/error "[record-audio] start recording - error: " %))
(when on-start-recording
(on-start-recording))))
(when on-request-record-audio-permission
(on-request-record-audio-permission))))
(when record-audio-permission-granted
(reset! touch-active? true))))
(and (not @idle?) (not @disabled?)))
on-responder-move
(fn [^js e]
(when-not @locked?
(let [location-x (oops/oget e "nativeEvent.locationX")
location-y (oops/oget e "nativeEvent.locationY")
page-x (oops/oget e "nativeEvent.pageX")
page-y (oops/oget e "nativeEvent.pageY")
identifier (oops/oget e "nativeEvent.identifier")
moved-to-send-button? (touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? true
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? true}
(send-button-area
{:active? @ready-to-send?
:reviewing-audio? false}))
moved-to-delete-button? (touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? true
:ignore-min-x? true
:ignore-max-x? false}
(delete-button-area
{:active? @ready-to-delete?
:reviewing-audio? false}))
moved-to-lock-button? (touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
(lock-button-area {:active? @ready-to-lock?}))
moved-to-record-button? (and
(touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-button-area-big)
(not= location-x page-x)
(not= location-y page-y))]
(when (= identifier @touch-identifier)
(cond
(and
(or
(and moved-to-record-button? @ready-to-lock?)
(and (not @locked?) moved-to-lock-button? @record-button-at-initial-position?))
(not @ready-to-delete?)
(not @ready-to-send?)
@recording?)
(reset! ready-to-lock? moved-to-lock-button?)
(and
(or
(and moved-to-record-button? @ready-to-delete?)
(and moved-to-delete-button? @record-button-at-initial-position?))
(not @ready-to-lock?)
(not @ready-to-send?)
@recording?)
(reset! ready-to-delete? moved-to-delete-button?)
(and
(or
(and moved-to-record-button? @ready-to-send?)
(and moved-to-send-button? @record-button-at-initial-position?))
(not @ready-to-lock?)
(not @ready-to-delete?)
@recording?)
(reset! ready-to-send? moved-to-send-button?))))))
on-responder-release
(fn [^js e]
(when (and
(not @idle?)
(not @reached-max-duration?))
(let [touch-area {:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
on-record-button? (touch-inside-area?
touch-area
record-button-area-big)
on-send-button? (touch-inside-area?
touch-area
(send-button-area
{:active? false
:reviewing-audio? true}))
on-delete-button? (touch-inside-area?
touch-area
(delete-button-area
{:active? false
:reviewing-audio? true}))
release-touch-timestamp (oops/oget e "nativeEvent.timestamp")
touch-timestamp-diff (- release-touch-timestamp @touch-timestamp)
audio-length @recording-length-ms]
(cond
(and @reviewing-audio? on-send-button?)
(do
(reset! reviewing-audio? false)
(reset! audio-current-time-ms 0)
(reset! force-show-controls? false)
(when on-send
(on-send {:file-path @output-file
:duration (min max-audio-duration-ms
(int (audio/get-player-duration @player-ref)))}))
(when @player-ref
(audio/stop-playing
@player-ref
(fn []
(destroy-player)
(log/debug "[record-audio] stop playing - success"))
#(log/error "[record-audio] stop playing - error: " %))))
(and @reviewing-audio? on-delete-button?)
(do
(reset! reviewing-audio? false)
(reset! audio-current-time-ms 0)
(reset! force-show-controls? false)
(destroy-player)
(when on-cancel
(on-cancel)))
(and @ready-to-lock? (not @record-button-is-animating?))
(do
(reset! locked? true)
(reset! ready-to-lock? false)
(when on-lock
(on-lock)))
(and (not @reviewing-audio?)
(or on-record-button?
(and (not @ready-to-delete?)
(not @ready-to-lock?)
(not @ready-to-send?))))
(do
(reset! disabled? (<= touch-timestamp-diff min-touch-duration))
(js/setTimeout
(fn []
(if (>= @recording-length-ms min-audio-duration-ms)
(do (reset! reviewing-audio? true)
(reset! idle? false)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path @recorder-ref))))
(do (when on-cancel
(on-cancel))
(reset! idle? true)))
(reset! locked? false)
(reset! recording? false)
(reset! ready-to-lock? false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path @recorder-ref))
(when (>= audio-length min-audio-duration-ms)
(reload-player nil))
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(reset! recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false))
(if (> touch-timestamp-diff min-touch-duration) 0 250)))
(and (not @locked?) (not @reviewing-audio?) (not @record-button-is-animating?))
(do
(reset! disabled? (<= touch-timestamp-diff min-touch-duration))
(js/setTimeout
(fn []
(audio/stop-recording
@recorder-ref
(fn []
(cond
@ready-to-send?
(when on-send
(on-send {:file-path (audio/get-recorder-file-path @recorder-ref)
:duration @recording-length-ms}))
@ready-to-delete?
(when on-cancel
(on-cancel)))
(reset! recording? false)
(reset! ready-to-send? false)
(reset! ready-to-delete? false)
(reset! ready-to-lock? false)
(reset! idle? true)
(js/setTimeout #(reset! idle? false) 1000)
(reset! recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %)))
(if (> touch-timestamp-diff min-touch-duration) 0 250)))))
(reset! touch-active? false))
(when @reached-max-duration?
(reset! reached-max-duration? false))
(reset! touch-timestamp nil))]
(fn []
(rn/use-mount (fn []
(when on-check-audio-permissions
(on-check-audio-permissions))
(when on-init
(on-init reset-recorder))
(when audio-file
(let [filename (last (string/split audio-file "/"))]
(reload-player filename)))
(reset! app-state-listener
(.addEventListener rn/app-state
"change"
#(when (= % "background")
(reset! playing-audio? false))))
#(.remove @app-state-listener)))
[rn/view
{:style style/bar-container
:pointer-events :box-none}
(when @reviewing-audio?
[:<>
[:f> f-play-button playing-audio? player-ref playing-timer audio-current-time-ms
seeking-audio? max-duration-ms]
[:f> soundtrack/f-soundtrack
{:audio-current-time-ms audio-current-time-ms
:player-ref @player-ref
:seeking-audio? seeking-audio?
:max-audio-duration-ms max-duration-ms}]])
(when (or @recording? @reviewing-audio?)
[:f> f-time-counter @recording? @recording-length-ms @ready-to-delete? @reviewing-audio?
@audio-current-time-ms])
(when @recording?
[:f> f-recording-bar @recording-length-ms @ready-to-delete?])
[rn/view
{:test-ID "record-audio"
:style style/button-container
:hit-slop {:top -70
:bottom 0
:left 0
:right 0}
:pointer-events :box-only
:on-start-should-set-responder on-start-should-set-responder
:on-responder-move on-responder-move
:on-responder-release on-responder-release}
[:f> delete-button/f-delete-button recording? ready-to-delete? reviewing-audio?
@force-show-controls?]
[:f> lock-button/f-lock-button recording? ready-to-lock? locked?]
[:f> send-button/f-send-button recording? ready-to-send? reviewing-audio?
@force-show-controls?]
[:f> record-button-big/f-record-button-big
{:recording? recording?
:ready-to-send? ready-to-send?
:ready-to-lock? ready-to-lock?
:ready-to-delete? ready-to-delete?
:record-button-is-animating? record-button-is-animating?
:record-button-at-initial-position? record-button-at-initial-position?
:locked? locked?
:reviewing-audio? reviewing-audio?
:recording-length-ms recording-length-ms
:clear-timeout clear-timeout
:touch-active? touch-active?
:recorder-ref recorder-ref
:reload-recorder-fn reload-recorder
:idle? idle?
:on-send on-send
:on-cancel on-cancel
:theme theme}]
[:f> record-button/f-record-button recording? reviewing-audio?]]])))])
(defn record-audio
[{:keys [on-init on-start-recording on-send on-cancel on-reviewing-audio audio-file on-lock
max-duration-ms on-check-audio-permissions record-audio-permission-granted
on-request-record-audio-permission]}]
(let [;;STATE
[recording? set-recording] (rn/use-state false)
[locked? set-locked] (rn/use-state false)
[ready-to-send? set-ready-to-send] (rn/use-state false)
[ready-to-lock? set-ready-to-lock] (rn/use-state false)
[ready-to-delete?
set-ready-to-delete] (rn/use-state false)
[reviewing-audio?
set-reviewing-audio] (rn/use-state (some? audio-file))
[playing-audio?
set-playing-audio] (rn/use-state false)
[recording-length-ms
set-recording-length-ms] (rn/use-state 0)
[audio-current-time-ms
set-audio-current-time-ms] (rn/use-state 0)
[seeking-audio? set-seeking-audio] (rn/use-state false)
[force-show-controls?
set-force-show-controls] (rn/use-state (some? audio-file))
;;ATOMS
recording-start-ms (rn/use-ref-atom (datetime/timestamp))
clear-timeout (rn/use-ref-atom nil)
record-button-at-initial-position? (rn/use-ref-atom true)
record-button-is-animating? (rn/use-ref-atom false)
idle? (rn/use-ref-atom false)
recorder-ref (rn/use-ref-atom nil)
player-ref (rn/use-ref-atom nil)
touch-active? (rn/use-ref-atom false)
playing-timer (rn/use-ref-atom nil)
output-file (rn/use-ref-atom audio-file)
reached-max-duration? (rn/use-ref-atom false)
touch-timestamp (rn/use-ref-atom nil)
touch-identifier (rn/use-ref-atom nil)
disabled? (rn/use-ref-atom false)
app-state-listener (rn/use-ref-atom nil)
;;HANDLERS
destroy-player (rn/use-callback
(fn []
(audio/destroy-player @player-ref)
(reset! player-ref nil)))
reload-player (handlers/get-reload-player
[player-ref destroy-player set-playing-audio playing-timer
set-audio-current-time-ms set-seeking-audio])
recorder-on-meter (handlers/get-recorder-on-meter
[recording-start-ms set-recording-length-ms
reached-max-duration?
locked? set-locked set-reviewing-audio idle? set-recording
set-ready-to-lock set-ready-to-send set-ready-to-delete
recorder-ref
output-file reload-player
on-reviewing-audio])
reload-recorder (handlers/get-reload-recorder [recorder-ref
recorder-on-meter])
on-start-should-set-responder (handlers/get-on-start-should-set-responder
[locked? idle? disabled? recorder-on-meter touch-timestamp
touch-identifier
reviewing-audio? record-audio-permission-granted
set-recording set-playing-audio output-file
recorder-ref recording-start-ms set-audio-current-time-ms
on-start-recording
on-request-record-audio-permission touch-active?])
on-responder-move (handlers/get-on-responder-move
[locked? ready-to-send? set-ready-to-send
ready-to-delete? set-ready-to-delete
ready-to-lock? set-ready-to-lock
touch-identifier
record-button-at-initial-position?
recording?])
on-responder-release (handlers/get-on-responder-release
[idle? reached-max-duration? touch-timestamp
recording-length-ms set-recording-length-ms
reviewing-audio? set-reviewing-audio
set-audio-current-time-ms set-force-show-controls on-send
output-file player-ref
destroy-player on-cancel record-button-is-animating?
ready-to-lock? set-ready-to-lock
locked? set-locked on-lock ready-to-delete?
set-ready-to-delete ready-to-send? set-ready-to-send
disabled? on-reviewing-audio
recorder-ref set-recording reload-player recording-start-ms
touch-active?])]
(def record-audio (quo.theme/with-theme record-audio-internal))
;;ON MOUNT
(rn/use-mount
(fn []
(when on-check-audio-permissions (on-check-audio-permissions))
(when on-init
(on-init
(fn reset-recorder []
(set-recording false)
(set-reviewing-audio false)
(set-locked false)
(set-ready-to-send false)
(set-ready-to-lock false)
(set-ready-to-delete false)
(set-audio-current-time-ms 0)
(set-recording-length-ms 0)
(set-seeking-audio false)
(set-playing-audio false)
(reset! touch-active? false)
(reset! reached-max-duration? false)
(reset! output-file nil)
(reset! idle? false)
(reset! record-button-is-animating? false)
(reset! record-button-at-initial-position? true)
(when @clear-timeout
(js/clearTimeout @clear-timeout)
(reset! clear-timeout nil))
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil))
(reload-recorder))))
(when audio-file
(let [filename (last (string/split audio-file "/"))]
(reload-player filename)))
(reset! app-state-listener
(.addEventListener rn/app-state
"change"
#(when (= % "background")
(set-playing-audio false))))))
;;ON UNMOUNT
(rn/use-unmount #(.remove @app-state-listener))
[rn/view
{:style style/bar-container
:pointer-events :box-none}
(when reviewing-audio?
[:<>
[play-button playing-audio? set-playing-audio player-ref playing-timer set-audio-current-time-ms
seeking-audio? set-seeking-audio max-duration-ms]
[soundtrack/soundtrack
{:audio-current-time-ms audio-current-time-ms
:set-audio-current-time-ms set-audio-current-time-ms
:player-ref @player-ref
:seeking-audio? seeking-audio?
:set-seeking-audio set-seeking-audio
:max-audio-duration-ms max-duration-ms}]])
(when (or recording? reviewing-audio?)
[time-counter recording? recording-length-ms ready-to-delete? reviewing-audio?
audio-current-time-ms])
(when recording?
[recording-bar recording-length-ms ready-to-delete?])
[rn/view
{:test-ID "record-audio"
:style style/button-container
:hit-slop {:top -70 :bottom 0 :left 0 :right 0}
:pointer-events :box-only
:on-start-should-set-responder on-start-should-set-responder
:on-responder-move on-responder-move
:on-responder-release on-responder-release}
[delete-button/delete-button recording? ready-to-delete? reviewing-audio? force-show-controls?]
[lock-button/lock-button recording? ready-to-lock? locked?]
[send-button/send-button recording? ready-to-send? reviewing-audio? force-show-controls?]
[record-button-big/record-button-big
{:recording? recording?
:set-recording set-recording
:ready-to-send? ready-to-send?
:set-ready-to-send set-ready-to-send
:ready-to-lock? ready-to-lock?
:set-ready-to-lock set-ready-to-lock
:ready-to-delete? ready-to-delete?
:set-ready-to-delete set-ready-to-delete
:record-button-is-animating? record-button-is-animating?
:record-button-at-initial-position? record-button-at-initial-position?
:locked? locked?
:set-locked set-locked
:reviewing-audio? reviewing-audio?
:recording-length-ms recording-length-ms
:set-recording-length-ms set-recording-length-ms
:clear-timeout clear-timeout
:touch-active? touch-active?
:recorder-ref recorder-ref
:reload-recorder-fn reload-recorder
:idle? idle?
:on-send on-send
:on-cancel on-cancel}]
[record-button/record-button recording? reviewing-audio?]]]))
@@ -2,7 +2,6 @@
(:require
[quo.components.record-audio.soundtrack.view :as soundtrack]
[react-native.audio-toolkit :as audio]
[reagent.core :as reagent]
[test-helpers.component :as h]))
(h/describe "soundtrack component"
@@ -10,23 +9,21 @@
(h/test "renders soundtrack"
(with-redefs [audio/get-player-duration (fn [] 2000)]
(let [player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [:f> soundtrack/f-soundtrack
{:player-ref @player-ref
:audio-current-time-ms audio-current-time-ms}])
(-> (h/expect (h/get-by-test-id "soundtrack"))
(.toBeTruthy)))))
(h/render [soundtrack/soundtrack
{:player-ref {}
:audio-current-time-ms 0}])
(-> (h/expect (h/get-by-test-id "soundtrack"))
(.toBeTruthy))))
(h/test "soundtrack on-sliding-start works"
(with-redefs [audio/get-player-duration (fn [] 2000)]
(let [seeking-audio? (reagent/atom false)
player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [:f> soundtrack/f-soundtrack
{:seeking-audio? seeking-audio?
:player-ref @player-ref
:audio-current-time-ms audio-current-time-ms}])
(let [seeking-audio? (atom false)]
(h/render [soundtrack/soundtrack
{:seeking-audio? seeking-audio?
:set-seeking-audio #(reset! seeking-audio? %)
:player-ref {}
:audio-current-time-ms 0
:set-audio-current-time-ms #()}])
(h/fire-event
:on-sliding-start
(h/get-by-test-id "soundtrack"))
@@ -36,13 +33,13 @@
(h/test "soundtrack on-sliding-complete works"
(with-redefs [audio/get-player-duration (fn [] 2000)
audio/seek-player (js/jest.fn)]
(let [seeking-audio? (reagent/atom false)
player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [:f> soundtrack/f-soundtrack
{:seeking-audio? seeking-audio?
:player-ref @player-ref
:audio-current-time-ms audio-current-time-ms}])
(let [seeking-audio? (atom false)]
(h/render [soundtrack/soundtrack
{:seeking-audio? seeking-audio?
:set-seeking-audio #(reset! seeking-audio? %)
:player-ref {}
:audio-current-time-ms 0
:set-audio-current-time-ms #()}])
(h/fire-event
:on-sliding-start
(h/get-by-test-id "soundtrack"))
@@ -58,13 +55,14 @@
(h/test "soundtrack on-value-change when seeking audio works"
(with-redefs [audio/get-player-duration (fn [] 2000)
audio/seek-player (js/jest.fn)]
(let [seeking-audio? (reagent/atom false)
player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [:f> soundtrack/f-soundtrack
{:seeking-audio? seeking-audio?
:player-ref @player-ref
:audio-current-time-ms audio-current-time-ms}])
(let [seeking-audio? (atom false)
audio-current-time-ms (atom 0)]
(h/render [soundtrack/soundtrack
{:seeking-audio? seeking-audio?
:set-seeking-audio #(reset! seeking-audio? %)
:player-ref {}
:audio-current-time-ms audio-current-time-ms
:set-audio-current-time-ms #(reset! audio-current-time-ms %)}])
(h/fire-event
:on-sliding-start
(h/get-by-test-id "soundtrack"))
@@ -1,42 +1,47 @@
(ns quo.components.record-audio.soundtrack.view
(:require
[quo.components.icons.icons :as icons]
[quo.components.record-audio.soundtrack.style :as style]
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.audio-toolkit :as audio]
[react-native.core :as rn]
[react-native.platform :as platform]
[react-native.slider :as slider]
[taoensso.timbre :as log]))
(def ^:private thumb-light (js/require "../resources/images/icons2/12x12/thumb-light.png"))
(def ^:private thumb-dark (js/require "../resources/images/icons2/12x12/thumb-dark.png"))
(def ^:private thumb-light (icons/icon-source :thumb-light12))
(def ^:private thumb-dark (icons/icon-source :thumb-dark12))
(defn f-soundtrack
[{:keys [audio-current-time-ms player-ref style seeking-audio? max-audio-duration-ms]}]
(let [audio-duration-ms (min max-audio-duration-ms (audio/get-player-duration player-ref))
theme (quo.theme/use-theme-value)]
[:<>
[slider/slider
{:test-ID "soundtrack"
:style (merge
(style/player-slider-container)
(or style {}))
:minimum-value 0
:maximum-value audio-duration-ms
:value @audio-current-time-ms
:on-sliding-start #(reset! seeking-audio? true)
:on-sliding-complete (fn [seek-time]
(reset! seeking-audio? false)
(audio/seek-player
player-ref
seek-time
#(log/debug "[record-audio] on seek - seek time: " seek-time)
#(log/error "[record-audio] on seek - error: " %)))
:on-value-change #(when @seeking-audio?
(reset! audio-current-time-ms %))
:thumb-image (quo.theme/theme-value thumb-light thumb-dark theme)
:minimum-track-tint-color (colors/theme-colors colors/primary-50 colors/primary-60 theme)
:maximum-track-tint-color (colors/theme-colors
(if platform/ios? colors/neutral-20 colors/neutral-40)
(if platform/ios? colors/neutral-80 colors/neutral-60)
theme)}]]))
(defn soundtrack
[{:keys [audio-current-time-ms set-audio-current-time-ms player-ref style
seeking-audio? set-seeking-audio max-audio-duration-ms]}]
(let [audio-duration-ms (min max-audio-duration-ms (audio/get-player-duration player-ref))
theme (quo.theme/use-theme-value)
on-sliding-start (rn/use-callback #(set-seeking-audio true))
on-sliding-complete (rn/use-callback
(fn [seek-time]
(set-seeking-audio false)
(audio/seek-player
player-ref
seek-time
#(log/debug "[record-audio] on seek - seek time: " seek-time)
#(log/error "[record-audio] on seek - error: " %)))
[player-ref])
on-value-change (rn/use-callback #(when seeking-audio? (set-audio-current-time-ms %))
[seeking-audio?])]
[slider/slider
{:test-ID "soundtrack"
:style (merge (style/player-slider-container) style)
:minimum-value 0
:maximum-value audio-duration-ms
:value audio-current-time-ms
:on-sliding-start on-sliding-start
:on-sliding-complete on-sliding-complete
:on-value-change on-value-change
:thumb-image (quo.theme/theme-value thumb-light thumb-dark theme)
:minimum-track-tint-color (colors/theme-colors colors/primary-50 colors/primary-60 theme)
:maximum-track-tint-color (colors/theme-colors
(if platform/ios? colors/neutral-20 colors/neutral-40)
(if platform/ios? colors/neutral-80 colors/neutral-60)
theme)}]))
@@ -7,17 +7,23 @@
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[react-native.draggable-flatlist :as draggable-flatlist]
[reagent.core :as reagent]))
[react-native.draggable-flatlist :as draggable-flatlist]))
(defn on-drag-end-fn
[data atom-data]
(reset! atom-data data)
(reagent/flush))
(defn key-fn [item index] (str (:title item) index))
(defn- reorder-category-internal
[{:keys [label data blur? theme container-style]}]
(reagent/with-let [atom-data (reagent/atom data)]
(defn reorder-category
[{:keys [label data blur? container-style]}]
(let [theme (quo.theme/use-theme-value)
[atom-data set-atom-data] (rn/use-state data)
render-fn (rn/use-callback
(fn [item _ _ _ _ drag]
[reorder-item/reorder-item item types/item
{:blur? blur? :drag drag}])
[blur?])
on-drag-end-fn (rn/use-callback (fn [_ _ data] (set-atom-data data)))
separator (rn/use-memo (fn [] [rn/view
{:style (style/reorder-separator blur? theme)}])
[blur? theme])]
[rn/view {:style (merge (style/container label) container-style)}
[text/text
{:weight :medium
@@ -25,14 +31,9 @@
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}}
label]
[draggable-flatlist/draggable-flatlist
{:data @atom-data
:key-fn (fn [item index] (str (:title item) index))
{:data atom-data
:key-fn key-fn
:style style/reorder-items
:render-fn (fn [item _ _ _ _ drag] [reorder-item/reorder-item item types/item
{:blur? blur? :drag drag}])
:on-drag-end-fn (fn [_ _ data]
(on-drag-end-fn data atom-data))
:separator [rn/view
{:style (style/reorder-separator blur? theme)}]}]]))
(def reorder-category (quo.theme/with-theme reorder-category-internal))
:render-fn render-fn
:on-drag-end-fn on-drag-end-fn
:separator separator}]]))
@@ -23,7 +23,7 @@
(def ?profile
[:map
[:type [:= :profile]]
[:profile-picture :schema.common/image-source]])
[:profile-picture [:maybe :schema.quo/profile-picture-source]]])
(def ?wallet
[:map
@@ -16,7 +16,6 @@
[quo.foundations.colors :as colors]
[quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]
[utils.i18n :as i18n]))
@@ -190,20 +189,20 @@
(defn- view-internal
[props]
(reagent/with-let [component-width (reagent/atom nil)
container-component [rn/view {:background-color style/overlay-color}]]
(let [[component-width
set-component-width] (rn/use-state nil)
on-layout (rn/use-callback #(set-component-width
(oops/oget % "nativeEvent.layout.width")))
props (-> props
(assoc :component-width component-width)
(clojure.set/rename-keys {:type :share-qr-type}))]
[quo.theme/provider {:theme :dark}
[rn/view
{:accessibility-label :share-qr-code
:style style/outer-container
:on-layout #(reset! component-width (oops/oget % "nativeEvent.layout.width"))}
(conj container-component
(when @component-width
[share-qr-code
(-> props
(assoc :component-width @component-width)
(clojure.set/rename-keys {:type :share-qr-type}))]))]]))
:on-layout on-layout}
[rn/view {:style {:background-color style/overlay-color}}
(when component-width
[share-qr-code props])]]]))
(def view
(quo.theme/with-theme
(schema/instrument #'view-internal component-schema/?schema)))
(def view (schema/instrument #'view-internal component-schema/?schema))
+39 -40
View File
@@ -3,8 +3,7 @@
[quo.components.tabs.tab.view :as tab]
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]))
[react-native.core :as rn]))
(def themes-for-blur
{:light {:background-color colors/neutral-80-opa-5}
@@ -14,41 +13,41 @@
{:light {:background-color colors/neutral-10}
:dark {:background-color colors/neutral-90}})
(defn- segmented-control-internal
[{:keys [default-active on-change]}]
(let [active-tab-id (reagent/atom default-active)]
(fn [{:keys [data size theme blur? container-style item-container-style
active-item-container-style]}]
(let [active-id @active-tab-id]
[rn/view
(merge
{:flex-direction :row
:background-color (get-in (if blur? themes-for-blur themes)
[theme :background-color])
:border-radius (case size
32 10
28 10
24 8
20 6)
:padding 2}
container-style)
(for [[indx {:keys [label id]}] (map-indexed vector data)]
^{:key id}
[rn/view
{:margin-left (if (= 0 indx) 0 2)
:flex 1}
[tab/view
{:id id
:active-item-container-style active-item-container-style
:item-container-style item-container-style
:segmented? true
:size size
:blur? blur?
:active (= id active-id)
:on-press (fn [tab-id]
(reset! active-tab-id tab-id)
(when on-change
(on-change tab-id)))}
label]])]))))
(def segmented-control (quo.theme/with-theme segmented-control-internal))
(defn segmented-control
[{:keys [data size blur? container-style item-container-style
active-item-container-style default-active on-change]}]
(let [theme (quo.theme/use-theme-value)
[active-tab-id
set-active-tab-id] (rn/use-state default-active)
on-press (rn/use-callback
(fn [tab-id]
(set-active-tab-id tab-id)
(when on-change (on-change tab-id)))
[on-change])]
[rn/view
(merge
{:flex-direction :row
:background-color (get-in (if blur? themes-for-blur themes)
[theme :background-color])
:border-radius (case size
32 10
28 10
24 8
20 6)
:padding 2}
container-style)
(for [[indx {:keys [label id]}] (map-indexed vector data)]
^{:key id}
[rn/view
{:margin-left (if (= 0 indx) 0 2)
:flex 1}
[tab/view
{:id id
:active-item-container-style active-item-container-style
:item-container-style item-container-style
:segmented? true
:size size
:blur? blur?
:active (= id active-tab-id)
:on-press on-press}
label]])]))
+84 -92
View File
@@ -41,7 +41,7 @@
(into [:<>] children)))
(defn- on-scroll-handler
[{:keys [on-scroll fading fade-end-percentage fade-end?]} ^js e]
[{:keys [on-scroll fading set-fading fade-end-percentage fade-end?]} ^js e]
(when fade-end?
(let [offset-x (oget e "nativeEvent.contentOffset.x")
content-width (oget e "nativeEvent.contentSize.width")
@@ -52,11 +52,8 @@
:layout-width layout-width
:max-fade-percentage fade-end-percentage})]
;; Avoid unnecessary re-rendering.
(when (not= new-percentage
(get @fading :fade-end-percentage))
(swap! fading assoc
:fade-end-percentage
new-percentage))))
(when (not= new-percentage (get fading :fade-end-percentage))
(set-fading (assoc fading :fade-end-percentage new-percentage)))))
(when on-scroll
(on-scroll e)))
@@ -64,6 +61,7 @@
[{:keys [id label notification-dot? accessibility-label]}
index _
{:keys [active-tab-id
set-active-tab-id
blur?
customization-color
flat-list-ref
@@ -85,9 +83,9 @@
:accessibility-label accessibility-label
:size size
:blur? blur?
:active (= id @active-tab-id)
:active (= id active-tab-id)
:on-press (fn [id]
(reset! active-tab-id id)
(set-active-tab-id id)
(when (and scroll-on-press? @flat-list-ref)
(.scrollToIndex ^js @flat-list-ref
#js
@@ -118,87 +116,81 @@
- `scroll-on-press?` When non-nil, clicking on a tag centers it the middle
(with animation enabled).
"
[{:keys [default-active fade-end-percentage]
:or {fade-end-percentage 0.8}}]
(let [active-tab-id (reagent/atom default-active)
fading (reagent/atom {:fade-end-percentage fade-end-percentage})
flat-list-ref (atom nil)]
(fn
[{:keys [data
fade-end-percentage
fade-end?
on-change
on-scroll
scroll-on-press?
scrollable?
style
container-style
size
blur?
in-scroll-view?
customization-color]
:or {fade-end-percentage fade-end-percentage
fade-end? false
scrollable? false
scroll-on-press? false
size default-tab-size}
:as props}]
(if scrollable?
[rn/view {:style {:margin-top (- (dec unread-count-offset))}}
[masked-view-wrapper
{:fade-end-percentage (get @fading :fade-end-percentage) :fade-end? fade-end?}
[(if in-scroll-view?
gesture/flat-list
rn/flat-list)
(merge
(dissoc props
:default-active
:fade-end-percentage
:fade-end?
:on-change
:scroll-on-press?
:size)
(when scroll-on-press?
{:initial-scroll-index (utils.collection/first-index #(= @active-tab-id (:id %)) data)})
{:ref #(reset! flat-list-ref %)
:style style
;; The padding-top workaround is needed because on Android
;; {:overflow :visible} doesn't work on components inheriting
;; from ScrollView (e.g. FlatList). There are open issues, here's
;; just one about this topic:
;; https://github.com/facebook/react-native/issues/31218
:content-container-style
(assoc container-style :padding-top (dec unread-count-offset))
:horizontal true
:scroll-event-throttle 64
:shows-horizontal-scroll-indicator false
:data data
:key-fn (comp str :id)
:on-scroll-to-index-failed identity
:on-scroll (partial on-scroll-handler
{:fade-end-percentage fade-end-percentage
:fade-end? fade-end?
:fading fading
:on-scroll on-scroll})
:render-fn tab-view
:render-data {:active-tab-id active-tab-id
:blur? blur?
:customization-color customization-color
:flat-list-ref flat-list-ref
:number-of-items (count data)
:on-change on-change
:scroll-on-press? scroll-on-press?
:size size
:style style}})]]]
[rn/view (merge style {:flex-direction :row})
(map-indexed (fn [index item]
^{:key (:id item)}
[tab-view item index nil
{:active-tab-id active-tab-id
:blur? blur?
:customization-color customization-color
:number-of-items (count data)
:on-change on-change
:size size
:style style}])
data)]))))
[{:keys [default-active data fade-end-percentage fade-end? on-change on-scroll scroll-on-press?
scrollable? style container-style size blur? in-scroll-view? customization-color]
:or {fade-end-percentage 0.8
fade-end? false
scrollable? false
scroll-on-press? false
size default-tab-size}
:as props}]
(let [[active-tab-id
set-active-tab-id] (rn/use-state default-active)
[fading set-fading] (rn/use-state {:fade-end-percentage fade-end-percentage})
flat-list-ref (rn/use-ref-atom nil)
clean-props (dissoc props
:default-active
:fade-end-percentage
:fade-end?
:on-change
:scroll-on-press?
:size)
on-scroll (rn/use-callback
(partial on-scroll-handler
{:fade-end-percentage fade-end-percentage
:fade-end? fade-end?
:fading fading
:set-fading set-fading
:on-scroll on-scroll})
[fade-end? fading])]
(if scrollable?
[rn/view {:style {:margin-top (- (dec unread-count-offset))}}
[masked-view-wrapper
{:fade-end-percentage (get fading :fade-end-percentage) :fade-end? fade-end?}
[(if in-scroll-view?
gesture/flat-list
rn/flat-list)
(merge
clean-props
(when scroll-on-press?
{:initial-scroll-index (utils.collection/first-index #(= active-tab-id (:id %)) data)})
{:ref #(reset! flat-list-ref %)
:style style
;; The padding-top workaround is needed because on Android
;; {:overflow :visible} doesn't work on components inheriting
;; from ScrollView (e.g. FlatList). There are open issues, here's
;; just one about this topic:
;; https://github.com/facebook/react-native/issues/31218
:content-container-style
(assoc container-style :padding-top (dec unread-count-offset))
:horizontal true
:scroll-event-throttle 64
:shows-horizontal-scroll-indicator false
:data data
:key-fn (comp str :id)
:on-scroll-to-index-failed identity
:on-scroll on-scroll
:render-fn tab-view
:render-data {:active-tab-id active-tab-id
:set-active-tab-id set-active-tab-id
:blur? blur?
:customization-color customization-color
:flat-list-ref flat-list-ref
:number-of-items (count data)
:on-change on-change
:scroll-on-press? scroll-on-press?
:size size
:style style}})]]]
[rn/view (merge style {:flex-direction :row})
(map-indexed (fn [index item]
^{:key (:id item)}
[tab-view item index nil
{:active-tab-id active-tab-id
:set-active-tab-id set-active-tab-id
:blur? blur?
:customization-color customization-color
:number-of-items (count data)
:on-change on-change
:size size
:style style}])
data)])))
+1 -1
View File
@@ -357,7 +357,7 @@
;;;; Record Audio
(def record-audio quo.components.record-audio.record-audio.view/record-audio)
(def soundtrack quo.components.record-audio.soundtrack.view/f-soundtrack)
(def soundtrack quo.components.record-audio.soundtrack.view/soundtrack)
;;;; Selectors
(def disclaimer quo.components.selectors.disclaimer.view/view)
+6 -8
View File
@@ -38,7 +38,8 @@
(when on-meter
(.on ^js recorder "meter" on-meter))
(when on-ended
(.on ^js recorder "ended" on-ended))))
(.on ^js recorder "ended" on-ended))
recorder))
(defn new-player
[audio options on-ended]
@@ -46,7 +47,8 @@
audio
(clj->js options))]
(when on-ended
(.on ^js player "ended" on-ended))))
(.on ^js player "ended" on-ended))
player))
(defn prepare-player
[player on-prepared on-error]
@@ -157,20 +159,16 @@
(on-error {:error (.-err err) :message (.-message err)})
(on-seek))))))
(defn can-play?
[player]
(and player (.-canPlay ^js player)))
(defn destroy-recorder
[recorder]
(stop-recording recorder
#(when (and recorder (not= (get-state recorder) DESTROYED))
#(when (and recorder (.-destroy ^js recorder) (not= (get-state recorder) DESTROYED))
(.destroy ^js recorder))
#()))
(defn destroy-player
[player]
(stop-playing player
#(when (and player (not= (get-state player) IDLE))
#(when (and player (.-destroy ^js player) (not= (get-state player) IDLE))
(.destroy ^js player))
#()))
+5 -5
View File
@@ -39,13 +39,13 @@
[message]
(let [cause (if platform/android?
(condp = message
android-not-enrolled-error-message ::not-enrolled
android-not-available-error-message ::not-available
::unknown)
android-not-enrolled-error-message :biometrics/not-enrolled-error
android-not-available-error-message :biometrics/not-available-error
:biometrics/unknown-error)
(condp #(string/includes? %2 %1) message
ios-not-enrolled-error-message ::not-enrolled
::unknown))]
ios-not-enrolled-error-message :biometrics/not-enrolled-error
:biometrics/unknown-error))]
(ex-info "Failed to authenticate with biometrics"
{:orig-error-message message}
cause)))
+4
View File
@@ -224,3 +224,7 @@
(defn open-url [link] (.openURL ^js linking link))
(def set-status-bar-style react-native/StatusBar.setBarStyle)
(defn sharing
[content]
(.share (.-Share ^js react-native) (clj->js content)))
+4 -1
View File
@@ -20,7 +20,8 @@
LinearTransition
enableLayoutAnimations
useAnimatedScrollHandler
runOnJS)]
runOnJS)
:as reanimated*]
["react-native-redash" :refer (withPause)]
[react-native.flat-list :as rn-flat-list]
[react-native.utils :as rn.utils]
@@ -107,6 +108,8 @@
(when (and anim (some? v))
(set! (.-value anim) v)))
(def interpolate* reanimated*/interpolate)
(defn interpolate
([shared-value input-range output-range]
(interpolate shared-value input-range output-range nil))
+6 -1
View File
@@ -29,10 +29,15 @@
[:on-success [:or fn? [:cat keyword? [:* :any]]]]
[:on-error [:or fn? [:cat keyword? [:* :any]]]]]])
(def ^:private ?exception
[:fn {:error/message "schema.common/exception should be of type ExceptionInfo"}
(fn [v] (instance? ExceptionInfo v))])
(defn register-schemas
[]
(registry/register ::theme ?theme)
(registry/register ::customization-color ?customization-color)
(registry/register ::public-key ?public-key)
(registry/register ::image-source ?image-source)
(registry/register ::rpc-call ?rpc-call))
(registry/register ::rpc-call ?rpc-call)
(registry/register ::exception ?exception))
@@ -0,0 +1,27 @@
(ns status-im.common.alert-banner.events
(:require [re-frame.core :as re-frame]))
(defn add-alert-banner
[{:keys [db]} [banner]]
(let [current-banners-count (count (get db :alert-banners))
db (assoc-in db [:alert-banners (:type banner)] banner)]
(cond-> {:db db}
(zero? current-banners-count)
(assoc :show-alert-banner nil))))
(defn remove-alert-banner
[{:keys [db]} [banner-type]]
(let [db (update-in db [:alert-banners] dissoc banner-type)
new-count (count (get db :alert-banners))]
(cond-> {:db db}
(zero? new-count)
(assoc :hide-alert-banner nil))))
(defn remove-all-alert-banners
[{:keys [db]}]
{:db (dissoc db :alert-banners)
:hide-alert-banner nil})
(re-frame/reg-event-fx :alert-banners/add add-alert-banner)
(re-frame/reg-event-fx :alert-banners/remove remove-alert-banner)
(re-frame/reg-event-fx :alert-banners/remove-all remove-all-alert-banners)
@@ -0,0 +1,42 @@
(ns status-im.common.alert-banner.events-test
(:require
[cljs.test :refer [deftest is testing]]
matcher-combinators.test
[status-im.common.alert-banner.events :as events]))
(deftest add-alert-banner
(testing "Alert banner is added"
(is (match? {:db {:alert-banners
{:alert {:text "Alert"
:type :alert}}}
:show-alert-banner nil}
(events/add-alert-banner {:db {}}
[{:text "Alert"
:type :alert}])))))
(deftest remove-alert-banner
(testing "Alert banner is removed"
(is (match? {:db {}
:hide-alert-banner nil}
(events/remove-alert-banner {:db {:alert-banners
{:alert {:text "Alert"
:type :alert}}}}
[:alert]))))
(testing "Alert banner is not removed"
(is (match? {:db {:alert-banners
{:alert {:text "Alert"
:type :alert}}}}
(events/remove-alert-banner {:db {:alert-banners
{:alert {:text "Alert"
:type :alert}}}}
[:error])))))
(deftest remove-all-alert-banners
(testing "All Alert banners are removed"
(is (match? {:db {}
:hide-alert-banner nil}
(events/remove-all-alert-banners {:db {:alert-banners
{:alert {:text "Alert"
:type :alert}
:error {:text "Error"
:type :error}}}})))))
@@ -0,0 +1,20 @@
(ns status-im.common.alert-banner.style)
(def border-radius 20)
(defn container
[background-color]
{:background-color background-color})
(def second-banner-wrapper
{:margin-top (- border-radius)
:overflow :hidden
:border-top-left-radius border-radius
:border-top-right-radius border-radius})
(defn hole-view
[background-color]
{:padding-top 11
:align-items :center
:height 60
:background-color background-color})
@@ -0,0 +1,68 @@
(ns status-im.common.alert-banner.view
(:require [quo.core :as quo]
[quo.foundations.colors :as colors]
quo.theme
[react-native.core :as rn]
[react-native.hole-view :as hole-view]
[react-native.safe-area :as safe-area]
[status-im.common.alert-banner.style :as style]
[status-im.constants :as constants]
[utils.re-frame :as rf]))
(defn get-colors-map
[theme]
{:alert
{:background-color (colors/resolve-color :warning theme 20)
:text-color (colors/resolve-color :warning theme)}
:error
{:background-color (colors/resolve-color :danger theme 20)
:text-color (colors/resolve-color :danger theme)}})
(defn- banner
[{:keys [text type second-banner? colors-map]}]
[rn/view (when second-banner? {:style style/second-banner-wrapper})
[hole-view/hole-view
{:style (style/hole-view (get-in colors-map [type :background-color]))
:holes (if second-banner?
[]
[{:x 0
:y constants/alert-banner-height
:width (:width (rn/get-window))
:height constants/alert-banner-height
:borderRadius style/border-radius}])}
[quo/text
{:size :paragraph-2
:weight :medium
:style {:color (get-in colors-map [type :text-color])}}
text]]])
(defn view
[]
(let [banners (rf/sub [:alert-banners])
theme (quo.theme/use-theme-value)
banners-count (count banners)
alert-banner (:alert banners)
error-banner (:error banners)
safe-area-top (safe-area/get-top)
colors-map (get-colors-map theme)]
[hole-view/hole-view
{:style {:background-color colors/neutral-100}
:holes [{:x 0
:y (+ safe-area-top (* constants/alert-banner-height banners-count))
:width (:width (rn/get-window))
:height constants/alert-banner-height
:borderRadius style/border-radius}]}
[rn/view
{:style {:height safe-area-top
:background-color (get-in colors-map
[(if error-banner :error :alert) :background-color])}}]
(when error-banner
[banner
(assoc error-banner
:colors-map colors-map
:second-banner? false)])
(when alert-banner
[banner
(assoc alert-banner
:colors-map colors-map
:second-banner? (= 2 banners-count))])]))
@@ -0,0 +1,72 @@
(ns status-im.common.biometric.effects
(:require
[react-native.biometrics :as biometrics]
[status-im.common.biometric.utils :as utils]
[status-im.common.keychain.events :as keychain]
[status-im.constants :as constant]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(rf/reg-fx
:effects.biometric/get-supported-type
(fn []
;;NOTE: if we can't save user password, we can't use biometric
(keychain/can-save-user-password?
(fn [can-save?]
(when (and can-save? (not utils/android-device-blacklisted?))
(-> (biometrics/get-supported-type)
(.then (fn [type]
(rf/dispatch [:biometric/set-supported-type type])))))))))
(rf/reg-fx
:effects.biometric/authenticate
(fn [{:keys [prompt-message on-success on-fail on-cancel on-done]
:or {on-done identity
on-success identity
on-cancel identity
on-fail identity}}]
(-> (biometrics/authenticate
{:prompt-message (or prompt-message (i18n/label :t/biometric-auth-reason-login))
:fallback-prompt-message (i18n/label
:t/biometric-auth-login-ios-fallback-label)
:cancel-button-text (i18n/label :t/cancel)})
(.then (fn [success?]
(on-done)
(if success?
(on-success)
(on-cancel))))
(.catch (fn [err]
(on-done)
(on-fail err))))))
(rf/reg-fx
:effects.biometric/check-if-available
(fn [{:keys [key-uid on-success on-fail]
:or {on-success identity
on-fail identity}}]
(keychain/can-save-user-password?
(fn [can-save?]
(if-not can-save?
(on-fail (ex-info "cannot-save-user-password"
{:effect :effects.biometric/check-if-available}))
(-> (biometrics/get-available)
(.then (fn [available?]
(when-not available?
(throw (js/Error. "biometric-not-available")))))
(.then #(keychain/get-auth-method! key-uid))
(.then (fn [auth-method]
(if (= auth-method constant/auth-method-biometric)
(on-success auth-method)
(throw (js/Error. "biometric-not-enabled")))))
(.catch (fn [err]
(let [message (.-message err)]
(on-fail (ex-info message
{:err err
:effect :effects.biometric/check-if-available}))
(when-not (or (= message "biometric-not-available")
(= message "biometric-not-enabled"))
(log/error "Failed to check if biometrics is available"
{:error err
:key-uid key-uid
:effect :effects.biometric/check-biometric})))))))))))
+44 -102
View File
@@ -1,120 +1,62 @@
(ns status-im.common.biometric.events
(:require
[native-module.core :as native-module]
[re-frame.core :as re-frame]
[react-native.biometrics :as biometrics]
[react-native.platform :as platform]
[status-im.common.keychain.events :as keychain]
status-im.common.biometric.effects
[status-im.constants :as constants]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(def android-device-blacklisted?
(and platform/android? (= (:brand (native-module/get-device-model-info)) "bannedbrand")))
(defn set-supported-type
[{:keys [db]} [supported-type]]
{:db (assoc-in db [:biometrics :supported-type] supported-type)})
(defn get-label-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-android (i18n/label :t/biometric-fingerprint)
constants/biometrics-type-face-id (i18n/label :t/biometric-faceid)
(i18n/label :t/biometric-touchid)))
(rf/reg-event-fx :biometric/set-supported-type set-supported-type)
(defn get-icon-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-face-id :i/face-id
:i/touch-id))
(re-frame/reg-fx
:biometric/get-supported-biometric-type
(fn []
;;NOTE: if we can't save user password, we can't use biometric
(keychain/can-save-user-password?
(fn [can-save?]
(when (and can-save? (not android-device-blacklisted?))
(-> (biometrics/get-supported-type)
(.then (fn [type]
(rf/dispatch [:biometric/get-supported-biometric-type-success type])))))))))
(rf/defn get-supported-biometric-auth-success
{:events [:biometric/get-supported-biometric-type-success]}
[{:keys [db]} supported-type]
{:db (assoc db :biometric/supported-type supported-type)})
(rf/defn show-message
{:events [:biometric/show-message]}
[_ error]
(let [code (ex-cause error)
content (if (#{::biometrics/not-enrolled
::biometrics/not-available}
(defn show-message
[_ [code]]
(let [content (if (#{:biometrics/not-enrolled-error
:biometrics/not-available-error}
code)
(i18n/label :t/grant-face-id-permissions)
(i18n/label :t/biometric-auth-error {:code code}))]
{:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content content}}))
{:fx [[:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content content}]]}))
(rf/reg-event-fx :biometric/show-message show-message)
(re-frame/reg-fx
:biometric/authenticate
(fn [{:keys [on-success on-fail prompt-message]}]
(-> (biometrics/authenticate
{:prompt-message (or prompt-message (i18n/label :t/biometric-auth-reason-login))
:fallback-prompt-message (i18n/label
:t/biometric-auth-login-ios-fallback-label)
:cancel-button-text (i18n/label :t/cancel)})
(.then (fn [not-canceled?]
(when (and on-success not-canceled?)
(on-success))))
(.catch (fn [err]
(when on-fail
(on-fail err)))))))
(defn on-authentication-done
[{:keys [db]}]
{:db (assoc-in db [:biometrics :auth-pending?] false)})
(rf/defn authenticate
{:events [:biometric/authenticate]}
[_ opts]
{:biometric/authenticate opts})
(rf/reg-event-fx :biometric/on-authentication-done on-authentication-done)
(rf/reg-event-fx
:biometric/on-enable-success
(fn [{:keys [db]} [password]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-biometric)
:dispatch [:keychain/save-password-and-auth-method
{:key-uid key-uid
:masked-password password}]})))
(defn authenticate
[{:keys [db]} [opts]]
(let [pending? (get-in db [:biometrics :auth-pending?])]
;;NOTE: prompting biometric check while another one is pending triggers error
(when-not pending?
{:db (assoc-in db [:biometrics :auth-pending?] true)
:fx [[:effects.biometric/authenticate
(assoc opts :on-done #(rf/dispatch [:biometric/on-authentication-done]))]]})))
(rf/reg-event-fx
:biometric/enable
(fn [_ [password]]
{:dispatch [:biometric/authenticate
{:on-success #(rf/dispatch [:biometric/on-enable-success password])
:on-fail #(rf/dispatch [:biometric/show-message %])}]}))
(rf/reg-event-fx :biometric/authenticate authenticate)
(rf/reg-event-fx
:biometric/disable
(fn [{:keys [db]}]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-none)
:keychain/clear-user-password key-uid})))
(defn enable-biometrics
[{:keys [db]} [password]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-biometric)
:fx [[:dispatch
[:keychain/save-password-and-auth-method
{:key-uid key-uid
:masked-password password}]]]}))
(rf/reg-event-fx :biometric/enable enable-biometrics)
(defn disable-biometrics
[{:keys [db]}]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-none)
:fx [[:keychain/clear-user-password key-uid]]}))
(rf/reg-event-fx :biometric/disable disable-biometrics)
(rf/reg-fx
:biometric/check-if-available
(fn [[key-uid callback]]
(keychain/can-save-user-password?
(fn [can-save?]
(when can-save?
(-> (biometrics/get-available)
(.then (fn [available?]
(when-not available?
(throw (js/Error. "biometric-not-available")))))
(.then #(keychain/get-auth-method! key-uid))
(.then (fn [auth-method]
(when auth-method (callback auth-method))))
(.catch (fn [err]
(when-not (= (.-message err) "biometric-not-available")
(log/error "Failed to check if biometrics is available"
{:error err
:key-uid key-uid
:event :profile.login/check-biometric}))))))))))
@@ -0,0 +1,68 @@
(ns status-im.common.biometric.events-test
(:require [cljs.test :refer [deftest is testing]]
matcher-combinators.test
[status-im.common.biometric.events :as sut]
[status-im.constants :as constants]
[utils.i18n :as i18n]
[utils.security.core :as security]))
(deftest set-supported-biometrics-type-test
(testing "successfully setting supported biometrics type"
(let [cofx {:db {}}
supported-type constants/biometrics-type-face-id
expected {:db (assoc-in (:db cofx) [:biometrics :supported-type] supported-type)}]
(is (match? expected (sut/set-supported-type cofx [supported-type]))))))
(deftest show-message-test
(testing "informs the user to enable biometrics from settings"
(let [cofx {:db {}}
expected {:fx [[:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content (i18n/label :t/grant-face-id-permissions)}]]}]
(is (match? expected
(sut/show-message cofx
[:biometrics/not-available-error])))))
(testing "shows a generic error message"
(let [cofx {:db {}}
error-cause :test-error
expected {:fx [[:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content (i18n/label :t/biometric-auth-error {:code error-cause})}]]}]
(is (match? expected
(sut/show-message cofx
[error-cause]))))))
(deftest authenticate-biometrics-test
(testing "passing the right args to authenticate"
(let [cofx {:db {}}
args {:on-success identity
:on-fail identity
:prompt-message "test"}
result (sut/authenticate cofx [args])]
(is (= (:prompt-message args) (get-in result [:fx 0 1 :prompt-message])))
(is (not (nil? (get-in result [:fx 0 1 :on-success]))))
(is (not (nil? (get-in result [:fx 0 1 :on-fail]))))))
(testing "skips biometric check if another one pending"
(let [cofx {:db {:biometrics {:auth-pending? true}}}
result (sut/authenticate cofx [{}])]
(is (nil? result)))))
(deftest enable-biometrics-test
(testing "successfully enabling biometrics"
(let [key-uid "test-uid"
password (security/mask-data "test-pw")
cofx {:db {:profile/profile {:key-uid key-uid}}}
expected-db (assoc (:db cofx) :auth-method constants/auth-method-biometric)
result (sut/enable-biometrics cofx [password])]
(is (match? expected-db (:db result)))
(is (= password (get-in result [:fx 0 1 1 :masked-password]))))))
(deftest disable-biometrics-test
(testing "successfully disabling biometrics"
(let [key-uid "test-uid"
cofx {:db {:profile/profile {:key-uid key-uid}}}
expected {:db (assoc (:db cofx) :auth-method constants/auth-method-none)
:fx [[:keychain/clear-user-password key-uid]]}]
(is (match? expected (sut/disable-biometrics cofx))))))
+22
View File
@@ -0,0 +1,22 @@
(ns status-im.common.biometric.utils
(:require
[native-module.core :as native-module]
[react-native.platform :as platform]
[status-im.constants :as constants]
[utils.i18n :as i18n]))
(def android-device-blacklisted?
(and platform/android? (= (:brand (native-module/get-device-model-info)) "bannedbrand")))
(defn get-label-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-android (i18n/label :t/biometric-fingerprint)
constants/biometrics-type-face-id (i18n/label :t/biometric-faceid)
(i18n/label :t/biometric-touchid)))
(defn get-icon-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-face-id :i/face-id
:i/touch-id))
@@ -2,14 +2,12 @@
(:require
[quo.foundations.colors :as colors]
[quo.theme :as theme]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]))
(defn container
[{:keys [top] :as _insets}]
(let [padding-top (if platform/ios? top (+ top 10))]
{:flex 1
:padding-top padding-top}))
[padding-top]
{:flex 1
:padding-top padding-top})
(defn background
[opacity]
@@ -49,21 +49,24 @@
set-animating-false (fn [ms]
(js/setTimeout #(reset! animating? false) ms))]
(fn [{:keys [content skip-background? theme]}]
(let [insets (safe-area/get-insets)
{:keys [height]} (rn/get-window)
opacity (reanimated/use-shared-value 0)
translate-y (reanimated/use-shared-value height)
close (fn []
(set-animating-true)
(reanimated/animate translate-y height 300)
(reanimated/animate opacity 0 300)
(rf/dispatch [:navigate-back])
true)
reset-open-sheet (fn []
(reanimated/animate translate-y 0 300)
(reanimated/animate opacity 1 300)
(set-animating-false 300)
(reset! scroll-enabled? true))]
(let [{:keys [top] :as insets} (safe-area/get-insets)
alert-banners-top-margin (rf/sub [:alert-banners/top-margin])
padding-top (+ alert-banners-top-margin
(if platform/ios? top (+ top 10)))
{:keys [height]} (rn/get-window)
opacity (reanimated/use-shared-value 0)
translate-y (reanimated/use-shared-value height)
close (fn []
(set-animating-true)
(reanimated/animate translate-y height 300)
(reanimated/animate opacity 0 300)
(rf/dispatch [:navigate-back])
true)
reset-open-sheet (fn []
(reanimated/animate translate-y 0 300)
(reanimated/animate opacity 1 300)
(set-animating-false 300)
(reset! scroll-enabled? true))]
(rn/use-mount
(fn []
(rn/hw-back-add-listener close)
@@ -71,7 +74,7 @@
(reanimated/animate opacity 1 300)
(set-animating-false 300)
#(rn/hw-back-remove-listener close)))
[rn/view {:style (style/container insets)}
[rn/view {:style (style/container padding-top)}
(when-not skip-background?
[reanimated/view {:style (style/background opacity)}])
[gesture/gesture-detector
+57 -24
View File
@@ -1,31 +1,36 @@
(ns status-im.common.scalable-avatar.view
(ns ^:workletize status-im.common.scalable-avatar.view
(:require [quo.core :as quo]
[react-native.reanimated :as reanimated]
[status-im.common.scalable-avatar.style :as style]))
(def scroll-animation-input-range [0 50])
(def header-extrapolation-option
{:extrapolateLeft "clamp"
:extrapolateRight "clamp"})
;;[status-im.common.scalable-avatar.style :as style]
[status-im.contexts.profile.settings.header.style :as style]))
(defn f-avatar
[{:keys [scroll-y full-name online? profile-picture customization-color border-color]}]
(let [image-scale-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[1 0.4]
header-extrapolation-option)
image-top-margin-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[0 20]
header-extrapolation-option)
image-side-margin-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[-4 -20]
header-extrapolation-option)]
[reanimated/view
{:style (style/wrapper {:scale image-scale-animation
:margin-top image-top-margin-animation
:margin image-side-margin-animation
:border-color border-color})}
[{:keys [scroll-y full-name online? profile-picture customization-color]}]
(let [avatar-total-size 88
border-height 4
translation-to-bottom (fn [scale inverse-scale]
(js* "'worklet'")
(let [current-avatar-size (* avatar-total-size (- 1 scale))
current-center (/ current-avatar-size 2)]
(* current-center inverse-scale)))
transform (reanimated/use-animated-style
(fn []
(js* "'worklet'")
(let [scale (reanimated/interpolate*
(.-value scroll-y)
#js [0 48]
#js [1 0.4]
"clamp")
inverse-scale (/ 1 scale)
translation (translation-to-bottom scale inverse-scale)
bottom (* border-height inverse-scale (- 1 scale))]
#js
{:transform #js
[#js {:scale scale}
#js {:translateX (- translation)}
#js {:translateY (- translation bottom)}]})))
theme (quo.theme/get-theme)]
[reanimated/view {:style [transform (style/avatar-container theme)]}
[quo/user-avatar
{:full-name full-name
:online? online?
@@ -35,6 +40,34 @@
:customization-color customization-color
:size :big}]]))
#_(defn f-avatar
[{:keys [scroll-y full-name online? profile-picture customization-color border-color]}]
(let [image-scale-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[1 0.4]
header-extrapolation-option)
image-top-margin-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[0 20]
header-extrapolation-option)
image-side-margin-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[-4 -20]
header-extrapolation-option)]
[reanimated/view
{:style (style/wrapper {:scale image-scale-animation
:margin-top image-top-margin-animation
:margin image-side-margin-animation
:border-color border-color})}
[quo/user-avatar
{:full-name full-name
:online? online?
:profile-picture profile-picture
:status-indicator? true
:ring? true
:customization-color customization-color
:size :big}]]))
(defn view
[props]
[:f> f-avatar props])
@@ -1,11 +1,93 @@
(ns status-im.common.standard-authentication.events
(:require
[utils.re-frame :as rf]))
[schema.core :as schema]
[status-im.common.standard-authentication.enter-password.view :as enter-password]
[status-im.common.standard-authentication.events-schema :as events-schema]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.security.core :as security]))
(rf/reg-event-fx :standard-auth/on-biometric-success
(fn [{:keys [db]} [callback]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:keychain/get-user-password [key-uid callback]]]})))
(defn authorize
[{:keys [db]} [args]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:effects.biometric/check-if-available
{:key-uid key-uid
:on-success #(rf/dispatch [:standard-auth/authorize-with-biometric args])
:on-fail #(rf/dispatch [:standard-auth/authorize-with-password args])}]]}))
(schema/=> authorize events-schema/?authorize)
(rf/reg-event-fx :standard-auth/authorize authorize)
(defn authorize-with-biometric
[_ [{:keys [on-auth-success on-auth-fail] :as args}]]
(let [args-with-biometric-btn
(assoc args
:on-press-biometric
#(rf/dispatch [:standard-auth/authorize-with-biometric args]))]
{:fx [[:dispatch [:dismiss-keyboard]]
[:dispatch
[:biometric/authenticate
{:prompt-message (i18n/label :t/biometric-auth-confirm-message)
:on-cancel #(rf/dispatch [:standard-auth/authorize-with-password
args-with-biometric-btn])
:on-success #(rf/dispatch [:standard-auth/on-biometric-success on-auth-success])
:on-fail (fn [err]
(when on-auth-fail (on-auth-fail err))
(rf/dispatch [:standard-auth/on-biometric-fail err]))}]]]}))
(schema/=> authorize-with-biometric events-schema/?authorize-with-biometric)
(rf/reg-event-fx :standard-auth/authorize-with-biometric authorize-with-biometric)
(defn on-biometric-success
[{:keys [db]} [on-auth-success]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:keychain/get-user-password [key-uid on-auth-success]]
[:dispatch [:standard-auth/reset-login-password]]]}))
(schema/=> on-biometric-success events-schema/?on-biometric-success)
(rf/reg-event-fx :standard-auth/on-biometric-success on-biometric-success)
(defn on-biometric-fail
[_ [error]]
(log/error (ex-message error)
(-> error
ex-data
(assoc :code (ex-cause error)
:event :standard-auth/on-biometric-fail)))
{:fx [[:dispatch [:standard-auth/reset-login-password]]
[:dispatch [:biometric/show-message (ex-cause error)]]]})
(schema/=> on-biometric-fail events-schema/?on-biometrics-fail)
(rf/reg-event-fx :standard-auth/on-biometric-fail on-biometric-fail)
(defn- bottom-sheet-password-view
[{:keys [on-press-biometric on-auth-success auth-button-icon-left auth-button-label]}]
(fn []
(let [handle-password-success (fn [password]
(rf/dispatch [:standard-auth/reset-login-password])
(-> password security/hash-masked-password on-auth-success))]
[enter-password/view
{:on-enter-password handle-password-success
:on-press-biometrics on-press-biometric
:button-icon-left auth-button-icon-left
:button-label auth-button-label}])))
(defn authorize-with-password
[_ [{:keys [on-close theme blur?] :as args}]]
{:fx [[:dispatch [:standard-auth/reset-login-password]]
[:dispatch
[:show-bottom-sheet
{:on-close (fn []
(rf/dispatch [:standard-auth/reset-login-password])
(when on-close
(on-close)))
:theme theme
:shell? blur?
:content #(bottom-sheet-password-view args)}]]]})
(schema/=> authorize-with-password events-schema/?authorize-with-password)
(rf/reg-event-fx :standard-auth/authorize-with-password authorize-with-password)
(rf/reg-event-fx
:standard-auth/reset-login-password
@@ -0,0 +1,53 @@
(ns status-im.common.standard-authentication.events-schema)
(def ^:private ?authorize-map
[:map {:closed true}
[:on-auth-success fn?]
[:on-auth-fail {:optional true} [:maybe fn?]]
[:on-close {:optional true} [:maybe fn?]]
[:auth-button-label {:optional true} [:maybe string?]]
[:auth-button-icon-left {:optional true} [:maybe keyword?]]
[:blur? {:optional true} [:maybe boolean?]]
[:theme {:optional true} [:maybe :schema.common/theme]]])
(def ?authorize
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:any])
(def ?authorize-with-biometric
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:any])
(def ?on-biometric-success
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple fn?]]]
:any])
(def ?on-biometrics-fail
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple
[:maybe fn?]
[:maybe :schema.common/exception]]]]
:any])
(def ?authorize-with-password
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:any])
@@ -56,6 +56,7 @@
error? (boolean (seq error-message))
default-value (rn/use-ref-atom "") ;;bug on Android
;;https://github.com/status-im/status-mobile/issues/19004
theme (quo.theme/use-theme-value)
on-change-password (rn/use-callback
(fn [entered-password]
(reset! default-value entered-password)
@@ -69,6 +70,7 @@
[quo/input
{:container-style {:flex 1}
:type :password
:theme theme
:default-value @default-value
:blur? blur?
:disabled? processing
@@ -1,67 +0,0 @@
(ns status-im.common.standard-authentication.standard-auth.authorize
(:require
[react-native.biometrics :as biometrics]
[status-im.common.standard-authentication.enter-password.view :as enter-password]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.security.core :as security]))
(defn reset-password
[]
(rf/dispatch [:set-in [:profile/login :password] nil])
(rf/dispatch [:set-in [:profile/login :error] ""]))
(defn authorize
[{:keys [biometric-auth? on-auth-success on-auth-fail on-close
auth-button-label theme blur? auth-button-icon-left]}]
(let [handle-auth-success (fn [biometric?]
(fn [entered-password]
(let [sha3-masked-password (if biometric?
entered-password
(security/hash-masked-password
entered-password))]
(on-auth-success sha3-masked-password))))
password-login (fn [{:keys [on-press-biometrics]}]
(rf/dispatch [:show-bottom-sheet
{:on-close on-close
:theme theme
:shell? blur?
:content (fn []
[enter-password/view
{:on-enter-password (handle-auth-success
false)
:on-press-biometrics on-press-biometrics
:button-icon-left auth-button-icon-left
:button-label auth-button-label}])}]))
; biometrics-login recursively passes itself as a parameter because if the user
; fails biometric auth they will be shown the password bottom sheet with an option
; to retrigger biometric auth, so they can endlessly repeat this cycle.
biometrics-login (fn [on-press-biometrics]
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch
[:biometric/authenticate
{:prompt-message (i18n/label :t/biometric-auth-confirm-message)
:on-success (fn []
(on-close)
(rf/dispatch [:standard-auth/on-biometric-success
(handle-auth-success true)]))
:on-fail (fn [error]
(on-close)
(log/error
(ex-message error)
(-> error ex-data (assoc :code (ex-cause error))))
(when on-auth-fail (on-auth-fail error))
(password-login {:on-press-biometrics
#(on-press-biometrics
on-press-biometrics)}))}]))]
(if biometric-auth?
(-> (biometrics/get-supported-type)
(.then (fn [biometric-type]
(if biometric-type
(biometrics-login biometrics-login)
(do
(reset-password)
(password-login {})))))
(.catch #(password-login {})))
(password-login {}))))
@@ -3,7 +3,6 @@
[quo.core :as quo]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[status-im.common.standard-authentication.standard-auth.authorize :as authorize]
[status-im.constants :as constants]
[utils.re-frame :as rf]))
@@ -16,14 +15,15 @@
biometric-auth? (= auth-method constants/auth-method-biometric)
on-complete (rn/use-callback
(fn [reset]
(authorize/authorize {:on-close #(js/setTimeout reset 200)
:auth-button-icon-left auth-button-icon-left
:theme theme
:blur? blur?
:biometric-auth? biometric-auth?
:on-auth-success on-auth-success
:on-auth-fail on-auth-fail
:auth-button-label auth-button-label}))
(rf/dispatch [:standard-auth/authorize
{:on-close #(js/setTimeout reset 200)
:auth-button-icon-left auth-button-icon-left
:theme theme
:blur? blur?
:biometric-auth? biometric-auth?
:on-auth-success on-auth-success
:on-auth-fail on-auth-fail
:auth-button-label auth-button-label}]))
[theme])]
[quo/slide-button
{:container-style container-style
+4 -4
View File
@@ -98,7 +98,7 @@
[cofx {address :account}]
(when-let [account (existing-account? cofx address)]
(navigation/navigate-to cofx
:wallet-account
:screen/wallet.accounts
account)))
(defn handle-not-found
@@ -173,7 +173,7 @@
(handle-url url))))
(defn generate-profile-url
[{:keys [db]} [{:keys [public-key cb]}]]
[{:keys [db]} [{:keys [public-key on-success]}]]
(let [profile-public-key (get-in db [:profile/profile :public-key])
profile? (or (not public-key) (= public-key profile-public-key))
ens-name? (if profile?
@@ -186,7 +186,7 @@
:params [public-key]
:on-success (fn [url]
(rf/dispatch [:universal-links/save-profile-url public-key url])
(when (fn? cb) (cb)))
(when (fn? on-success) (on-success url)))
:on-error #(log/error "failed to wakuext_shareUserURLWithData"
{:error %
:public-key public-key})}]})))
@@ -200,7 +200,7 @@
[:?
[:map
[:public-key {:optional true} :schema.common/public-key]
[:cb {:optional true} fn?]]]]]]
[:on-success {:optional true} fn?]]]]]]
[:map
[:json-rpc/call :schema.common/rpc-call]]])
+4
View File
@@ -465,3 +465,7 @@
(def ^:const bridge-name-transfer "Transfer")
(def ^:const bridge-name-erc-721-transfer "ERC721Transfer")
(def ^:const alert-banner-height 40)
(def ^:const status-hostname "status.app")
@@ -23,7 +23,9 @@
:blocked? (oops/oget js-contact "blocked")
:added? (oops/oget js-contact "added")
:has-added-us? (oops/oget js-contact "hasAddedUs")
:mutual? (oops/oget js-contact "mutual")})
:mutual? (oops/oget js-contact "mutual")
:emoji-hash (oops/oget js-contact "emojiHash")
:bio (oops/oget js-contact "bio")})
(defn prepare-events-for-contact
[db chats-js]
@@ -1,7 +1,7 @@
(ns status-im.contexts.chat.messenger.composer.events
(:require [clojure.string :as string]
[legacy.status-im.chat.models.mentions :as mentions]
[legacy.status-im.data-store.messages :as data-store-messages]
[legacy.status-im.data-store.messages :as data-store.messages]
[status-im.constants :as constants]
[status-im.contexts.chat.messenger.composer.link-preview.events :as link-preview]
[status-im.contexts.chat.messenger.messages.transport.events :as messages.transport]
@@ -132,6 +132,37 @@
:response-to message-id})
images)))
(defn- build-status-link-previews
[status-link-previews]
(map (fn [{{:keys [community-id color description display-name
members-count active-members-count icon banner]}
:community
url :url}]
{:url url
:community
{:community-id community-id
:color color
:description description
:display-name display-name
:members-count members-count
:active-members-count active-members-count
:icon {:data-uri (:data-uri icon)
:width (:width icon)
:height (:height icon)}
:banner {:data-uri (:data-uri banner)
:width (:width banner)
:height (:height banner)}}})
status-link-previews))
(defn- build-link-previews
[link-previews]
(->> link-previews
(map #(select-keys %
[:url :title :description :thumbnail
:status-link-preview? :favicon]))
(filter (fn [{:keys [status-link-preview?]}]
(not status-link-preview?)))))
(defn build-text-message
[{:keys [db]} input-text current-chat-id]
(when-not (string/blank? input-text)
@@ -140,15 +171,16 @@
preferred-name (get-in db [:profile/profile :preferred-name])
emoji? (emoji-only-content? {:text input-text
:response-to message-id})]
{:chat-id current-chat-id
:content-type (if emoji?
constants/content-type-emoji
constants/content-type-text)
:text input-text
:response-to message-id
:ens-name preferred-name
:link-previews (map #(select-keys % [:url :title :description :thumbnail])
(get-in db [:chat/link-previews :unfurled]))})))
{:chat-id current-chat-id
:content-type (if emoji?
constants/content-type-emoji
constants/content-type-text)
:text input-text
:response-to message-id
:ens-name preferred-name
:link-previews (build-link-previews (get-in db [:chat/link-previews :unfurled]))
:status-link-previews (build-status-link-previews
(get-in db [:chat/status-link-previews :unfurled]))})))
(rf/defn send-messages
[{:keys [db] :as cofx} input-text current-chat-id]
@@ -181,29 +213,41 @@
(when message-id
{:response-to message-id}))])))))
(defn- process-link-previews
[link-previews]
(->> link-previews
(map #(select-keys %
[:url :title :description :thumbnail
:status-link-preview? :favicon]))
(map data-store.messages/->link-preview-rpc)
(filter (fn [{:keys [status-link-preview?]}]
(not status-link-preview?)))))
(rf/defn send-edited-message
[{:keys [db]
:as cofx} text {:keys [message-id quoted-message chat-id]}]
(rf/merge
cofx
{:json-rpc/call [{:method "wakuext_editMessage"
:params [{:id message-id
:text text
:content-type (if (emoji-only-content?
{:text text
:response-to quoted-message})
constants/content-type-emoji
constants/content-type-text)
:linkPreviews (map #(-> %
(select-keys [:url :title :description
:thumbnail])
data-store-messages/->link-preview-rpc)
(get-in db [:chat/link-previews :unfurled]))}]
:js-response true
:on-error #(log/error "failed to edit message " %)
:on-success (fn [result]
(rf/dispatch [:sanitize-messages-and-process-response
result]))}]}
{:json-rpc/call
[{:method "wakuext_editMessage"
:params [{:id message-id
:text text
:content-type (if (emoji-only-content?
{:text text
:response-to quoted-message})
constants/content-type-emoji
constants/content-type-text)
:linkPreviews (process-link-previews (get-in db
[:chat/link-previews :unfurled]))
:statusLinkPreviews (map data-store.messages/->status-link-previews-rpc
(get-in db
[:chat/status-link-previews
:unfurled]))}]
:js-response true
:on-error #(log/error "failed to edit message " %)
:on-success (fn [result]
(rf/dispatch [:sanitize-messages-and-process-response
result]))}]}
(link-preview/reset-unfurled)
(cancel-message-edit)))
@@ -80,24 +80,51 @@
[]
curr-previews))
(defn- merge-preview-types
[new-previews new-status-link-previews]
(-> (map
(fn [{{:keys [display-name members-count banner icon
description]} :community
url :url}]
;; Other types of previews need to be done here too
;; Like User link preview and Channel link preview
{:url url
:display-name display-name
:members-count members-count
:banner banner
:icon icon
:description description
;; Need to set status-link-preview?
;; to true to not send status-link-previews
;; with normal link-previews
:status-link-preview? true})
new-status-link-previews)
(concat new-previews)))
(rf/defn unfurl-parsed-urls-success
{:events [:link-preview/unfurl-parsed-urls-success]}
[{:keys [db]} request-id {new-previews :linkPreviews}]
[{:keys [db]} request-id {new-previews :linkPreviews status-link-previews :statusLinkPreviews}]
(when (= request-id (get-in db [:chat/link-previews :request-id]))
(let [new-previews (map data-store.messages/<-link-preview-rpc new-previews)
curr-previews (get-in db [:chat/link-previews :unfurled])
indexed-new-previews (utils.collection/index-by :url new-previews)]
(let [new-previews (map data-store.messages/<-link-preview-rpc new-previews)
new-status-link-previews (map data-store.messages/<-status-link-previews-rpc
status-link-previews)
merged-preview-types (merge-preview-types new-previews new-status-link-previews)
curr-previews (get-in db [:chat/link-previews :unfurled])
indexed-new-previews (utils.collection/index-by :url merged-preview-types)]
(log/debug "URLs unfurled"
:chat/link-previews
{:event :link-preview/unfurl-parsed-urls-success
:previews (map #(update % :thumbnail dissoc :data-uri) new-previews)
:previews (map #(update % :thumbnail dissoc :data-uri) merged-preview-types)
:request-id request-id})
{:db (-> db
(assoc-in [:chat/status-link-previews :unfurled] new-status-link-previews)
(update-in [:chat/link-previews :unfurled] reconcile-unfurled indexed-new-previews)
(update-in [:chat/link-previews :cache]
merge
indexed-new-previews
(utils.collection/index-by :url
(failed-previews curr-previews new-previews))))})))
(failed-previews curr-previews
merged-preview-types))))})))
(rf/defn unfurl-parsed-urls-error
{:events [:link-preview/unfurl-parsed-urls-error]}
@@ -6,6 +6,8 @@
(def url-github "https://github.com")
(def url-gitlab "https://gitlab.com")
(def url-community
"https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11")
(def preview-github {:url url-github :thumbnail nil})
(def preview-gitlab {:url url-gitlab :thumbnail nil})
(def request-id "abc123")
@@ -31,7 +33,15 @@
(is (match? {:json-rpc/call [{:method "wakuext_getTextURLs"
:params [url-github]}]}
(remove-rpc-callbacks
(events/unfurl-urls cofx url-github)))))))
(events/unfurl-urls cofx url-github))))))
(testing "fetches status community URLs"
(let [cofx {:db {:chat/link-previews {:unfurled {}
:cache {}
:request-id "123"}}}]
(is (match? {:json-rpc/call [{:method "wakuext_getTextURLs"
:params [url-community]}]}
(remove-rpc-callbacks
(events/unfurl-urls cofx url-community)))))))
(deftest unfurl-parsed-urls-test
(with-redefs [events/new-request-id (constantly request-id)]
@@ -1,8 +1,11 @@
(ns status-im.contexts.chat.messenger.composer.link-preview.view
(:require
[clojure.string :as string]
[quo.core :as quo]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[status-im.common.resources :as resources]
[status-im.constants]
[status-im.contexts.chat.messenger.composer.constants :as constants]
[status-im.contexts.chat.messenger.composer.link-preview.events]
[status-im.contexts.chat.messenger.composer.link-preview.style :as style]
@@ -33,13 +36,18 @@
:horizontal-spacing style/padding-horizontal
:loading-message (i18n/label :t/link-preview-loading-message)
:on-clear #(rf/dispatch [:link-preview/clear])
:data (map (fn [{:keys [title thumbnail hostname loading? url]}]
{:title title
:body hostname
:loading? loading?
:thumbnail (:data-uri thumbnail)
:url url})
previews)}]]))
:data (map
(fn [{:keys [title display-name thumbnail hostname loading? url favicon]}]
{:title (or display-name title)
:body (or (when-not display-name hostname)
status-im.constants/status-hostname)
:logo (if (string/starts-with? url "https://status.app")
(resources/get-mock-image :status-logo)
favicon)
:loading? loading?
:thumbnail (:data-uri thumbnail)
:url url})
previews)}]]))
(defn view
[]
@@ -210,12 +210,14 @@
:else :i/play-audio)
{:size 20
:color colors/white}]]
[:f> quo/soundtrack
{:style style/slider-container
:audio-current-time-ms progress
:player-ref (@active-players player-key)
:seeking-audio? seeking-audio?
:max-audio-duration-ms constants/audio-max-duration-ms}]
[quo/soundtrack
{:style style/slider-container
:audio-current-time-ms @progress
:set-audio-current-time-ms #(reset! progress %)
:player-ref (@active-players player-key)
:seeking-audio? @seeking-audio?
:set-seeking-audio? #(reset! seeking-audio? %)
:max-audio-duration-ms constants/audio-max-duration-ms}]
[quo/text
{:style style/timestamp
:accessibility-label :audio-duration-label
@@ -16,12 +16,13 @@
(let [previews (rf/sub [:chats/message-link-previews chat-id message-id])]
(when (seq previews)
[:<>
(for [{:keys [url title description thumbnail hostname]} previews]
(for [{:keys [url title description thumbnail hostname favicon]} previews]
^{:key url}
[quo/link-preview
{:title title
:description description
:link hostname
:logo (:url favicon)
:thumbnail (:url thumbnail)
:thumbnail-size (when (nearly-square? thumbnail) :large)
:container-style {:margin-top 8}}])])))
@@ -0,0 +1,28 @@
(ns status-im.contexts.chat.messenger.messages.content.status-link-preview.view
(:require [quo.core :as quo]
[react-native.core :as rn]
[utils.re-frame :as rf]))
(defn view
[{:keys [chat-id message-id]}]
(let [status-link-previews (rf/sub [:chats/message-status-link-previews chat-id message-id])
link-previews? (rf/sub [:chats/message-link-previews? chat-id message-id])]
(when (seq status-link-previews)
[rn/view {:style {:margin-top (when link-previews? 8)}}
(for [{:keys [url community]} status-link-previews]
(when community
(let [{community-description :description
community-icon :icon
community-banner :banner
community-name :display-name
members-count :members-count} community]
^{:key url}
[quo/internal-link-card
{:type :community
:size :message
:description community-description
:members-count members-count
:title community-name
:banner (:url community-banner)
:icon (:url community-icon)
:on-press #(rf/dispatch [:universal-links/handle-url url])}])))])))
@@ -5,6 +5,7 @@
[react-native.core :as rn]
[react-native.platform :as platform]
[status-im.contexts.chat.messenger.messages.content.link-preview.view :as link-preview]
[status-im.contexts.chat.messenger.messages.content.status-link-preview.view :as status-link-preview]
[status-im.contexts.chat.messenger.messages.content.text.style :as style]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -165,4 +166,5 @@
[message-data]
[:<>
[render-parsed-text message-data]
[link-preview/view message-data]])
[link-preview/view message-data]
[status-link-preview/view message-data]])
@@ -8,6 +8,7 @@
[legacy.status-im.data-store.chats :as data-store.chats]
[legacy.status-im.data-store.communities :as data-store.communities]
[legacy.status-im.data-store.invitations :as data-store.invitations]
[legacy.status-im.data-store.messages :as data-store.messages]
[legacy.status-im.group-chats.core :as models.group]
[legacy.status-im.multiaccounts.update.core :as update.core]
[legacy.status-im.pairing.core :as models.pairing]
@@ -356,32 +357,35 @@
(conj set-hash-fxs
#(sanitize-messages-and-process-response % response-js false)))))
(defn- image->rpc [image] (set/rename-keys image {:data-uri :dataUri}))
(defn- link-preview->rpc
[preview]
(update preview
:thumbnail
(fn [thumbnail]
(set/rename-keys thumbnail {:data-uri :dataUri}))))
(-> preview
(update :thumbnail image->rpc)
(update :favicon image->rpc)))
(defn build-message
[msg]
(-> msg
(update :link-previews #(map link-preview->rpc %))
(update :status-link-previews #(set (map data-store.messages/->status-link-previews-rpc %)))
(set/rename-keys
{:album-id :albumId
:audio-duration-ms :audioDurationMs
:audio-path :audioPath
:chat-id :chatId
:community-id :communityId
:content-type :contentType
:ens-name :ensName
:image-height :imageHeight
:image-path :imagePath
:image-width :imageWidth
:link-previews :linkPreviews
:response-to :responseTo
:sticker :sticker
:text :text})))
{:album-id :albumId
:audio-duration-ms :audioDurationMs
:audio-path :audioPath
:chat-id :chatId
:community-id :communityId
:content-type :contentType
:ens-name :ensName
:image-height :imageHeight
:image-path :imagePath
:image-width :imageWidth
:link-previews :linkPreviews
:status-link-previews :statusLinkPreviews
:response-to :responseTo
:sticker :sticker
:text :text})))
(rf/defn send-chat-messages
[_ messages]
@@ -14,6 +14,7 @@
:canManageUsers :can-manage-users?
:categoryID :category-id
:canPost :can-post?
:tokenGated :token-gated?
:isControlNode :is-control-node?
:pinMessageAllMembersEnabled :pin-message-all-members-enabled
:isMember :is-member?
+13 -20
View File
@@ -21,28 +21,21 @@
(defn handle-community
[{:keys [db]} [community-js]]
(when community-js
(let [{:keys [token-permissions
(let [{:keys [clock
token-permissions-check joined id last-opened-at]
:as community} (data-store.communities/<-rpc community-js)
has-channel-perm? (fn [id-perm-tuple]
(let [{:keys [type]} (second id-perm-tuple)]
(or (= type constants/community-token-permission-can-view-channel)
(=
type
constants/community-token-permission-can-view-and-post-channel))))
:as community} (data-store.communities/<-rpc community-js)
previous-last-opened-at (get-in db [:communities id :last-opened-at])]
{:db (assoc-in db
[:communities id]
(assoc community :last-opened-at (max last-opened-at previous-last-opened-at)))
:fx [[:dispatch [:communities/initialize-permission-addresses id]]
(when (not joined)
[:dispatch [:chat.ui/spectate-community id]])
(when (nil? token-permissions-check)
[:dispatch [:communities/check-permissions-to-join-community id]])
(when (some has-channel-perm? token-permissions)
[:dispatch [:communities/check-all-community-channels-permissions id]])
(when joined
[:dispatch [:communities/get-revealed-accounts id]])]})))
(when (> clock (get-in db [:communities id :clock]))
{:db (assoc-in db
[:communities id]
(assoc community :last-opened-at (max last-opened-at previous-last-opened-at)))
:fx [[:dispatch [:communities/initialize-permission-addresses id]]
(when (not joined)
[:dispatch [:chat.ui/spectate-community id]])
(when (nil? token-permissions-check)
[:dispatch [:communities/check-permissions-to-join-community id]])
(when joined
[:dispatch [:communities/get-revealed-accounts id]])]}))))
(rf/reg-event-fx :communities/handle-community handle-community)

Some files were not shown because too many files have changed in this diff Show More