Compare commits

...
Author SHA1 Message Date
Volodymyr Kozieiev 09e922fd05 fx namespaces removed in favor of utils.re-frame 2022-12-16 15:03:55 +00:00
Icaro Motta 269d13e2f8 Move Activity Center to new app structure (#14554) 2022-12-16 10:45:00 -03:00
Siddarth Kumar fc07fbf787 UI for mobile to mobile local pairing - updated (#14514)
* ui for local pairing

lint-fix

removed un-necessary +

addressing some of the feedback on PR

more feedback + removing feature toggle from ui

getting rid of comments/log messages over here

tidy up logs

fix typos and more i18n stuff

swap % with a named parameter

getting rid of global state + lint-fix

get rid of un-used function

icon guidelines and more kebab case stuff :>

moving stuff to events and utils namespace

:main-icons -> :i :)

address feedback and adhere to guidelines etc

fixed the :t/ qualification

moree feedback :-D

referring status-im.utils.security for now

adding "cs" to constants

make tests pass

re-frame to rf

addressing feedback

moving icons to icons2 & renaming stuff

trying to make this file the way it was before

missed out on updating these references

getting rid of the icons moved to icons2

This reverts commit be8552c0d3daaf7a7333cfeaf304d97c86d50d3e.

fixing mistakes

getting rid of the s

* this rename makes sense to me

* adding an alias to the view

* fixed broken up namespaces
2022-12-16 18:40:56 +05:30
Ajay Sivan 621e41e6ab feat: Added Dividers Date Component to quo2 (#14546)
* feat: Added Dividers Date Component  to quo2
2022-12-15 17:18:54 +01:00
Andrea Maria Piana 3e3a365ffb Enable waku2 by default 2022-12-15 12:39:49 +00:00
Ibrahem Khalil 4e6a37fdd0 Message input issues (#14450)
Message input issues (#14450)
2022-12-15 10:49:54 +01:00
Icaro Motta 08fb0de7b0 Remove old Notification Center (#14533) 2022-12-14 18:00:32 -03:00
pavloburykh f47bc6e40e e2e: new community tests 2022-12-14 10:07:46 +01:00
Jamie Caprani 6a05d43b75 chore: add some component tests (#14511) 2022-12-13 14:19:20 -08:00
Volodymyr Kozieiev 68ea7cdfc5 Communities now correctly listed in pending/joined sections. (#14515) 2022-12-13 20:55:32 +00:00
Icaro Motta 5693df5a74 Lint namespace aliases (#14526) 2022-12-13 17:04:26 -03:00
flexsurfer c7a371c104 move messages screen to status-im2 (#14530)
* move messages screen to status-im2
2022-12-13 13:27:13 +01:00
Parvesh Monu 11d02c1057 fix unread badge and add back button accessibility id (#14525) 2022-12-13 14:56:46 +05:30
Churikova Tetiana c13be92505 e2e: new activity center flow 2022-12-09 13:58:54 +01:00
Icaro Motta ae10908984 Set Activity Center as the new default (#14492) 2022-12-09 09:52:18 -03:00
Jamie Caprani 53ef1ef4b5 chore: refactor scroll page component to be more generic (#14453)
chore: refactor scroll page component to be more generic
2022-12-09 04:07:21 -08:00
yqrashawn bd84a36582 feat: new delete message (#14232) 2022-12-09 15:35:41 +08:00
Roman Volosovskyi 9be8a5b961 Add peers stats screen 2022-12-08 13:28:09 +01:00
flexsurfer 5ca46fe805 Bug/composer background fix (#14502)
* [#14385] Chat view freezes if open/close chat multiple times (iOS)
2022-12-07 17:36:26 +01:00
jakub 2f1c236333 ci: update Apple dev team ID to update the org
We are trasnferring the Apple app to the new Switzerland based Apple
development organization/team from the old Singapore one, and to make
this work we also need to update the development team ID.

This should fix the following failure:
```
Could not find App ID with bundle identifier 'im.status.ethereum'
You can easily generate a new App ID on the Developer Portal using 'produce':
```
https://ci.infra.status.im/job/status-mobile/job/platforms/job/ios/854/console

Signed-off-by: Jakub Sokołowski <jakub@status.im>
2022-12-07 10:45:14 +01:00
Yevheniia Berdnyk f2fa3a1886 Fixed test_group_chat_pin_messages and moved to PR tests 2022-12-06 23:16:55 +02:00
Parvesh Monu e6628bd346 fix app crashing on opening community channel (#14499) 2022-12-06 23:16:28 +05:30
Icaro Motta 6e272a96c8 Introduce subscription tests (#14472) 2022-12-06 13:36:05 -03:00
flexsurfer da0f0d3a81 move messages main screen to status-im2 (#14491)
* move messages main screen to status-im2
2022-12-05 14:22:06 +01:00
frank f7af7ca25d Allow owner/admin to delete messages of a community (#14366) 2022-12-02 20:13:02 +08:00
Christoph Pader f3ad8beb6d fix: channel list-item not clickable (#14469) 2022-12-02 11:45:26 +01:00
yqrashawn 2e68fb22f5 feat: add display-name in author component (#14473) 2022-12-02 14:42:16 +08:00
Christoph Pader f18044c9dc Implement Token Gating (#13899)
* feat: implement token gating component

* fix: add .calva to gitignore
2022-12-01 13:30:07 +00:00
Jamie Caprani 5947769881 chore: update action drawer with designer feedback (#14465) 2022-12-01 03:58:50 -08:00
Parvesh Monu d4897de205 partially implement shell jump-to navigation (#14410) 2022-11-30 21:46:01 +05:30
Jamie Caprani d948939ce3 fix: adjust community cards on discover page (#14444)
fix: adjust community cards on discover page
2022-11-30 01:58:44 -08:00
Yevheniia Berdnyk 1000cb79a2 Creating users and getting names in parallel 2022-11-29 18:09:39 +02:00
Mohamed Javid f55b646c20 [Chore] Use common for repeated code (#14454)
* [Chore] Use common for repeated code

* [Chore] Removed empty file
2022-11-29 22:47:35 +08:00
Mohamed Javid bc7578ae85 [Feature] Added Mentions in Activity Center (#14451)
* [Feature][#14352] Added mentions in Activity Center

* [Update][#14352] Update in namespace

* [Update][#14352] Code Style

* [Update][#14352] Created commons for AC and fix warning on text
2022-11-29 21:55:33 +08:00
Omar Basem 49e9738ff8 Group Details Screen (2) (#14427)
* feat: group details screen (2)
2022-11-29 10:41:19 +04:00
Brian Sztamfater 9e1412007f fix: edit message
Signed-off-by: Brian Sztamfater <brian@status.im>
2022-11-28 10:21:50 -03:00
Icaro Motta c198133769 Update guidelines (#14445) 2022-11-28 09:58:36 -03:00
Jamie Caprani 6d5ff4aa92 chore: fix issues with e2e tests after refactors (#14440) 2022-11-28 03:17:12 -08:00
Jamie Caprani 0e614d51ef feat: add scrolling in community (#14281) 2022-11-28 02:24:31 -08:00
Icaro Motta 42de2a6384 Mark notifications as read based on call to action (#14439) 2022-11-25 16:10:29 -03:00
Ibrahem Khalil 3d71fdba98 [14429] Fix mentions (#14442) 2022-11-25 15:04:15 +02:00
Jamie Caprani 9de4417f03 chore: update nix config to reference babel.config.js file (#14436)
* chore: update nix config to reference babel.config.js file

* chore: update component test make command
2022-11-24 13:05:58 -08:00
Parvesh Monu 9e7db45d08 fix everyone is displaying as online in community channel and chat list (#14431)
also fixes issue with visibility status popover not opening
2022-11-24 19:45:27 +05:30
Omar Basem 14c243803f Photo Selector (1) (#14426)
* feat: photo-selector (1)
2022-11-24 15:29:54 +04:00
jakub e61889f16c ci: add missing suffix to android e2e builds
Fix for same naming of e2e and universal builds.

Signed-off-by: Jakub Sokołowski <jakub@status.im>
2022-11-24 10:23:28 +01:00
Parvesh Monu 1230594046 test build fix (#14435) 2022-11-24 14:50:57 +05:30
jakub 3fc1ba357e ci: bump jenkins lib to match desktop
Related to: https://github.com/status-im/status-desktop/pull/8420
Depends on: https://github.com/status-im/status-jenkins-lib/pull/53

Signed-off-by: Jakub Sokołowski <jakub@status.im>
2022-11-23 19:48:55 +01:00
flexsurfer 3152cf0aa3 adjust navigation colors and small bugfixes (#14403)
* adjust navigation colors and small bugfixes
2022-11-23 17:30:11 +01:00
Jamie Caprani c6e8aad745 feat: add component tests using react-testing-library and jest (#14331) 2022-11-23 05:59:18 -08:00
flexsurfer bab0fc7ac0 move chats home (#14419)
* move chats home
2022-11-23 14:33:40 +01:00
frank 32d85d5059 Allow styling text in composer when selecting it with native actions (#14249) 2022-11-23 10:28:44 +08:00
Churikova Tetiana a9295ac17e e2e: test sections and fix 2022-11-22 17:44:28 +01:00
jakub 7dccbf2395 ci: separate buckets for mobile and desktop builds
Possible fix for slow upload speeds and failures caused by most probably
hitting per-bucket rate limits of DigitalOcean:

>- 500 total operations per second to any individual bucket.
>- 300 combined PUT, POST, COPY, DELETE, and LIST operations per second to any individual Space. We may further limit LIST operations if necessary under periods of high load.y

https://docs.digitalocean.com/products/spaces/details/limits/#rate-limits

Depends on: https://github.com/status-im/status-jenkins-lib/pull/52

Signed-off-by: Jakub Sokołowski <jakub@status.im>
2022-11-22 16:55:59 +01:00
Brian Sztamfater 4a87122336 feat: author component
Signed-off-by: Brian Sztamfater <brian@status.im>
2022-11-21 15:46:56 -03:00
Roman Volosovskyi d18c3ad102 [#14381] Wrap texts with view to ensure separate accessibility labels 2022-11-21 15:40:29 +01:00
Icaro Motta e53f58d8c0 Fix various Activity Log layout issues (#14405) 2022-11-21 11:19:50 -03:00
Omar Basem 5492fb472f Group Details Screen (#14377)
* feat: group details screen
2022-11-21 16:03:49 +04:00
flexsurfer c08d58577d move quo preview (#14409) 2022-11-21 11:03:07 +01:00
Yevheniia Berdnyk 946640339a e2e: pin messages in 1-1 and group chats 2022-11-21 06:49:53 +02:00
Jamie CapraniandErik Seppanen 72d43ba745 Visual tests setup (#14329)
feat: configuration setup for visual tests

Co-authored-by: Erik Seppanen <esep@protonmail.com>
2022-11-20 15:46:04 -08:00
Icaro Motta f4d9162ff8 Fix reconciliation for new filter type :all (#14401) 2022-11-18 06:15:39 -03:00
Omar Basem 73bbfa7d31 Fix Metro Warnings ⚠️ (#14389)
* fix: metro warnings ⚠️
2022-11-17 21:19:54 +04:00
Mohamed Javid 84da3128bf [Fix] Activity Centre UI bug (#14398)
* [Fix] Activity Centre UI bug

* [Fix] Namespace update for config

* [Chore] Moved navigation to Activity Center as reframe event

* [Chore] Code Format
2022-11-18 00:15:49 +08:00
flexsurfer 3777ebc36e move communities 2 (#14394) 2022-11-17 16:43:02 +01:00
Roman Volosovskyi 4c6b4e2d02 [#14388] Re-enable bug reporting 2022-11-17 15:01:45 +01:00
Roman Volosovskyi e469d97702 [#14372] Fix message content type on editing 2022-11-17 08:23:53 +01:00
Roman Volosovskyi cf753a0892 [#14381] Add accessibility lables for chat key and timestamp in message 2022-11-17 07:16:30 +01:00
Icaro Motta 0f7ccce3df Implement identity verification flow (#14365) 2022-11-16 16:46:04 -03:00
Mohamed Javid 509ffc2a01 [Feature] Added messages skeleton (#14072)
* [Feature][#14025] Added Messages Skeleton while loading messages

* [Improvements][#14025] Added animation to message skeleton

* [Improvements][#14025] Changed to Reanimated2 for message skeleton animation

* [Chores][#14025] Removed unused code

* [Improvements][#14025] Added preview screen for messages skeleton

* [Improvements][#14025] Style update for messages skeleton

* [Chore][#14025] Indentation for message skeleton

* [Improvements][#14025] On Layout calculation cleanup

* [Fix][#14025] Added Skeleton on New UI only

* [Chore] Moved Message Skeleton to new UI

* [Feature][#14025] Added Message Skeleton on message gap

* [TEMP][#14025] Added delay of preloading messages for testing

* [Lint][#14025] Lint fixes for Message Gap component

* [Chore][#14025] Reanimated namespace update

* [Chore][#14025] React native namespace update

* [Chore][#14025] Rollback preload messages
2022-11-17 00:43:58 +08:00
John Ngei e44d4c83c0 communites home screen context actions (#14371) 2022-11-16 17:48:18 +03:00
flexsurfer cc733a379c move communities (#14384) 2022-11-16 14:09:16 +01:00
flexsurfer 803328dc4c move switcher (#14378)
* move switcher
2022-11-16 12:00:16 +01:00
Roman Volosovskyi 255049c312 Fix uniq :key warning on opening chat 2022-11-16 10:46:32 +01:00
flexsurfer 31b6e076be new structure continue, move utils, move fx macro to re-frame utils n… (#14373)
* new structure continue, move utils, move fx macro to re-frame utils namespace
2022-11-16 09:09:25 +01:00
499 changed files with 14873 additions and 8483 deletions
-8
View File
@@ -1,8 +0,0 @@
{
"presets": [
"module:metro-react-native-babel-preset"
],
"plugins": [
"react-native-reanimated/plugin"
]
}
+1 -1
View File
@@ -200,7 +200,7 @@ status-im.utils.platform/isMacOs?
status-im.utils.platform/isNix?
status-im.utils.platform/isWin?
status-im.utils.platform/android-version>=
status-im.utils.debounce/clear-all
utils.debounce/clear-all
status-im.transport.db/create-chat
status-im.utils.priority-map/priority-map
status-im.utils.priority-map/priority-map-by
+20 -13
View File
@@ -1,13 +1,20 @@
{:lint-as {status-im.utils.views/defview clojure.core/defn
status-im.utils.views/letsubs clojure.core/let
reagent.core/with-let clojure.core/let
status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all
quo.react/with-deps-check clojure.core/fn
quo.previews.preview/list-comp clojure.core/for
status-im.utils.styles/def clojure.core/def
status-im.utils.styles/defn clojure.core/defn
taoensso.tufte/defnp clojure.core/defn}
:linters {:invalid-arity {:skip-args [status-im.utils.fx/defn]}
;;TODO remove number when this is fixed
;;https://github.com/borkdude/clj-kondo/issues/867
:unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY number]}}}
{:lint-as {status-im.utils.views/defview clojure.core/defn
status-im.utils.views/letsubs clojure.core/let
reagent.core/with-let clojure.core/let
status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all
utils.re-frame/defn clj-kondo.lint-as/def-catch-all
quo.react/with-deps-check clojure.core/fn
quo.previews.preview/list-comp clojure.core/for
status-im.utils.styles/def clojure.core/def
status-im.utils.styles/defn clojure.core/defn
status-im.test-helpers/deftest-sub clojure.core/defn
taoensso.tufte/defnp clojure.core/defn}
:linters {:consistent-alias {:level :error
:aliases {clojure.string string
taoensso.timbre log}}
:invalid-arity {:skip-args [status-im.utils.fx/defn utils.re-frame/defn]}
;; TODO remove number when this is fixed
;; https://github.com/borkdude/clj-kondo/issues/867
:unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY
number
status-im.test-helpers/restore-app-db]}}}
+37
View File
@@ -0,0 +1,37 @@
module.exports = {
"testRunner": "jest",
"testRegex": "\\.visual\\.js$",
"runner-config": "visual-test/config.json",
"devices": {
"simulator": {
"type": "ios.simulator",
"device": {
"type": "iPhone 11 Pro"
}
}
},
"apps": {
"ios.release": {
"name": "StatusIm",
"type": "ios.app",
"binaryPath": "ios/build/Build/Products/Release-iphonesimulator/StatusIm.app",
"build": "make release-ios"
},
"ios.debug": {
"name": "StatusIm",
"type": "ios.app",
"binaryPath": process.env.TEST_BINARY_PATH,
"build": "make run-ios SIMULATOR='iPhone 11 Pro'"
}
},
"configurations": {
"ios.sim.release": {
"device": "simulator",
"app": "ios.release"
},
"ios.sim.debug": {
"device": "simulator",
"app": "ios.debug"
}
}
}
+2 -1
View File
@@ -4,7 +4,7 @@ DEFAULT_NETWORK=mainnet_rpc
DEV_BUILD=1
ETHEREUM_DEV_CLUSTER=1
EXTENSIONS=0
FLEET=eth.prod
FLEET=status.prod
GROUP_CHATS_ENABLED=1
LOG_LEVEL=info
MAILSERVER_CONFIRMATIONS_ENABLED=1
@@ -32,3 +32,4 @@ COMMANDS_ENABLED=1
TWO_MINUTES_SYNCING=1
SWAP_ENABLED=1
STICKERS_TEST_ENABLED=1
LOCAL_PAIRING_ENABLED=1
+2 -1
View File
@@ -2,7 +2,7 @@ DEBUG_WEBVIEW=1
DEFAULT_NETWORK=goerli_rpc
ETHEREUM_DEV_CLUSTER=1
EXTENSIONS=0
FLEET=eth.staging
FLEET=status.prod
GROUP_CHATS_ENABLED=1
LOG_LEVEL=debug
MAILSERVER_CONFIRMATIONS_ENABLED=0
@@ -32,3 +32,4 @@ COMMUNITIES_MANAGEMENT_ENABLED=1
DELETE_MESSAGE_ENABLED=1
TWO_MINUTES_SYNCING=1
STICKERS_TEST_ENABLED=1
LOCAL_PAIRING_ENABLED=1
+2 -1
View File
@@ -3,7 +3,7 @@ DEBUG_WEBVIEW=1
DEFAULT_NETWORK=goerli_rpc
ETHEREUM_DEV_CLUSTER=1
EXTENSIONS=0
FLEET=eth.prod
FLEET=status.prod
GROUP_CHATS_ENABLED=1
LOG_LEVEL=debug
MAILSERVER_CONFIRMATIONS_ENABLED=1
@@ -34,3 +34,4 @@ DELETE_MESSAGE_ENABLED=1
TWO_MINUTES_SYNCING=1
ENABLE_QUO_PREVIEW=1
STICKERS_TEST_ENABLED=1
LOCAL_PAIRING_ENABLED=1
+1 -1
View File
@@ -2,7 +2,7 @@ DEBUG_WEBVIEW=1
DEFAULT_NETWORK=mainnet_rpc
ETHEREUM_DEV_CLUSTER=1
EXTENSIONS=0
FLEET=eth.prod
FLEET=status.prod
GROUP_CHATS_ENABLED=1
LOG_LEVEL=info
MAILSERVER_CONFIRMATIONS_ENABLED=1
+1 -1
View File
@@ -2,7 +2,7 @@ DEBUG_WEBVIEW=0
DEFAULT_NETWORK=mainnet_rpc
ETHEREUM_DEV_CLUSTER=0
EXTENSIONS=0
FLEET=eth.prod
FLEET=status.prod
GROUP_CHATS_ENABLED=1
LOG_LEVEL=
MAILSERVER_CONFIRMATIONS_ENABLED=1
+9
View File
@@ -7,6 +7,7 @@
# Xcode
#
/component-spec
result/
build/
*.pbxuser
@@ -175,3 +176,11 @@ test/appium/tests/users.py
##node bindings
/bin/
/lib/
## visual tests
/artifacts
/.calva/
## component-tests
*.log
+26 -3
View File
@@ -48,7 +48,7 @@ export _NIX_GCROOTS = /nix/var/nix/gcroots/per-user/$(USER)/status-mobile
# Defines which variables will be kept for Nix pure shell, use semicolon as divider
export _NIX_KEEP ?= TMPDIR,BUILD_ENV,STATUS_GO_SRC_OVERRIDE
# Useful for Andoird release builds
# Useful for Android release builds
TMP_BUILD_NUMBER := $(shell ./scripts/version/gen_build_no.sh | cut -c1-10)
# MacOS root is read-only, read nix/README.md for details
@@ -307,13 +307,36 @@ test-watch: ##@ Watch tests and re-run no changes to cljs files
test: export TARGET := clojure
test: ##@test Run tests once in NodeJS
# Here we creates the gyp bindings for nodejs
# Here we create the gyp bindings for nodejs
yarn install
yarn shadow-cljs compile mocks && \
yarn shadow-cljs compile test && \
node --require ./test-resources/override.js target/test/test.js
#--------------
run-visual-test-ios: export TARGET := clojure
run-visual-test-ios: XCODE_DERIVED_DATA := $(HOME)/Library/Developer/Xcode/DerivedData
run-visual-test-ios: APPLICATION_NAME := StatusIm-brfnruzfrkkycpbndmdoeyrigthc
run-visual-test-ios: export TEST_BINARY_PATH := $(XCODE_DERIVED_DATA)/$(APPLICATION_NAME)/Build/Products/Debug-iphonesimulator/StatusIm.app
run-visual-test-ios: ##@test Run tests once in NodeJS
detox test --configuration ios.sim.debug
component-test-watch: export TARGET := clojure
component-test-watch: export COMPONENT_TEST := true
component-test-watch: export BABEL_ENV := test
component-test-watch: ##@ Watch tests and re-run no changes to cljs files
yarn install
nodemon --exec 'yarn shadow-cljs compile component-test && jest --config=test/jest/jest.config.js' -e cljs
component-test: export TARGET := clojure
component-test: export COMPONENT_TEST := true
component-test: export BABEL_ENV := test
component-test: ##@test Run tests once in NodeJS
# Here we create the gyp bindings for nodejs
yarn install
yarn shadow-cljs compile component-test && \
jest --config=test/jest/jest.config.js
# Other
#--------------
+24
View File
@@ -0,0 +1,24 @@
module.exports = {
"presets": [
"module:metro-react-native-babel-preset"
],
"plugins": [
"react-native-reanimated/plugin"
],
"env": {
"test": {
"presets": [
'@babel/preset-react',
[
'@babel/preset-env',
{
targets: {
node: '14',
},
},
],
],
}
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
@@ -36,7 +36,7 @@ pipeline {
LANG = "en_US.UTF-8"
LC_ALL = "en_US.UTF-8"
LANGUAGE = "en_US.UTF-8"
TARGET = 'android'
TARGET = "android${utils.isE2EBuild() ? "-e2e" : ""}"
BUILD_ENV = 'prod'
NIX_CONF_DIR = "${env.WORKSPACE}/nix"
FASTLANE_DISABLE_COLORS = 1
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
pipeline {
agent { label 'linux' }
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
pipeline {
agent { label params.AGENT_LABEL }
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
+1 -1
View File
@@ -62,7 +62,7 @@ pipeline {
--rerun_count=2 \
--testrail_report=True \
-m testrail_id \
-m \"new_ui_critical\" \
-m \"new_ui_critical or new_ui_medium\" \
-k \"${params.KEYWORD_EXPRESSION}\" \
--apk=${params.APK_NAME}
"""
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
pipeline {
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
pipeline {
agent { label 'macos' }
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.6.1'
library 'status-jenkins-lib@v1.6.3'
pipeline {
agent { label 'linux' }
+1 -1
View File
@@ -93,6 +93,6 @@ To use a physical iPhone your device UDID must be added to provisioning profiles
- Invite your Apple account to be Developer in Status team.
3. Run a build in XCode using the project from `status-mobile/ios` directory.
- You might see error: `Select a development team in the Signing & Capabilities editor`
- Select `STATUS HOLDINGS PTE. LTD.` as the development team and rebuild again.
- Select `Status Research & Development GmbH` as the development team and rebuild again.
Once build finishes Status should start on your iPhone with its logs in terminal running `make run-metro`.
+88
View File
@@ -62,6 +62,31 @@ with the source file using it. For a real example, see
(do-something)]])
```
#### Styles def vs defn
Always use `def` over `defn`, unless the style relies on dynamic values, such as
deref'ed atoms.
```clojure
;; bad
(defn title-column []
{:height 56})
;; good
(def title-column
{:height 56})
```
```clojure
;; bad
(def community-card
{:background-color (colors/theme-colors colors/white colors/neutral-90)})
;; good
(defn community-card []
{:background-color (colors/theme-colors colors/white colors/neutral-90)})
```
### Using TODOs comments
_TODO_ comments are used extensively in the codebase, but prefer to use them
@@ -249,6 +274,22 @@ test macros `deftest` and `is`, which are ubiquitous in the Clojure community.
(:require [cljs-time.coerce :as time.coerce]))
```
### Javascript interop
Use [binaryage/oops](https://github.com/binaryage/cljs-oops) macros instead of
core interop macros.
```clojure
;; bad
(fn [^js event]
(.-width (.-nativeEvent event)))
;; good
(require '[oops.core :as oops])
(fn [event]
(oops/oget event "nativeEvent.width"))
```
### Accessibility labels
Use keywords instead of strings. As a bonus, remember keywords are cached in
@@ -296,6 +337,53 @@ keywords and concatenating them into a single string.
(i18n/label :t/biometric-auth-error {:code error-code})
```
### Tests
#### Subscription tests
Test [layer-3 subscriptions](https://day8.github.io/re-frame/subscriptions/) by
actually subscribing to them, so reframe's signal graph gets validated too.
```clojure
;; bad
(defn user-recipes
[[current-user all-recipes location]]
...)
(re-frame/reg-sub
:user/recipes
:<- [:current-user]
:<- [:all-recipes]
:<- [:location]
user-recipes)
(deftest user-recipes-test
(testing "builds list of recipes"
(let [current-user {...}
all-recipes {...}
location [...]]
(is (= expected (recipes [current-user all-recipes location]))))))
;; good
(require '[status-im.test-helpers :as h])
(re-frame/reg-sub
:user/recipes
:<- [:current-user]
:<- [:all-recipes]
:<- [:location]
(fn [[current-user all-recipes location]]
...))
(h/deftest-sub :user/recipes
[sub-name]
(testing "builds list of recipes"
(swap! rf-db/app-db assoc
:current-user {...}
:all-recipes {...}
:location [...])
(is (= expected (rf/sub [sub-name])))))
```
## Project Structure
First, the bird's-eye view with some example ClojureScript files:
+46 -23
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
@@ -319,7 +319,6 @@
FBF6AB84EBC2085D28C3CDEF /* Pods-Status-StatusImPR.debug.xcconfig */,
BF23313BEFBC84C6BB7E7677 /* Pods-Status-StatusImPR.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -401,12 +400,12 @@
TargetAttributes = {
00E356ED1AD99517003FC87E = {
CreatedOnToolsVersion = 6.2;
DevelopmentTeam = DTX7Z4U3YA;
DevelopmentTeam = 8B5X2M6H2Y;
ProvisioningStyle = Manual;
TestTargetID = 13B07F861A680F5B00A75B9A;
};
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = DTX7Z4U3YA;
DevelopmentTeam = 8B5X2M6H2Y;
LastSwiftMigration = 1140;
ProvisioningStyle = Manual;
SystemCapabilities = {
@@ -425,7 +424,7 @@
};
};
3AAD2AB924A3A60E0075D594 = {
DevelopmentTeam = DTX7Z4U3YA;
DevelopmentTeam = 8B5X2M6H2Y;
ProvisioningStyle = Manual;
};
};
@@ -757,7 +756,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
@@ -768,7 +767,11 @@
);
INFOPLIST_FILE = StatusImTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -791,14 +794,18 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution";
COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
);
INFOPLIST_FILE = StatusImTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -818,12 +825,12 @@
BUNDLE_ID_SUFFIX = .debug;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = StatusIm/StatusIm.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CUSTOM_PRODUCT_NAME = "Status Debug";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
EXCLUDED_ARCHS = "";
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -857,7 +864,11 @@
);
INFOPLIST_FILE = StatusIm/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
@@ -888,12 +899,12 @@
BUNDLE_ID_SUFFIX = "";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = StatusIm/StatusIm.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CUSTOM_PRODUCT_NAME = Status;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
EXCLUDED_ARCHS = "";
FRAMEWORK_SEARCH_PATHS = (
"$(PROJECT_DIR)/../modules/react-native-status/ios/RCTStatus",
@@ -919,7 +930,11 @@
);
INFOPLIST_FILE = StatusIm/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
@@ -953,7 +968,7 @@
CODE_SIGN_STYLE = Manual;
CUSTOM_PRODUCT_NAME = "Status Debug";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
FRAMEWORK_SEARCH_PATHS = (
"$(PROJECT_DIR)/../modules/react-native-status/ios/RCTStatus",
"$(inherited)",
@@ -984,7 +999,11 @@
);
INFOPLIST_FILE = StatusImPR/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
@@ -996,7 +1015,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum.pr;
PRODUCT_NAME = "Status PR";
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc im.status.ethereum.pr";
PROVISIONING_PROFILE_SPECIFIER = "match Development im.status.ethereum.pr";
SWIFT_OBJC_BRIDGING_HEADER = "StatusIm-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -1013,12 +1032,12 @@
BUNDLE_ID_SUFFIX = "";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = StatusImPR/StatusImPR.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CUSTOM_PRODUCT_NAME = Status;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
FRAMEWORK_SEARCH_PATHS = (
"$(PROJECT_DIR)/../modules/react-native-status/ios/RCTStatus",
"$(inherited)",
@@ -1043,7 +1062,11 @@
);
INFOPLIST_FILE = StatusImPR/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
@@ -1096,7 +1119,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -1179,7 +1202,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = YES;
DEVELOPMENT_TEAM = DTX7Z4U3YA;
DEVELOPMENT_TEAM = 8B5X2M6H2Y;
ENABLE_BITCODE = NO;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -0,0 +1,78 @@
package im.status.ethereum.module;
import android.view.ActionMode;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.views.textinput.ReactEditText;
import javax.annotation.Nonnull;
class RNSelectableTextInputModule extends ReactContextBaseJavaModule {
private ActionMode lastActionMode;
public RNSelectableTextInputModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Nonnull
@Override
public String getName() {
return "RNSelectableTextInputManager";
}
@ReactMethod
public void setupMenuItems(final Integer selectableTextViewReactTag, final Integer textInputReactTag) {
ReactApplicationContext reactContext = this.getReactApplicationContext();
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new UIBlock() {
public void execute (NativeViewHierarchyManager nvhm) {
RNSelectableTextInputViewManager rnSelectableTextManager = (RNSelectableTextInputViewManager) nvhm.resolveViewManager(selectableTextViewReactTag);
ReactEditText reactTextView = (ReactEditText) nvhm.resolveView(textInputReactTag);
rnSelectableTextManager.registerSelectionListener(reactTextView);
}
});
}
@ReactMethod
public void startActionMode(final Integer textInputReactTag) {
ReactApplicationContext reactContext = this.getReactApplicationContext();
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new UIBlock() {
public void execute (NativeViewHierarchyManager nvhm) {
ReactEditText reactTextView = (ReactEditText) nvhm.resolveView(textInputReactTag);
lastActionMode = reactTextView.startActionMode(reactTextView.getCustomSelectionActionModeCallback(), ActionMode.TYPE_FLOATING);
}
});
}
@ReactMethod
public void hideLastActionMode(){
ReactApplicationContext reactContext = this.getReactApplicationContext();
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new UIBlock() {
public void execute (NativeViewHierarchyManager nvhm) {
if(lastActionMode!=null){
lastActionMode.finish();
lastActionMode = null;
}
}
});
}
@ReactMethod
public void setSelection(final Integer textInputReactTag, final Integer start, final Integer end){
ReactApplicationContext reactContext = this.getReactApplicationContext();
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new UIBlock() {
public void execute (NativeViewHierarchyManager nvhm) {
ReactEditText reactTextView = (ReactEditText) nvhm.resolveView(textInputReactTag);
reactTextView.setSelection(start, end);
}
});
}
}
@@ -0,0 +1,102 @@
package im.status.ethereum.module;
import android.view.ActionMode;
import android.view.ActionMode.Callback;
import android.view.Menu;
import android.view.MenuItem;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.textinput.ReactEditText;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.views.view.ReactViewManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class RNSelectableTextInputViewManager extends ReactViewManager {
public static final String REACT_CLASS = "RNSelectableTextInput";
private String[] _menuItems = new String[0];
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public ReactViewGroup createViewInstance(ThemedReactContext context) {
return new ReactViewGroup(context);
}
@ReactProp(name = "menuItems")
public void setMenuItems(ReactViewGroup reactViewGroup, ReadableArray items) {
if(items != null) {
List<String> result = new ArrayList<String>(items.size());
for (int i = 0; i < items.size(); i++) {
result.add(items.getString(i));
}
this._menuItems = result.toArray(new String[items.size()]);
}
}
public void registerSelectionListener(final ReactEditText view) {
view.setCustomSelectionActionModeCallback(new Callback() {
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
menu.clear();
for (int i = 0; i < _menuItems.length; i++) {
menu.add(0, i, 0, _menuItems[i]);
}
return true;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
int selectionStart = view.getSelectionStart();
int selectionEnd = view.getSelectionEnd();
String selectedText = view.getText().toString().substring(selectionStart, selectionEnd);
// Dispatch event
onSelectNativeEvent(view, item.getItemId(), selectedText, selectionStart, selectionEnd);
mode.finish();
return true;
}
});
}
public void onSelectNativeEvent(ReactEditText view, int eventType, String content, int selectionStart, int selectionEnd) {
WritableMap event = Arguments.createMap();
event.putInt("eventType", eventType);
event.putString("content", content);
event.putInt("selectionStart", selectionStart);
event.putInt("selectionEnd", selectionEnd);
// Dispatch
ReactContext reactContext = (ReactContext) view.getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(view.getId(), "topSelection", event);
}
@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.builder()
.put("topSelection", MapBuilder.of("registrationName","onSelection"))
.build();
}
}
@@ -940,6 +940,52 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
StatusThreadPoolExecutor.getInstance().execute(r);
}
@ReactMethod
public void getConnectionStringForBootstrappingAnotherDevice(final String configJSON, final Callback callback) throws JSONException {
final JSONObject jsonConfig = new JSONObject(configJSON);
final String keyUID = jsonConfig.getString("keyUID");
final String keyStorePath = this.getKeyStorePath(keyUID);
jsonConfig.put("keystorePath", keyStorePath);
if (!checkAvailability()) {
callback.invoke(false);
return;
}
Runnable runnableTask = new Runnable() {
@Override
public void run() {
String res = Statusgo.getConnectionStringForBootstrappingAnotherDevice(jsonConfig.toString());
callback.invoke(res);
}
};
StatusThreadPoolExecutor.getInstance().execute(runnableTask);
}
@ReactMethod
public void inputConnectionStringForBootstrapping(final String connectionString, final String configJSON, final Callback callback) throws JSONException {
final JSONObject jsonConfig = new JSONObject(configJSON);
final String keyStorePath = pathCombine(this.getNoBackupDirectory(), "/keystore");
jsonConfig.put("keystorePath", keyStorePath);
if (!checkAvailability()) {
callback.invoke(false);
return;
}
Runnable runnableTask = new Runnable() {
@Override
public void run() {
String res = Statusgo.inputConnectionStringForBootstrapping(connectionString,jsonConfig.toString());
callback.invoke(res);
}
};
StatusThreadPoolExecutor.getInstance().execute(runnableTask);
}
@ReactMethod
public void hashTypedData(final String data, final Callback callback) {
Log.d(TAG, "hashTypedData");
@@ -7,6 +7,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -29,12 +30,15 @@ public class StatusPackage implements ReactPackage {
List<NativeModule> modules = new ArrayList<>();
modules.add(new StatusModule(reactContext, this.rootedDevice));
modules.add(new RNSelectableTextInputModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
return Arrays.<ViewManager>asList(
new RNSelectableTextInputViewManager()
);
}
}
@@ -318,6 +318,42 @@ RCT_EXPORT_METHOD(hashMessage:(NSString *)message
callback(@[result]);
}
//////////////////////////////////////////////////////////////////// getConnectionStringForBootstrappingAnotherDevice
RCT_EXPORT_METHOD(getConnectionStringForBootstrappingAnotherDevice:(NSString *)configJSON
callback:(RCTResponseSenderBlock)callback) {
NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:nil];
NSString *keyUID = [configDict objectForKey:@"keyUID"];
NSURL *multiaccountKeystoreDir = [self getKeyStoreDir:keyUID];
NSString *keystoreDir = multiaccountKeystoreDir.path;
[configDict setValue:keystoreDir forKey:@"keystorePath"];
NSString *modifiedConfigJSON = [configDict bv_jsonStringWithPrettyPrint:NO];
NSString *result = StatusgoGetConnectionStringForBootstrappingAnotherDevice(modifiedConfigJSON);
callback(@[result]);
}
//////////////////////////////////////////////////////////////////// inputConnectionStringForBootstrapping
RCT_EXPORT_METHOD(inputConnectionStringForBootstrapping:(NSString *)cs
configJSON:(NSString *)configJSON
callback:(RCTResponseSenderBlock)callback) {
NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:nil];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *rootUrl =[[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *multiaccountKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"];
NSString *keystoreDir = multiaccountKeystoreDir.path;
[configDict setValue:keystoreDir forKey:@"keystorePath"];
NSString *modifiedConfigJSON = [configDict bv_jsonStringWithPrettyPrint:NO];
NSString *result = StatusgoInputConnectionStringForBootstrapping(cs,modifiedConfigJSON);
callback(@[result]);
}
//////////////////////////////////////////////////////////////////// hashTypedData
RCT_EXPORT_METHOD(hashTypedData:(NSString *)data
callback:(RCTResponseSenderBlock)callback) {
@@ -502,11 +538,11 @@ RCT_EXPORT_METHOD(saveAccountAndLoginWithKeycard:(NSString *)multiaccountData
- (NSString *) getExportDbFilePath {
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"export.db"];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:filePath]) {
[fileManager removeItemAtPath:filePath error:nil];
}
return filePath;
}
@@ -957,10 +993,10 @@ RCT_EXPORT_METHOD(exportUnencryptedDatabase:(NSString *)accountData
#if DEBUG
NSLog(@"exportUnencryptedDatabase() method called");
#endif
NSString *filePath = [self getExportDbFilePath];
StatusgoExportUnencryptedDatabase(accountData, password, filePath);
callback(@[filePath]);
}
+2 -2
View File
@@ -45,8 +45,8 @@ in stdenv.mkDerivation rec {
filter = lib.mkFilter {
root = path;
include = [
"package.json" "yarn.lock" "metro.config.js" ".babelrc"
"resources/.*" "translations/.*" "src/js/worklet_factory.js"
"package.json" "yarn.lock" "metro.config.js" "babel.config.js"
"resources/.*" "translations/.*" "src/js/.*"
"modules/react-native-status/android.*" "android/.*"
envFileName "VERSION" "status-go-version.json" "react-native.config.js"
];
+10 -1
View File
@@ -74,7 +74,7 @@
"react-native-touch-id": "^4.4.1",
"react-native-webview": "git+https://github.com/status-im/react-native-webview.git#refs/tags/v11.16.0-status",
"react-syntax-highlighter": "^15.5.0",
"rn-emoji-keyboard": "git+https://github.com/status-im/rn-emoji-keyboard.git#refs/tags/v0.4.3",
"rn-emoji-keyboard": "0.7.0",
"tdigest": "^0.1.1"
},
"devDependencies": {
@@ -82,12 +82,21 @@
"@babel/helper-builder-react-jsx": "7.0.0",
"@babel/plugin-transform-block-scoping": "7.0.0",
"@babel/preset-env": "7.1.0",
"@babel/preset-react": "^7.18.6",
"@babel/register": "7.0.0",
"@jest/globals": "^25.1.0",
"@mapbox/node-pre-gyp": "^1.0.9",
"@testing-library/jest-native": "^5.0.0",
"@testing-library/react-native": "^11.2.0",
"@types/jest": "^28.1.6",
"detox": "^19.9.1",
"jest": "^25.1.0",
"jest-circus": "^26.0.0",
"jest-image-snapshot": "^5.1.0",
"nodemon": "^2.0.16",
"nyc": "^14.1.1",
"process": "0.11.10",
"react-test-renderer": "16.13.1",
"rn-snoopy": "git+https://github.com/status-im/rn-snoopy.git#refs/tags/v2.0.2-status",
"shadow-cljs": "2.11.16"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

+2 -2
View File
@@ -2,10 +2,10 @@
CHANGES=$(git diff --no-ext-diff --diff-filter=d --cached --unified=0 --no-prefix --minimal --exit-code src/quo src/quo2 | grep '^+' || echo '')
INVALID_CHANGES=$(echo "$CHANGES" | grep -E '(re-frame/dispatch|rf/dispatch|re-frame/subscribe|rf/subscribe|rf/sub|<sub|>evt)')
INVALID_CHANGES=$(echo "$CHANGES" | grep -E '(re-frame/dispatch|rf/dispatch|re-frame/subscribe|rf/subscribe|rf/sub|<sub|>evt|status-im)')
if test -n "$INVALID_CHANGES"; then
echo "re-frame dispatch/subscribe is not allowed in quo/quo2 components"
echo "re-frame dispatch/subscribe and status-im are not allowed in quo/quo2 components"
echo ''
echo "$INVALID_CHANGES"
exit 1
+15 -8
View File
@@ -64,15 +64,15 @@
{:closure-defines
{status-im.utils.config/POKT_TOKEN #shadow/env "POKT_TOKEN"
status-im.utils.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY"}
:compiler-options {:output-feature-set :es6
:compiler-options {:output-feature-set :es6
;;disable for android build as there
;;is an intermittent warning with deftype
:warnings-as-errors false
:infer-externs :auto
:static-fns true
:fn-invoke-direct true
:optimizations :advanced
:js-options {:js-provider :closure}}}}
:warnings-as-errors false
:infer-externs :auto
:static-fns true
:fn-invoke-direct true
:optimizations :advanced
:js-options {:js-provider :closure}}}}
;; the tests are ran with node, react-native dependencies are mocked
;; by using node --require override.js, which uses the node-library
;; produced by the target :mocks below and redefines node require
@@ -106,4 +106,11 @@
:output-to "target/mocks/mocks.js"
:output-dir "target/mocks"
:compiler-options {:optimizations :simple
:source-map false}}}}
:source-map false}}
:component-test {:target :npm-module
:entries [quo2.core-spec]
:ns-regexp "-component-spec$"
:output-dir "component-spec"
:compiler-options {:warnings-as-errors false
:static-fns false
:infer-externs true}}}}
+186
View File
@@ -0,0 +1,186 @@
import { useDerivedValue, withTiming, withSequence, withDelay, Easing } from 'react-native-reanimated';
// Shell Worklets
// Home Stack States
const CLOSE_WITH_ANIMATION = 0;
const OPEN_WITH_ANIMATION = 1;
const CLOSE_WITHOUT_ANIMATION = 3;
const OPEN_WITHOUT_ANIMATION = 4;
// Derived Values
export function stackOpacity (stackId, selectedStackId) {
return useDerivedValue(
function () {
'worklet'
return selectedStackId.value == stackId ? 1 : 0;
}
);
}
export function stackPointer (stackId, selectedStackId) {
return useDerivedValue(
function () {
'worklet'
return selectedStackId.value == stackId ? "auto" : "none";
}
);
}
export function bottomTabIconColor (stackId, selectedStackId, homeStackState,
passThrough, selectedTabColor, defaultColor,
passThroughColor) {
return useDerivedValue(
function () {
'worklet'
var homeStackStateValue = homeStackState.value;
if (selectedStackId.value == stackId &&
(homeStackStateValue == OPEN_WITH_ANIMATION ||
homeStackStateValue == OPEN_WITHOUT_ANIMATION)){
return selectedTabColor;
}
else if (passThrough.value){
return passThroughColor;
}
else {
return defaultColor;
}
}
);
}
export function bottomTabsHeight(homeStackState, height, extendedHeight) {
return useDerivedValue(
function () {
'worklet'
switch (homeStackState.value) {
case OPEN_WITH_ANIMATION:
return withTiming(extendedHeight, defaultDurationAndEasing);
break;
case CLOSE_WITH_ANIMATION:
return withTiming(height, defaultDurationAndEasing);
break;
case OPEN_WITHOUT_ANIMATION:
return extendedHeight;
break;
case CLOSE_WITHOUT_ANIMATION:
return height;
break;
}
}
)
}
// Home Stack
const shellAnimationTime = 200;
const defaultDurationAndEasing = {
duration: shellAnimationTime,
easing: Easing.bezier(0, 0, 1, 1),
}
export function homeStackOpacity (homeStackState) {
return useDerivedValue(
function () {
'worklet'
switch (homeStackState.value) {
case OPEN_WITH_ANIMATION:
return withTiming(1, defaultDurationAndEasing);
break;
case CLOSE_WITH_ANIMATION:
return withTiming(0, defaultDurationAndEasing);
break;
case OPEN_WITHOUT_ANIMATION:
return 1;
break;
case CLOSE_WITHOUT_ANIMATION:
return 0;
break;
}
}
);
}
export function homeStackTop (homeStackState, top) {
return useDerivedValue(
function () {
'worklet'
switch (homeStackState.value) {
case OPEN_WITH_ANIMATION:
return withTiming(0, defaultDurationAndEasing);
break;
case CLOSE_WITH_ANIMATION:
return withTiming(top, defaultDurationAndEasing);
break;
case OPEN_WITHOUT_ANIMATION:
return 0;
break;
case CLOSE_WITHOUT_ANIMATION:
return top;
break;
}
}
);
}
export function homeStackLeft (selectedStackId, animateHomeStackLeft, homeStackState, left) {
return useDerivedValue(
function () {
'worklet'
if (animateHomeStackLeft.value) {
var leftValue = left[selectedStackId.value];
switch (homeStackState.value) {
case OPEN_WITH_ANIMATION:
return withSequence(withTiming(leftValue, {duration: 0}), withTiming(0, defaultDurationAndEasing))
break;
case CLOSE_WITH_ANIMATION:
return withTiming(leftValue, defaultDurationAndEasing);
break;
case OPEN_WITHOUT_ANIMATION:
return 0;
break;
case CLOSE_WITHOUT_ANIMATION:
return leftValue;
break;
}
} else {
return 0;
}
}
);
}
export function homeStackPointer (homeStackState) {
return useDerivedValue(
function () {
'worklet'
var homeStackStateValue = homeStackState.value;
return (homeStackStateValue == OPEN_WITH_ANIMATION ||
homeStackStateValue == OPEN_WITHOUT_ANIMATION) ? "auto" : "none";
}
);
}
export function homeStackScale (homeStackState, minimizeScale) {
return useDerivedValue(
function () {
'worklet'
switch (homeStackState.value) {
case OPEN_WITH_ANIMATION:
return withTiming(1, defaultDurationAndEasing);
break;
case CLOSE_WITH_ANIMATION:
return withTiming(minimizeScale, defaultDurationAndEasing);
break;
case OPEN_WITHOUT_ANIMATION:
return 1;
break;
case CLOSE_WITHOUT_ANIMATION:
return minimizeScale;
break;
}
}
);
}
-105
View File
@@ -1,5 +1,3 @@
import { useDerivedValue, withTiming, withSequence, withDelay, Easing } from 'react-native-reanimated';
// Generic Worklets
export function applyAnimationsToStyle(animations, style) {
@@ -29,106 +27,3 @@ export function applyAnimationsToStyle(animations, style) {
return Object.assign(animatedStyle, style);
};
};
// Switcher Worklets
export function stackOpacity (stackId, selectedStackId) {
return useDerivedValue(
function () {
'worklet'
return selectedStackId.value == stackId ? 1 : 0;
}
);
}
export function stackPointer (stackId, selectedStackId) {
return useDerivedValue(
function () {
'worklet'
return selectedStackId.value == stackId ? "auto" : "none";
}
);
}
export function bottomTabIconColor (stackId, selectedStackId, homeStackOpen,
passThrough, selectedTabColor, defaultColor,
passThroughColor) {
return useDerivedValue(
function () {
'worklet'
if (selectedStackId.value == stackId && homeStackOpen.value){
return selectedTabColor;
}
else if (passThrough.value){
return passThroughColor;
}
else {
return defaultColor;
}
}
);
}
// Home Stack
const shellAnimationTime = 200;
const defaultDurationAndEasing = {
duration: shellAnimationTime,
easing: Easing.bezier(0, 0, 1, 1),
}
export function homeStackOpacity (homeStackOpen) {
return useDerivedValue(
function () {
'worklet'
return withTiming(homeStackOpen.value ? 1 : 0, defaultDurationAndEasing);
}
);
}
export function homeStackTop (homeStackOpen, top) {
return useDerivedValue(
function () {
'worklet'
return withTiming(homeStackOpen.value ? 0 : top, defaultDurationAndEasing);
}
);
}
export function homeStackLeft (selectedStackId, animateHomeStackLeft, homeStackOpen, left) {
return useDerivedValue(
function () {
'worklet'
if (animateHomeStackLeft.value) {
var leftValue = left[selectedStackId.value];
if (homeStackOpen.value) {
return withSequence(withTiming(leftValue, {duration: 0}), withTiming(0, defaultDurationAndEasing))
} else {
return withTiming(leftValue, defaultDurationAndEasing);
}
} else {
return 0;
}
}
);
}
export function homeStackPointer (homeStackOpen) {
return useDerivedValue(
function () {
'worklet'
return homeStackOpen.value ? "auto" : "none";
}
);
}
export function homeStackScale (homeStackOpen, minimizeScale) {
return useDerivedValue(
function () {
'worklet'
return withTiming(homeStackOpen.value ? 1 : minimizeScale, defaultDurationAndEasing);
}
);
}
+3
View File
@@ -282,6 +282,8 @@
(def worklet-factory
#js {:applyAnimationsToStyle (fn [])})
(def shell-worklets #js {})
;; Update i18n_resources.cljs
(defn mock [module]
(case module
@@ -328,6 +330,7 @@
"@react-native-async-storage/async-storage" async-storage
"react-native-svg" react-native-svg
"../src/js/worklet_factory.js" worklet-factory
"../src/js/shell_worklets.js" shell-worklets
"./fleets.js" default-fleets
"./chats.js" default-chats
"@walletconnect/client" wallet-connect-client
+2 -2
View File
@@ -56,7 +56,7 @@
(defn button [{:keys [on-press disabled type theme before after
haptic-feedback haptic-type on-long-press on-press-start
accessibility-label loading border-radius style]
accessibility-label loading border-radius style test-ID]
:or {theme :main
type :primary
haptic-feedback true
@@ -92,7 +92,7 @@
{:on-press-start (fn []
(optional-haptic)
(on-press-start))}))
[rn/view {:style (merge (style-container type) style)}
[rn/view {:test-ID test-ID :style (merge (style-container type) style)}
(when before
[rn/view
[icons/icon before {:color icon-color}]])
+1 -1
View File
@@ -34,7 +34,7 @@
(def touchable-opacity (reagent/adapt-react-class (.-TouchableOpacity ^js rn)))
(def touchable-highlight (reagent/adapt-react-class (.-TouchableHighlight ^js rn)))
(def touchable-without-feedback (reagent/adapt-react-class (.-TouchableWithoutFeedback ^js rn)))
(def text-input (reagent/adapt-react-class (.-TextInput ^js rn)))
(def text-input (reagent/adapt-react-class (.-TextInput ^js rn)))
(def keyboard-avoiding-view-class (reagent/adapt-react-class (.-KeyboardAvoidingView ^js rn)))
+44
View File
@@ -0,0 +1,44 @@
const waitToNavigate = duration => new Promise(resolve => setTimeout(() => resolve(), duration));
const SUPER_SECRET_PASSWORD = 'password'
const loginToHomePage = async () => {
if (device.getPlatform() === 'ios') {
await device.setStatusBar({ time: '12:34', dataNetwork: 'wifi', wifiBars: '3', batteryState: 'charging', batteryLevel: '100' });
}
await device.reloadReactNative();
await element(by.id('terms-of-service')).tap();
await waitToNavigate(400);
await element(by.id('get-started')).tap();
await waitToNavigate(300);
await element(by.id('generate-keys')).tap();
await waitToNavigate(200);
await element(by.text('Next')).tap();
await waitToNavigate(700);
await element(by.text('Next')).tap();
await waitToNavigate(700);
await element(by.id('password-placeholder')).typeText(SUPER_SECRET_PASSWORD);
await element(by.id('confirm-password-placeholder')).typeText(SUPER_SECRET_PASSWORD);
await element(by.text('Next')).tap();
await waitToNavigate(200);
await element(by.id("browser-stack")).tap();
await waitToNavigate(400);
await element(by.text("Quo2.0 Preview")).tap();
await waitToNavigate(400);
}
describe('Default Renders', () => {
beforeAll(async () => loginToHomePage())
beforeEach(async () => {
});
afterEach(async () => {
await element(by.id("back-button")).tap();
await waitToNavigate(200);
});
it(`button page should match image render`, async () => {
await element(by.id(`quo2-:button`)).tap();
await waitToNavigate(200);
const res = await jestExpect(`button`).toMatchImageSnapshot();
})
});
@@ -15,18 +15,12 @@
(defn group-avatar [_]
(fn [{:keys [color size]}]
(let [container-size (get-in sizes [:container size])
icon-size (get-in sizes [:icon size])
;; theme (or override-theme (if (colors/dark?) :dark :light))
]
icon-size (get-in sizes [:icon size])]
[rn/view {:width container-size
:height container-size
:align-items :center
:justify-content :center
:border-radius (/ container-size 2)
:background-color color
;; :background-color (if (= theme :light) TODO: colors/custom-color method here is not working for some reason.
;; (colors/custom-color color 50)
;; (colors/custom-color color 60))
}
[icon/icon :i/group {:size icon-size ; TODO: group icon sizes 12 and 20 (small and large) are missing
:background-color (colors/custom-color-by-theme color 50 60)}
[icon/icon :i/group {:size icon-size
:color colors/white-opa-70}]])))
+7 -7
View File
@@ -122,20 +122,20 @@
first-initial-letter
initials)
"")]
[rn/view {:style {:width outer-dimensions
:height outer-dimensions
:border-radius outer-dimensions}}
[rn/view {:accessibility-label :user-avatar
:style {:width outer-dimensions
:height outer-dimensions
:border-radius outer-dimensions}}
(when (and ring? identicon?)
[icons/icon :main-icons/identicon-ring {:width outer-dimensions
:height outer-dimensions
:size outer-dimensions
:no-color true}])
[icons/icon :i/identicon-ring {:size outer-dimensions
:no-color true}])
(if profile-picture
;; display image
[fast-image/fast-image {:source {:uri profile-picture}
:style (container-styling inner-dimensions outer-dimensions)}]
;; else display initials
[container inner-dimensions outer-dimensions
^{:key :icon-text}
[text/text {:weight :semi-bold
:size font-size
:style {:color colors/white-opa-70}}
@@ -2,7 +2,7 @@
(:require [react-native.core :as rn]
[quo2.foundations.colors :as colors]
[quo2.components.markdown.text :as text]
[clojure.string :as clojure-string]))
[clojure.string :as string]))
(def circle-sizes {:small 20
:medium 32
@@ -29,10 +29,10 @@
(let [circle-size (size circle-sizes)
small? (= size :small)
f-name-initial (-> f-name
clojure-string/upper-case
string/upper-case
(subs 0 1))
l-name-initial (-> l-name
clojure-string/upper-case
string/upper-case
(subs 0 1))
circle-color (colors/custom-color color 50 20)
text-color (colors/custom-color-by-theme color 50 60)]
+202 -176
View File
@@ -7,147 +7,149 @@
[quo2.components.icon :as quo2.icons]))
(def themes
{:light {:primary {:icon-color colors/white
:label {:style {:color colors/white}}
:background-color {:default colors/primary-50
:pressed colors/primary-60
:disabled colors/primary-50}}
:secondary {:icon-color colors/primary-50
:label {:style {:color colors/primary-50}}
:background-color {:default colors/primary-50-opa-20
:pressed colors/primary-50-opa-40
:disabled colors/primary-50-opa-20}}
:grey {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-50
:label {:style {:color colors/neutral-100}}
:background-color {:default colors/neutral-10
:pressed colors/neutral-20
:disabled colors/neutral-10}}
:dark-grey {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-50
:label {:style {:color colors/neutral-100}}
:background-color {:default colors/neutral-20
:pressed colors/neutral-30
:disabled colors/neutral-20}}
:outline {:icon-color colors/neutral-50
:icon-secondary-color colors/neutral-50
:label {:style {:color colors/neutral-100}}
:border-color {:default colors/neutral-20
:pressed colors/neutral-40
:disabled colors/neutral-20}}
:ghost {:icon-color colors/neutral-50
:icon-secondary-color colors/neutral-50
:label {:style {:color colors/neutral-100}}
:background-color {:pressed colors/neutral-10}}
:danger {:icon-color colors/white
:label {:style {:color colors/white}}
:background-color {:default colors/danger-50
:pressed colors/danger-60
:disabled colors/danger-50}}
:positive {:icon-color colors/white
:label {:style {:color colors/white}}
:background-color {:default colors/success-50
:pressed colors/success-60
:disabled colors/success-50-opa-30}}
:photo-bg {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-80-opa-40
:label {:style {:color colors/neutral-100}}
:background-color {:default colors/white-opa-40
:pressed colors/white-opa-50
:disabled colors/white-opa-40}}
:blur-bg {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-80-opa-40
:label {:style {:color colors/neutral-100}}
:background-color {:default colors/neutral-80-opa-5
:pressed colors/neutral-80-opa-10
:disabled colors/neutral-80-opa-5}}
:blur-bg-outline {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-80-opa-40
:label {:style {:color colors/neutral-100}}
:border-color {:default colors/neutral-80-opa-10
:pressed colors/neutral-80-opa-20
:disabled colors/neutral-80-opa-10}}
{:light {:primary {:icon-color colors/white
:label-color colors/white
:background-color {:default colors/primary-50
:pressed colors/primary-60
:disabled colors/primary-50}}
:secondary {:icon-color colors/primary-50
:label-color colors/primary-50
:background-color {:default colors/primary-50-opa-20
:pressed colors/primary-50-opa-40
:disabled colors/primary-50-opa-20}}
:grey {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-50
:label-color colors/neutral-100
:background-color {:default colors/neutral-10
:pressed colors/neutral-20
:disabled colors/neutral-10}}
:dark-grey {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-50
:label-color colors/neutral-100
:background-color {:default colors/neutral-20
:pressed colors/neutral-30
:disabled colors/neutral-20}}
:outline {:icon-color colors/neutral-50
:icon-secondary-color colors/neutral-50
:label-color colors/neutral-100
:border-color {:default colors/neutral-20
:pressed colors/neutral-40
:disabled colors/neutral-20}}
:ghost {:icon-color colors/neutral-50
:icon-secondary-color colors/neutral-50
:label-color colors/neutral-100
:background-color {:pressed colors/neutral-10}}
:danger {:icon-color colors/white
:label-color colors/white
:background-color {:default colors/danger-50
:pressed colors/danger-60
:disabled colors/danger-50}}
:positive {:icon-color colors/white
:label-color colors/white
:background-color {:default colors/success-50
:pressed colors/success-60
:disabled colors/success-50-opa-30}}
:photo-bg {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-80-opa-40
:label-color colors/neutral-100
:background-color {:default colors/white-opa-40
:pressed colors/white-opa-50
:disabled colors/white-opa-40}}
:blur-bg {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-80-opa-40
:label-color colors/neutral-100
:background-color {:default colors/neutral-80-opa-5
:pressed colors/neutral-80-opa-10
:disabled colors/neutral-80-opa-5}}
:blur-bg-outline {:icon-color colors/neutral-100
:icon-secondary-color colors/neutral-80-opa-40
:label-color colors/neutral-100
:border-color {:default colors/neutral-80-opa-10
:pressed colors/neutral-80-opa-20
:disabled colors/neutral-80-opa-10}}
:shell {:icon-color colors/white
:label {:style {:color colors/white}}
:label-color colors/white
:background-color {:default colors/neutral-95
:pressed colors/neutral-95
:disabled colors/neutral-95}}}
:dark {:primary {:icon-color colors/white
:label {:style {:color colors/white}}
:background-color {:default colors/primary-60
:pressed colors/primary-50
:disabled colors/primary-60}}
:secondary {:icon-color colors/primary-50
:label {:style {:color colors/primary-50}}
:background-color {:default colors/primary-50-opa-20
:pressed colors/primary-50-opa-30
:disabled colors/primary-50-opa-20}}
:grey {:icon-color colors/white
:icon-secondary-color colors/neutral-40
:label {:style {:color colors/white}}
:background-color {:default colors/neutral-80
:pressed colors/neutral-60
:disabled colors/neutral-80}}
:dark-grey {:icon-color colors/white
:icon-secondary-color colors/neutral-40
:label {:style {:color colors/white}}
:background-color {:default colors/neutral-70
:pressed colors/neutral-60
:disabled colors/neutral-70}}
:outline {:icon-color colors/neutral-40
:icon-secondary-color colors/neutral-40
:label {:style {:color colors/white}}
:border-color {:default colors/neutral-70
:pressed colors/neutral-60
:disabled colors/neutral-70}}
:ghost {:icon-color colors/neutral-40
:icon-secondary-color colors/neutral-40
:label {:style {:color colors/white}}
:background-color {:pressed colors/neutral-80}}
:danger {:icon-color colors/white
:label {:style {:color colors/white}}
:background-color {:default colors/danger-60
:pressed colors/danger-50
:disabled colors/danger-60}}
:positive {:icon-color colors/white
:label {:style {:color colors/white}}
:background-color {:default colors/success-60
:pressed colors/success-50
:disabled colors/success-60-opa-30}}
:photo-bg {:icon-color colors/white
:icon-secondary-color colors/neutral-30
:label {:style {:color colors/white}}
:background-color {:default colors/neutral-80-opa-40
:pressed colors/neutral-80-opa-50
:disabled colors/neutral-80-opa-40}}
:blur-bg {:icon-color colors/white
:icon-secondary-color colors/white-opa-40
:label {:style {:color colors/white}}
:background-color {:default colors/white-opa-5
:pressed colors/white-opa-10
:disabled colors/white-opa-5}}
:blur-bg-outline {:icon-color colors/white
:icon-secondary-color colors/white-opa-40
:label {:style {:color colors/white}}
:border-color {:default colors/white-opa-10
:pressed colors/white-opa-20
:disabled colors/white-opa-5}}
:shell {:icon-color colors/white
:label {:style {:color colors/white}}
:background-color {:default colors/neutral-95}}}})
:dark {:primary {:icon-color colors/white
:label-color colors/white
:background-color {:default colors/primary-60
:pressed colors/primary-50
:disabled colors/primary-60}}
:secondary {:icon-color colors/primary-50
:label-color colors/primary-50
:background-color {:default colors/primary-50-opa-20
:pressed colors/primary-50-opa-30
:disabled colors/primary-50-opa-20}}
:grey {:icon-color colors/white
:icon-secondary-color colors/neutral-40
:label-color colors/white
:background-color {:default colors/neutral-80
:pressed colors/neutral-60
:disabled colors/neutral-80}}
:dark-grey {:icon-color colors/white
:icon-secondary-color colors/neutral-40
:label-color colors/white
:background-color {:default colors/neutral-70
:pressed colors/neutral-60
:disabled colors/neutral-70}}
:outline {:icon-color colors/neutral-40
:icon-secondary-color colors/neutral-40
:label-color colors/white
:border-color {:default colors/neutral-70
:pressed colors/neutral-60
:disabled colors/neutral-70}}
:ghost {:icon-color colors/neutral-40
:icon-secondary-color colors/neutral-40
:label-color colors/white
:background-color {:pressed colors/neutral-80}}
:danger {:icon-color colors/white
:label-color colors/white
:background-color {:default colors/danger-60
:pressed colors/danger-50
:disabled colors/danger-60}}
:positive {:icon-color colors/white
:label-color colors/white
:background-color {:default colors/success-60
:pressed colors/success-50
:disabled colors/success-60-opa-30}}
:photo-bg {:icon-color colors/white
:icon-secondary-color colors/neutral-30
:label-color colors/white
:background-color {:default colors/neutral-80-opa-40
:pressed colors/neutral-80-opa-50
:disabled colors/neutral-80-opa-40}}
:blur-bg {:icon-color colors/white
:icon-secondary-color colors/white-opa-40
:label-color colors/white
:background-color {:default colors/white-opa-5
:pressed colors/white-opa-10
:disabled colors/white-opa-5}}
:blur-bg-outline {:icon-color colors/white
:icon-secondary-color colors/white-opa-40
:label-color colors/white
:border-color {:default colors/white-opa-10
:pressed colors/white-opa-20
:disabled colors/white-opa-5}}
:shell {:icon-color colors/white
:label-color colors/white
:background-color {:default colors/neutral-95}}}})
(defn shape-style-container [type icon size]
{:border-radius (if (and icon (#{:primary :secondary :danger} type))
24
(case size
56 12
40 12
32 10
24 8))})
(defn style-container [type size disabled background-color border-color icon above width before after]
(merge {:height size
:align-items :center
:justify-content :center
:flex-direction (if above :column :row)
:border-radius (if (and icon (#{:primary :secondary :danger} type))
24
(case size
56 12
40 12
32 10
24 8))
:background-color background-color
:padding-horizontal (when-not (or icon before after)
(case size 56 16 40 16 32 12 24 8))
@@ -159,6 +161,7 @@
(case size 56 0 40 9 32 5 24 3))
:padding-bottom (when-not (or icon before after)
(case size 56 0 40 9 32 5 24 4))}
(shape-style-container type icon size)
(when width
{:width width})
(when icon
@@ -169,14 +172,19 @@
(when disabled
{:opacity 0.3})))
(defn community-themed? [type community-color]
(and (= type :community) (string? community-color)))
(defn button
"with label
[button opts \"label\"]
opts
{:type :primary/:secondary/:grey/:dark-grey/:outline/:ghost/
:danger/:photo-bg/:blur-bg/:blur-bg-ouline/:shell
:danger/:photo-bg/:blur-bg/:blur-bg-ouline/:shell/:community
:size 40/32/24
:icon true/false
:community-color '#FFFFFF'
:community-text-color '#000000'
:before :icon-keyword
:after :icon-keyword}
@@ -184,20 +192,21 @@
[button {:icon true} :main-icons/close-circle]"
[_ _]
(let [pressed (reagent/atom false)]
(fn [{:keys [on-press disabled type size before after above width
(fn [{:keys [on-press disabled type size community-color community-text-color before after above width
override-theme override-background-color
on-long-press accessibility-label icon icon-no-color style]
on-long-press accessibility-label icon icon-no-color style test-ID]
:or {type :primary
size 40}}
children]
(let [{:keys [icon-color icon-secondary-color background-color label border-color]}
(let [{:keys [icon-color icon-secondary-color background-color label-color border-color]}
(get-in themes [(or
override-theme
(theme/get-theme)) type])
state (cond disabled :disabled @pressed :pressed :else :default)
icon-size (when (= 24 size) 12)
icon-secondary-color (or icon-secondary-color icon-color)]
[rn/touchable-without-feedback (merge {:disabled disabled
[rn/touchable-without-feedback (merge {:test-ID test-ID
:disabled disabled
:accessibility-label accessibility-label}
(when on-press
{:on-press (fn []
@@ -211,48 +220,65 @@
(reset! pressed nil))})
[rn/view {:style (merge
(style-container
type
size
disabled
(or override-background-color (get background-color state))
(get border-color state)
icon
above
width
before
after)
(shape-style-container type icon size)
{:background-color
(if (= state :pressed)
(colors/theme-colors colors/neutral-100 colors/white)
:transparent)}
style)}
(when above
[rn/view
[quo2.icons/icon above {:container-style {:margin-bottom 2}
:color icon-secondary-color
:size icon-size}]])
(when before
[rn/view
[quo2.icons/icon before {:container-style {:margin-left (if (= size 40) 12 8)
:margin-right 4}
[rn/view {:style (merge
(style-container
type
size
disabled
(or override-background-color (get background-color state))
(get border-color state)
icon
above
width
before
after)
(when
(community-themed? type community-color)
(merge
{:background-color community-color}
(when (= state :pressed) {:opacity 0.9}))))}
(when above
[rn/view
[quo2.icons/icon above {:container-style {:margin-bottom 2}
:color icon-secondary-color
:size icon-size}]])
[rn/view
(cond
(or icon icon-no-color)
[quo2.icons/icon children {:color icon-color
:no-color icon-no-color
:size icon-size}]
(when before
[rn/view
[quo2.icons/icon before {:container-style {:margin-left (if (= size 40) 12 8)
:margin-right 4}
:color icon-secondary-color
:size icon-size}]])
[rn/view
(cond
(or icon icon-no-color)
[quo2.icons/icon children {:color icon-color
:no-color icon-no-color
:size icon-size}]
(string? children)
[text/text (merge {:size (when (#{56 24} size) :paragraph-2)
:weight :medium
:number-of-lines 1}
label)
children]
(string? children)
[text/text {:size (when (#{56 24} size) :paragraph-2)
:weight :medium
:number-of-lines 1
:style {:color (if
(and
(community-themed? type community-color)
(string? community-text-color))
community-text-color
label-color)}}
(vector? children)
children)]
(when after
[rn/view
[quo2.icons/icon after {:container-style {:margin-left 4
:margin-right (if (= size 40) 12 8)}
:color icon-secondary-color
:size icon-size}]])]]))))
children]
(vector? children)
children)]
(when after
[rn/view
[quo2.icons/icon after {:container-style {:margin-left 4
:margin-right (if (= size 40) 12 8)}
:color icon-secondary-color
:size icon-size}]])]]]))))
+2 -2
View File
@@ -1,7 +1,7 @@
(ns quo2.components.code.snippet
(:require ["react-native" :as react-native]
[cljs-bean.core :as bean]
[clojure.string :as str]
[clojure.string :as string]
[oops.core :as oops]
[react-native.core :as rn]
[quo2.theme :as theme]
@@ -52,7 +52,7 @@
:style (text-style (get-in node [:properties :className]))}]
(render-nodes children))
;; Remove newlines as we already render each line separately.
(-> value str/trim-newline)))
(-> value string/trim-newline)))
nodes))
(defn- code-block [{:keys [rows line-number-width]}]
@@ -6,15 +6,11 @@
(defn community-card-view-item
[{:keys [name description locked
status tokens cover tags featured]} on-press]
status tokens cover tags width]} on-press]
[rn/touchable-opacity {:on-press on-press}
[rn/view {:style (merge (style/community-card 20)
{:margin-bottom 16}
(if featured
{:margin-right 12}
{:flex 1
:margin-horizontal 20}))}
[rn/view {:style {:height 230
[rn/view {:style (style/community-card 20)}
[rn/view {:style {:width width
:height 230
:border-radius 20}
:on-press on-press}
[rn/view
@@ -28,13 +28,9 @@
[community-stats {:icon :i/group
:members-count "629.2K" ;;TODO here should be formatted value, use money/format-members from outside this component
:icon-color icon-color}]
[community-stats {:icon :i/lightning
[community-stats {:icon :i/lightning
:members-count "112.1K"
:icon-color icon-color}]
(when (= type :card-view)
[community-stats {:icon :i/placeholder
:count 4
:icon-color icon-color}])]))
:icon-color icon-color}]]))
(defn community-tags [tags]
[rn/view (style/community-tags-container)
@@ -69,10 +65,12 @@
:style {:margin-top (if (= size :large) 8 2)}}
description])])
(defn permission-tag-container [{:keys [locked? tokens]}]
(defn permission-tag-container [{:keys [locked? tokens on-press]}]
[permission/tag {:background-color (colors/theme-colors
colors/neutral-10
colors/neutral-80)
:locked? locked?
:tokens tokens
:size 24}])
:size 24
:on-press on-press}])
@@ -0,0 +1,259 @@
(ns quo2.components.community.token-gating
(:require [quo.react-native :as rn]
[quo2.components.avatars.channel-avatar :as channel-avatar]
[quo2.components.buttons.button :as button]
[quo2.components.icon :as icon]
[quo2.components.info.information-box :as information-box]
[quo2.components.markdown.text :as text]
[quo2.components.tags.token-tag :as token-tag]
[quo2.foundations.colors :as colors]
[status-im.i18n.i18n :as i18n]
[status-im.ui.components.fast-image :as fast-image]))
(def ^:private token-tag-horizontal-spacing 7)
(def token-tag-vertical-spacing 5)
(def styles {:container {:flex 1
:padding-horizontal 20}
:header-container {:flex-direction :row
:align-items :center}
:header-spacing-community {:margin-bottom 16}
:header-spacing-channel {:margin-bottom 24}
:header-avatar {:margin-right 8}
:header-community-avatar-image {:width 32 :height 32}
:header-title-container {:flex 1
:flex-direction :row
:align-items :center}
:header-title-lock {:margin-left 6}
:header-info-button {:margin-left 8}
:token-requirement-text-spacing {:margin-vertical 8}
:token-requirement-list-spacing {:padding-vertical (- 8 token-tag-vertical-spacing)}
:token-row {:flex-direction :row
:flex-wrap :wrap
:align-items :center}
:token-row-container {:flex-direction :row
:align-items :center
:margin-horizontal (- token-tag-horizontal-spacing)}
:token-row-container-no-shift {:margin-left 0}
:token-row-or-text {:margin-right 3}
:token-tag-spacing {:margin-horizontal token-tag-horizontal-spacing
:margin-vertical token-tag-vertical-spacing}
:divider {:height 1
:margin-horizontal -20
:margin-top (- 12 token-tag-vertical-spacing)
:margin-bottom 8}
:membership-request-denied {:margin-top 13}
:enter-button {:margin-top 16}
:info-text {:margin-top 12
:text-align :center
:padding-horizontal 20}})
(defn multiple-token-requirements? [token-requirement-lists]
(when (vector? (first token-requirement-lists))
(> (count token-requirement-lists) 1)))
(defn is-token-requirement-met? [token-requirement-list]
(and (seq token-requirement-list)
(every?
(fn [token]
(get token :is-sufficient?))
token-requirement-list)))
(defn are-multiple-token-requirements-met? [token-requirement-lists]
(if (multiple-token-requirements? token-requirement-lists)
(some is-token-requirement-met? token-requirement-lists)
(is-token-requirement-met? token-requirement-lists)))
(defn token-requirement-list-row [tokens community-color]
[rn/view {:style (merge
(get styles :token-row)
(get styles :token-requirement-list-spacing))}
(map-indexed (fn [token-index token]
(let [{:keys [token-img-src token amount is-sufficient? is-purchasable?]} token]
^{:key token-index}
[rn/view {:style (get styles :token-tag-spacing)}
[token-tag/token-tag {:token token
:value amount
:size 24
:border-color community-color
:is-required is-sufficient?
:is-purchasable is-purchasable?
:token-img-src token-img-src}]]))
tokens)])
(defn token-requirement-list [props community-color]
(let [{:keys [gate token-requirements-changed? required-tokens-lost?]} props
[gate-type token-requirement-lists] gate
multiple-token-requirements? (multiple-token-requirements? token-requirement-lists)
is-sufficient? (are-multiple-token-requirements-met? token-requirement-lists)
you-must-hold-label (if (= gate-type :join)
(cond
token-requirements-changed? :t/you-must-now-hold
required-tokens-lost? :t/you-must-always-hold
:else :t/you-must-hold)
:t/you-must-hold)
message-label (cond
(= gate-type :join)
(cond
token-requirements-changed? :t/community-join-requirements-changed
required-tokens-lost? :t/community-join-requirements-tokens-lost
:else (if is-sufficient? :t/community-join-requirements-met :t/community-join-requirements-not-met))
(= gate-type :read)
(if is-sufficient?
:t/community-channel-read-requirements-met
:t/community-channel-read-requirements-not-met)
(= gate-type :write)
(if is-sufficient?
:t/community-channel-write-requirements-met
:t/community-channel-write-requirements-not-met))]
[rn/view
[rn/view {:style (get styles :token-requirement-text-spacing)}
[text/text {:weight :medium
:number-of-lines 1}
(i18n/label
message-label)]
[text/text {:size :paragraph-2
:number-of-lines 1}
(i18n/label
you-must-hold-label)]]
(if multiple-token-requirements?
(map-indexed
(fn [token-requirement-index tokens]
^{:key token-requirement-index}
[rn/view {:style (merge
(get styles :token-row-container)
(when-not (= token-requirement-index 0)
(get styles :token-row-container-no-shift)))}
(when-not (= token-requirement-index 0)
[text/text {:style (get styles :token-row-or-text)
:size :paragraph-2} "or"])
[token-requirement-list-row tokens community-color]])
token-requirement-lists)
[rn/view {:style (get styles :token-row-container)}
[token-requirement-list-row token-requirement-lists community-color]])]))
(defn is-community-locked? [community]
(not (are-multiple-token-requirements-met? (get-in community [:gates :join]))))
(defn is-channel-locked? [channel]
(not (are-multiple-token-requirements-met?
(or (get-in channel [:gates :read])
(get-in channel [:gates :write])))))
(defn token-gating
"[token-gating opts]
opts
{
:community {
:name string
:community-avatar-img-src string
:community-color string
:community-text-color string
:token-requirements-changed? boolean
:required-tokens-lost? boolean
}
:channel {
:name string
:emoji string
:emoji-background-color :string
:on-enter-channel callback
:membership-request-denied? boolean
}
}"
[_ _]
(fn [{:keys [community channel]}]
(let [type (if (some? community) :community :channel)
{:keys [name
emoji
emoji-background-color
on-enter-channel
community-avatar-img-src
community-color
community-text-color
membership-request-denied?
token-requirements-changed?
required-tokens-lost?
gates]} (if (= type :community) community channel)
locked? (if
(= type :community)
(is-community-locked? community)
(is-channel-locked? channel))]
[rn/view {:style (merge (get styles :container) {:background-color (colors/theme-colors colors/white colors/neutral-90)})}
[rn/view {:style (merge
(get styles :header-container)
(if
(= type :community)
(get styles :header-spacing-community)
(get styles :header-spacing-channel)))}
[rn/view {:style (get styles :header-avatar)}
(if (= type :community)
[fast-image/fast-image {:source community-avatar-img-src
:style (get styles :header-community-avatar-image)}]
[channel-avatar/channel-avatar {:big? true
:locked? locked?
:emoji emoji
:emoji-background-color emoji-background-color}])]
[rn/view {:style (get styles :header-title-container)}
[text/text {:weight :semi-bold
:number-of-lines 1
:size :heading-1
:style (get styles :header-text)} (if (= type :community) name (str "# " name))]
(when (= type :community)
[icon/icon (if locked?
:main-icons2/locked
:main-icons2/unlocked)
{:container-style (get styles :header-title-lock)
:color (colors/theme-colors
colors/neutral-50
colors/neutral-40)}])]
[button/button {:type :outline
:size 32
:icon true
:style (get styles :header-info-button)} :main-icons2/info]]
(map-indexed (fn [gate-index gate]
^{:key gate-index}
[:<>
(when-not (= gate-index 0)
[rn/view {:style (merge
(get styles :divider)
{:background-color (colors/theme-colors
colors/neutral-10
colors/neutral-80)})}])
[token-requirement-list {:gate gate :token-requirements-changed? token-requirements-changed? :required-tokens-lost? required-tokens-lost?} community-color]])
gates)
(when (= type :channel)
(if membership-request-denied?
[information-box/information-box {:type :error
:icon :main-icons2/untrustworthy
:no-icon-color? true
:style (get styles :membership-request-denied)}
(i18n/label
:t/membership-request-denied)]
[:<>
[button/button {:type :community
:community-color community-color
:community-text-color community-text-color
:style (get styles :enter-button)
:disabled locked?
:on-press on-enter-channel}
(str "# " (i18n/label
:t/enter-channel))]
[text/text
{:size :paragraph-2
:style (merge
(get styles :info-text)
{:color (colors/theme-colors
colors/neutral-50
colors/neutral-40)})}
(i18n/label
:t/community-enter-channel-info)]]))])))
@@ -0,0 +1,34 @@
(ns quo2.components.counter.--tests--.counter-component-spec
(:require ["@testing-library/react-native" :as rtl]
[quo2.components.counter.counter :as counter]
[reagent.core :as reagent]))
(defn render-counter
([]
(render-counter {} nil))
([opts value]
(rtl/render (reagent/as-element [counter/counter opts value]))))
(js/global.test "default render of counter component"
(fn []
(render-counter)
(-> (js/expect (rtl/screen.getByTestId "counter-component"))
(.toBeTruthy))))
(js/global.test "renders counter with a string value"
(fn []
(render-counter {} "1")
(-> (js/expect (rtl/screen.getByText "1"))
(.toBeTruthy))))
(js/global.test "renders counter with an integer value"
(fn []
(render-counter {} 1)
(-> (js/expect (rtl/screen.getByText "1"))
(.toBeTruthy))))
(js/global.test "renders counter with value 99+ when the value is greater than 99"
(fn []
(render-counter {} "100")
(-> (js/expect (rtl/screen.getByText "99+"))
(.toBeTruthy))))
+5 -2
View File
@@ -20,7 +20,7 @@
(defn counter
"type: default, secondary, grey, outline
value: integer"
[{:keys [type override-text-color override-bg-color style]} value]
[{:keys [type override-text-color override-bg-color style accessibility-label]} value]
(let [type (or type :default)
text-color (or override-text-color
(if (or
@@ -38,7 +38,10 @@
1 16
2 20
28)]
[rn/view {:style (cond-> (merge
[rn/view {:test-ID :counter-component
:accessible true
:accessibility-label accessibility-label
:style (cond-> (merge
{:align-items :center
:justify-content :center
:border-radius 6
+18
View File
@@ -0,0 +1,18 @@
(ns quo2.components.dividers.date
(:require [react-native.core :as rn]
[quo2.components.separator :as separator]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]))
(defn date [value]
[rn/view {:margin-vertical 16
:padding-right 8
:padding-left 62}
[text/text {:weight :medium
:accessibility-label :divider-date-text
:size :label
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)
:text-transform :capitalize
:margin-bottom 4}}
value]
[separator/separator]])
@@ -0,0 +1,63 @@
(ns quo2.components.drawers.--tests--.action-drawers-component-spec
(:require ["@testing-library/react-native" :as rtl]
[quo2.components.drawers.action-drawers :as action-drawer]
[reagent.core :as reagent]))
(defn render-action-drawer
([options]
(rtl/render (reagent/as-element [action-drawer/action-drawer options]))))
(js/global.test "action-drawer renders with elements label displaying"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"}]])
(-> (js/expect (rtl/screen.getByText "a sample label"))
(.toBeTruthy))))
(js/global.test "action-drawer renders with elements sub-label displaying"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:sub-label "a sample sub label"}]])
(-> (js/expect (rtl/screen.getByText "a sample sub label"))
(.toBeTruthy))))
(js/global.test "action-drawer on click action works on element"
(let [event (js/jest.fn)]
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:on-press event}]])
(rtl/fireEvent.press (rtl/screen.getByText "a sample label"))
(-> (js/expect event)
(.toHaveBeenCalled)))))
(js/global.test "action-drawer renders two icons when set"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:right-icon :i/friend
:accessibility-label :first-element}]])
(-> (js/expect (rtl/screen.getByLabelText "right-icon-for-action"))
(.toBeTruthy))
(-> (js/expect (rtl/screen.queryByLabelText "left-icon-for-action"))
(.toBeTruthy))))
(js/global.test "action-drawer does not render a divider when the add-divider? prop is false"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:add-divider? false
:accessibility-label :first-element}]])
(-> (js/expect (rtl/screen.getAllByLabelText "divider"))
(.not)
(.toBeTruthy))))
(js/global.test "action-drawer renders a divider when the add-divider? prop is true"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:add-divider? true
:accessibility-label :first-element}]])
(-> (js/expect (rtl/screen.getAllByLabelText "divider"))
(.toBeTruthy))))
+69 -54
View File
@@ -6,69 +6,84 @@
(defn- get-icon-color [danger?]
(if danger?
(colors/theme-colors colors/danger-50 colors/danger-60)
colors/danger-50
(colors/theme-colors colors/neutral-50 colors/neutral-40)))
(def divider
[rn/view {:style {:border-top-width 1
:border-top-color (colors/theme-colors
colors/neutral-10 colors/neutral-90)
:margin-top 8
:margin-bottom 7
:align-items :center
:flex-direction :row}
:accessible true
:accessibility-label :divider}])
(defn action [{:keys [icon
label
sub-label
right-icon
danger?
on-press] :as action-props}]
on-press
add-divider?
accessibility-label] :as action-props}]
(when action-props
[rn/touchable-opacity {:on-press on-press}
[rn/view {:style
{:height (if sub-label 56 47)
:margin-horizontal 20
:flex-direction :row}}
[:<> {:key label}
(when add-divider? divider)
[rn/touchable-highlight {:accessibility-label accessibility-label
:style {:border-radius 12
:height (if sub-label 58 50)
:margin-horizontal 8}
:underlay-color (colors/theme-colors colors/neutral-5 colors/neutral-90)
:on-press on-press}
[rn/view {:style
{:height 20
:margin-top :auto
:margin-bottom :auto
:margin-right 12
:width 20}}
[icon/icon icon
{:color (get-icon-color danger?)
:size 20}]]
[rn/view
{:style
{:flex 1
:justify-content :center}}
[text/text
{:size :paragraph-1
:weight :medium
:style {:color
(when danger?
(colors/theme-colors colors/danger-50 colors/danger-60))}}
label]
(when sub-label
[text/text
{:size :paragraph-2
:style {:color
(colors/theme-colors colors/neutral-50 colors/neutral-40)}}
sub-label])]
(when right-icon
[rn/view {:style
{:height 20
:margin-top :auto
:margin-bottom :auto
:width 20}}
[icon/icon right-icon
{:color (get-icon-color danger?)
:size 20}]])]]))
(defn divider []
[rn/view {:style {:border-top-width 1
:border-top-color (colors/theme-colors colors/neutral-10 colors/neutral-80)
:margin-top 8
:margin-bottom 7
:align-items :center
:flex-direction :row}}])
{:height (if sub-label 58 50)
:margin-horizontal 12
:flex-direction :row}}
[rn/view {:accessibility-label :left-icon-for-action
:accessible true
:style
{:height 20
:margin-top :auto
:margin-bottom :auto
:margin-right 12
:width 20}}
[icon/icon icon
{:color (get-icon-color danger?)
:size 20}]]
[rn/view
{:style
{:flex 1
:justify-content :center}}
[text/text
{:size :paragraph-1
:weight :medium
:style {:color
(when danger?
(colors/theme-colors colors/danger-50 colors/danger-60))}}
label]
(when sub-label
[text/text
{:size :paragraph-2
:style {:color
(colors/theme-colors colors/neutral-50 colors/neutral-40)}}
sub-label])]
(when right-icon
[rn/view {:style
{:height 20
:margin-top :auto
:margin-bottom :auto
:width 20}
:accessible true
:accessibility-label :right-icon-for-action}
[icon/icon right-icon
{:color (get-icon-color danger?)
:size 20}]])]]]))
(defn action-drawer [sections]
[:<> {:style
{:flex 1}}
(interpose
[divider]
[:<>
(doall
(for [actions sections]
(map action actions)))])
(doall
(map action actions))))])
+14 -8
View File
@@ -1,24 +1,29 @@
(ns quo2.components.icons.icons
(:require [clojure.java.io :as io]
[clojure.string :as cstr]))
[clojure.string :as string]))
(def icon-path "./resources/images/icons2/")
(defn combine-path [path el]
(if (System/getenv "COMPONENT_TEST")
(str "." path el "@2x.png")
(str "." path el ".png")))
(defn require-icon [size path]
(fn [el]
(let [s (str "." path el ".png")
(let [s (combine-path path el)
k (-> el
(cstr/replace "_" "-")
(cstr/replace " " "-")
(cstr/lower-case)
(string/replace "_" "-")
(string/replace " " "-")
(string/lower-case)
(str size))]
[k `(js/require ~s)])))
(defn get-files [path]
(->> (io/file path)
file-seq
(filter #(cstr/ends-with? % "png"))
(map #(first (cstr/split (.getName %) #"@")))
(filter #(string/ends-with? % "png"))
(map #(first (string/split (.getName %) #"@")))
distinct))
(defn get-icons [size]
@@ -30,4 +35,5 @@
(get-icons 12)
(get-icons 16)
(get-icons 20)
(get-icons 24)))
(get-icons 24)
(get-icons 32)))
+9 -7
View File
@@ -19,19 +19,21 @@
(defn info-message
"[info-message opts \"message\"]
opts
{:type :default/:success/:error
:size :default/:tiny
:icon :main-icons/info ;; info message icon
:text-color colors/white ;; text color override
:icon-color colors/white ;; icon color override"
[{:keys [type size icon text-color icon-color]} message]
{:type :default/:success/:error
:size :default/:tiny
:icon :main-icons/info ;; info message icon
:text-color colors/white ;; text color override
:icon-color colors/white ;; icon color override
:no-icon-color? false ;; disable tint color for icon"
[{:keys [type size icon text-color icon-color no-icon-color?]} message]
(let [weight (if (= size :default) :regular :medium)
size (if (= size :default) :paragraph-2 :label)
text-color (or text-color (get-color type))
icon-color (or icon-color text-color)]
[rn/view {:style {:flex-direction :row
:flex 1}}
[quo2.icons/icon icon {:color icon-color
[quo2.icons/icon icon {:color icon-color
:no-color no-icon-color?
:size 12
:container-style {:margin-top 3}}]
[text/text {:size size
@@ -49,11 +49,12 @@
:closed? true/false ;; information box's state
:id :information-box-id ;; unique id (required for closable? information box)
:icon :main-icons/info ;; information box icon
:no-icon-color? false ;; disable tint color for icon
:style style
:button-label \"PressMe\" ;; add action button with label
:on-button-press action ;; (required for information box with button-label)
:on-close on-close ;; (optional on-close call)"
[{:keys [type closable? closed? id icon style button-label on-button-press on-close]} message]
[{:keys [type closable? closed? id icon style button-label on-button-press on-close no-icon-color?]} message]
(let [background-color (get-color-by-type type :bg)
border-color (get-color-by-type type :border)
icon-color (get-color-by-type type :icon)
@@ -73,7 +74,8 @@
[info-message/info-message {:size :default
:icon icon
:text-color text-color
:icon-color icon-color} message]
:icon-color icon-color
:no-icon-color? no-icon-color?} message]
(when closable?
[rn/touchable-opacity
{:on-press on-close
+50 -49
View File
@@ -1,56 +1,57 @@
(ns quo2.components.list-items.channel
(:require [react-native.core :as rn]
[quo2.foundations.colors :as colors]
(:require [quo2.components.avatars.channel-avatar :as channel-avatar]
[quo2.components.counter.counter :as quo2.counter]
[quo2.components.icon :as quo2.icons]
[quo2.components.avatars.channel-avatar :as channel-avatar]
[quo2.components.markdown.text :as quo2.text]
[quo2.theme :as theme]))
[quo2.foundations.colors :as colors]
[quo2.theme :as theme]
[react-native.core :as rn]))
(defn list-item [{:keys [name locked? mentions-count unread-messages?
muted? is-active-channel? emoji channel-color]
muted? is-active-channel? emoji channel-color on-press]
:or {channel-color colors/primary-50}}]
[rn/view {:style (merge {:height 48
:display :flex
:border-radius 12
:flex-direction :row
:justify-content :space-between
:align-items :center
:width "100%"
:padding-left 12
:padding-right 12}
(when is-active-channel?
{:background-color (colors/theme-alpha channel-color 0.05 0.05)}))}
[rn/view {:display :flex
:flex-direction :row
:justify-content :flex-start
:align-items :center}
[channel-avatar/channel-avatar
{:big? true
:locked? locked?
:emoji-background-color (colors/theme-alpha channel-color 0.1 0.1)
:emoji emoji}]
[quo2.text/text
{:style (merge {:margin-left 12}
(when (and (not locked?) muted?)
{:color (if (theme/dark?) colors/neutral-60 colors/neutral-40)}))
:weight :medium :size :paragraph-1} (str "# " name)]]
[rn/view {:style {:height 20}}
(when (and (not locked?)
muted?)
[quo2.icons/icon :i/muted
{:size 20
:no-color true}])
(when (and (not locked?)
(not muted?)
(pos? (int mentions-count)))
[rn/view {:style {:margin-right 2
:margin-top 2}}
[quo2.counter/counter {:override-bg-color channel-color} mentions-count]])
(when (and (not locked?)
(not muted?)
(not (pos? (int mentions-count)))
unread-messages?)
[quo2.icons/icon :i/channel-notification
{:size 20
:no-color true}])]])
[rn/touchable-opacity {:on-press on-press}
[rn/view {:style (merge {:height 48
:display :flex
:border-radius 12
:flex-direction :row
:justify-content :space-between
:align-items :center
:width "100%"
:padding-left 12
:padding-right 12}
(when is-active-channel?
{:background-color (colors/theme-alpha channel-color 0.05 0.05)}))}
[rn/view {:display :flex
:flex-direction :row
:justify-content :flex-start
:align-items :center}
[channel-avatar/channel-avatar
{:big? true
:locked? locked?
:emoji-background-color (colors/theme-alpha channel-color 0.1 0.1)
:emoji emoji}]
[quo2.text/text
{:style (merge {:margin-left 12}
(when (and (not locked?) muted?)
{:color (if (theme/dark?) colors/neutral-60 colors/neutral-40)}))
:weight :medium :size :paragraph-1} (str "# " name)]]
[rn/view {:style {:height 20}}
(when (and (not locked?)
muted?)
[quo2.icons/icon :i/muted
{:size 20
:no-color true}])
(when (and (not locked?)
(not muted?)
(pos? (int mentions-count)))
[rn/view {:style {:margin-right 2
:margin-top 2}}
[quo2.counter/counter {:override-bg-color channel-color} mentions-count]])
(when (and (not locked?)
(not muted?)
(not (pos? (int mentions-count)))
unread-messages?)
[quo2.icons/icon :i/channel-notification
{:size 20
:no-color true}])]]])
+77
View File
@@ -0,0 +1,77 @@
(ns quo2.components.loaders.skeleton
(:require [react-native.core :as rn]
[react-native.masked-view :as masked-view]
[reagent.core :as reagent]
[react-native.reanimated :as reanimated]
[quo2.foundations.colors :as colors]))
(def message-skeleton-height 54)
(def avatar-skeleton-size 32)
(def message-content-width [{:author 80
:message 249}
{:author 124
:message 156}
{:author 96
:message 212}
{:author 112
:message 144}])
;; Standlone message skeleton
(defn message-skeleton []
[:f>
(fn []
(let [color (colors/theme-colors colors/neutral-5 colors/neutral-70)
loading-color (colors/theme-colors colors/neutral-10 colors/neutral-60)
content-width (rand-nth message-content-width)
author-width (content-width :author)
message-width (content-width :message)
{window-width :width} (rn/use-window-dimensions)
translate-x (reanimated/use-shared-value (- window-width))
animated-gradient-style (reanimated/apply-animations-to-style
{:transform [{:translateX translate-x}]}
{:width window-width
:height "100%"})]
(reanimated/animate-shared-value-with-repeat translate-x window-width 1000 :linear (- 1) false)
[masked-view/masked-view
{:style {:height message-skeleton-height}
:maskElement (reagent/as-element
[rn/view {:style {:height message-skeleton-height
:flex-direction :row
:padding-vertical 11
:background-color :transparent
:padding-left 21}}
[rn/view {:style {:height avatar-skeleton-size
:width avatar-skeleton-size
:border-radius (/ avatar-skeleton-size 2)
:background-color color
:overflow :hidden}}]
[rn/view {:style {:padding-left 8
:background-color :transparent}}
[rn/view {:style {:height 8
:width author-width
:border-radius 6
:background-color color
:margin-bottom 8
:overflow :hidden}}]
[rn/view {:style {:height 16
:width message-width
:border-radius 6
:overflow :hidden
:background-color color}}]]])}
[rn/view {:style {:flex 1
:background-color color}}
[reanimated/linear-gradient {:colors [color color loading-color color color]
:start {:x 0 :y 0}
:end {:x 1 :y 0}
:style animated-gradient-style}]]]))])
(defn skeleton [parent-height]
(let [number-of-skeletons (int (Math/floor (/ parent-height message-skeleton-height)))]
[rn/view {:style {:background-color (colors/theme-colors
colors/white
colors/neutral-90)
:flex 1}}
(for [n (range number-of-skeletons)]
[message-skeleton {:key n}])]))
@@ -0,0 +1,39 @@
(ns quo2.components.messages.author.style
(:require [quo2.foundations.colors :as colors]))
(def container
{:flex 1
:flex-wrap :wrap
:height 18
:flex-direction :row
:align-items :center})
(defn ens-text []
{:color (colors/theme-colors colors/neutral-100 colors/white)})
(defn nickname-text []
{:color (colors/theme-colors colors/neutral-100 colors/white)})
(def middle-dot-nickname
{:color colors/neutral-50
:margin-horizontal 4})
(def chat-key-text
{:color colors/neutral-50
:margin-left 8})
(def middle-dot-chat-key
{:color colors/neutral-50
:margin-left 4})
(defn profile-name-text [nickname?]
{:color (if nickname?
(colors/theme-colors colors/neutral-60 colors/neutral-40)
(colors/theme-colors colors/neutral-100 colors/white))})
(def icon-container
{:margin-left 4})
(defn time-text [ens?]
{:color colors/neutral-50
:margin-left (if ens? 8 4)})
@@ -0,0 +1,88 @@
(ns quo2.components.messages.author.view
(:require
[quo2.components.messages.author.style :as style]
[react-native.core :as rn]
[quo2.components.markdown.text :as text]
[quo2.components.icon :as icons]
[status-im.utils.utils :as utils]
[clojure.string :as string]))
(def middle-dot "·")
(defn display-name [{:keys [profile-name nickname ens-name text-style]}]
(let [ens? (-> ens-name string/blank? not)
nickname? (-> nickname string/blank? not)]
(if ens?
[text/text (merge {:weight :semi-bold
:size :paragraph-2
:style (style/ens-text)}
text-style)
ens-name]
[:<>
(if nickname?
[text/text (merge {:weight :semi-bold
:size :paragraph-2
:style (style/nickname-text)}
text-style)
nickname]
[text/text (merge {:weight :semi-bold
:size :paragraph-2
:style (style/profile-name-text nickname?)}
text-style)
profile-name])])))
(defn author [{:keys [profile-name nickname chat-key ens-name time-str contact? verified? untrustworthy?]}]
[:f>
(fn []
(let [ens? (-> ens-name string/blank? not)
nickname? (-> nickname string/blank? not)]
[rn/view {:style style/container}
(if ens?
[text/text {:weight :semi-bold
:size :paragraph-2
:style (style/ens-text)}
ens-name]
[:<>
(when nickname?
[:<>
[text/text {:weight :semi-bold
:size :paragraph-2
:style (style/nickname-text)}
nickname]
[text/text {:size :paragraph-2
:style style/middle-dot-nickname}
middle-dot]])
[text/text {:weight (if nickname? :medium :semi-bold)
:size :paragraph-2
:style (style/profile-name-text nickname?)}
profile-name]])
(when contact?
[icons/icon :main-icons2/contact
{:size 12
:no-color true
:container-style style/icon-container}])
(cond
verified?
[icons/icon :main-icons2/verified
{:size 12
:no-color true
:container-style style/icon-container}]
untrustworthy?
[icons/icon :main-icons2/untrustworthy
{:size 12
:no-color true
:container-style style/icon-container}])
(when-not ens?
[text/text {:monospace true
:size :paragraph-2
:style style/chat-key-text}
(utils/get-shortened-address chat-key)])
(when-not ens?
[text/text {:monospace true
:size :paragraph-2
:style style/middle-dot-chat-key}
middle-dot])
[text/text {:monospace true
:size :paragraph-2
:style (style/time-text ens?)}
time-str]]))])
@@ -1,12 +1,12 @@
(ns quo2.components.messages.system-message
(:require [react-native.core :as rn]
[react-native.reanimated :as reanimated]
[quo2.theme :as theme]
[quo2.components.avatars.icon-avatar :as icon-avatar]
(:require [quo2.components.avatars.icon-avatar :as icon-avatar]
[quo2.components.avatars.user-avatar :as user-avatar]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[utils :as utils]))
[quo2.theme :as theme]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[utils.string :as utils]))
(def themes-landed {:pinned colors/primary-50-opa-5
:added colors/primary-50-opa-5
@@ -60,19 +60,19 @@
:flex-direction :row}
[rn/view {:align-items :center
:flex-direction :row}
[sm-icon {:icon :main-icons/delete16
[sm-icon {:icon :main-icons/delete
:color :danger
:opacity 5}]
[text/text {:size :paragraph-2
:style {:color (get-color :text)
:margin-right 5}}
(or label (:message-deleted labels))]
(or (get labels label) label (:message-deleted labels))]
[sm-timestamp timestamp-str]]])
(defmethod sm-render :added [{:keys [state mentions timestamp-str labels]}]
[rn/view {:align-items :center
:flex-direction :row}
[sm-icon {:icon :main-icons/add-user16
[sm-icon {:icon :main-icons/add-user
:color :primary
:opacity (if (= state :landed) 0 5)}]
[sm-user-avatar (:image (first mentions))]
@@ -94,7 +94,7 @@
[rn/view {:flex-direction :row
:flex 1
:align-items :center}
[sm-icon {:icon :main-icons/pin16
[sm-icon {:icon :main-icons/pin
:color :primary
:opacity (if (= state :landed) 0 5)}]
[rn/view {:flex-direction :column
@@ -26,7 +26,7 @@
:icon-color-anim reanimated shared value
"
[{:keys [icon new-notifications? notification-indicator counter-label
on-press pass-through? icon-color-anim accessibility-label]}]
on-press pass-through? icon-color-anim accessibility-label test-ID]}]
[:f>
(fn []
(let [icon-animated-style (reanimated/apply-animations-to-style
@@ -40,7 +40,8 @@
:height 40
:border-radius 10})]
[rn/touchable-without-feedback
{:on-press on-press
{:test-ID test-ID
:on-press on-press
:on-press-in #(toggle-background-color background-color false pass-through?)
:on-press-out #(toggle-background-color background-color true pass-through?)
:accessibility-label accessibility-label}
@@ -14,31 +14,35 @@
:customization-color customization-color}]))
(defn floating-shell-button
"[floating-shell-button dynamic-buttons style]
dynamic-buttons
{:button-type {:on-press on-press :count count}}"
[dynamic-buttons style opacity-anim pointer-anim]
[:f>
(fn []
(let [original-style (merge {:flex-direction :row
:margin-horizontal 12} style)
animated-style (reanimated/apply-animations-to-style
{:opacity opacity-anim
:pointer-events pointer-anim}
original-style)]
[reanimated/view {:style animated-style}
;; Left Section
[rn/view {:style {:flex 1}}
[dynamic-button-view :search dynamic-buttons {:position :absolute
:right 8}]]
;; Mid Section (jump-to)
[dynamic-button-view :jump-to dynamic-buttons nil]
"[floating-shell-button dynamic-buttons style opacity-anim pointer-anim]
dynamic-buttons {:button-type {:on-press on-press :count count}}
style override style
opacity-anim reanimated value (optional)"
([dynamic-button style]
(floating-shell-button dynamic-button style nil))
([dynamic-buttons style opacity-anim]
[:f>
(fn []
(let [original-style (merge {:flex-direction :row
:margin-horizontal 12
:pointer-events :box-none} style)
animated-style (reanimated/apply-animations-to-style
(if opacity-anim
{:opacity opacity-anim} {})
original-style)]
[reanimated/view {:style animated-style}
;; Left Section
[rn/view {:style {:flex 1}}
[dynamic-button-view :search dynamic-buttons {:position :absolute
:right 8}]]
;; Mid Section (jump-to)
[dynamic-button-view :jump-to dynamic-buttons nil]
;; Right Section
[rn/view {:style {:flex 1}}
[rn/view {:style {:position :absolute
:flex-direction :row
:right 0}}
[dynamic-button-view :mention dynamic-buttons {:margin-left 8}]
[dynamic-button-view :notification-down dynamic-buttons {:margin-left 8}]
[dynamic-button-view :notification-up dynamic-buttons {:margin-left 8}]
[dynamic-button-view :bottom dynamic-buttons {:margin-left 8}]]]]))])
[rn/view {:style {:flex 1}}
[rn/view {:style {:position :absolute
:flex-direction :row
:right 0}}
[dynamic-button-view :mention dynamic-buttons {:margin-left 8}]
[dynamic-button-view :notification-down dynamic-buttons {:margin-left 8}]
[dynamic-button-view :notification-up dynamic-buttons {:margin-left 8}]
[dynamic-button-view :bottom dynamic-buttons {:margin-left 8}]]]]))]))
+105 -105
View File
@@ -3,7 +3,9 @@
[quo2.foundations.colors :as colors]
[quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[clojure.string :as string]))
[clojure.string :as string]
[quo2.components.buttons.button :as button]
[quo2.components.avatars.user-avatar :as user-avatar]))
(def ^:private centrify-style
{:display :flex
@@ -12,12 +14,10 @@
(def ^:private align-left (assoc centrify-style :align-items :flex-start))
(def ^:private icon-styles (assoc centrify-style :width 32 :height 32 :border-radius 10))
(defn- big? [size] (= size :big))
(defn- icon-props [color size]
(merge {:size 20
(merge {:size 20
:container-style {:width (if (big? size)
20
16)
@@ -28,34 +28,35 @@
{:color color}
{:no-color true})))
(defn render-left-section [{:keys [on-press icon
icon-color icon-background-color]}
put-middle-section-on-left?]
[rn/view {:style (merge
icon-styles
{:background-color icon-background-color
:width 32
:height 32}
(when put-middle-section-on-left? {:margin-right 5}))}
[rn/touchable-opacity {:on-press on-press}
[icons/icon icon (icon-props icon-color :big)]]])
(defn left-section-view [{:keys [on-press icon accessibility-label type] :or {type :grey}}
put-middle-section-on-left?]
[rn/view {:style (when put-middle-section-on-left? {:margin-right 5})}
[button/button {:on-press on-press
:icon true
:type type
:size 32
:accessibility-label accessibility-label}
icon]])
(defn- mid-section-comp
[{:keys [description-user-icon horizontal-description? text-secondary-color align-mid? text-color icon main-text type description]}]
[rn/view {:style (assoc
centrify-style
:flex-direction :row
:margin-horizontal 2)}
[{:keys [description-img description-user-icon horizontal-description?
text-secondary-color align-mid? text-color icon main-text type description]}]
[rn/view {:style (assoc centrify-style
:flex-direction :row
:margin-horizontal 2)}
(when (or (and (not horizontal-description?)
align-mid?
(not= :text-with-description type))
(and description-user-icon
(and (or description-img description-user-icon)
(not icon)))
[rn/image {:source {:uri description-user-icon}
:style {:width 32
:height 32
:border-radius 32
:margin-right 8}}])
(if description-img
[rn/view {:margin-right 8}
[description-img]]
[rn/image {:source {:uri description-user-icon}
:style {:width 32
:height 32
:border-radius 32
:margin-right 8}}]))
[rn/view {:style {:flex-direction (if horizontal-description?
:row
:column)}}
@@ -73,76 +74,69 @@
horizontal-description? (assoc :margin-left 4 :margin-top 2))}
description])]])
(defn- render-mid-section
(defn- mid-section-view
[{:keys [horizontal-description? one-icon-align-left? type left-align?
main-text right-icon main-text-icon-color left-icon] :as props}]
(let [text-color (if (colors/dark?) colors/neutral-5 colors/neutral-95)
text-secondary-color (if (colors/dark?) colors/neutral-40 colors/neutral-50)
main-text right-icon main-text-icon-color left-icon on-press avatar] :as props}]
(let [text-color (if (colors/dark?) colors/neutral-5 colors/neutral-95)
text-secondary-color (if (colors/dark?) colors/neutral-40 colors/neutral-50)
component-instance [mid-section-comp (assoc props :text-secondary-color text-secondary-color)]]
[rn/view {:style (merge
(if left-align?
align-left
centrify-style)
{:flex 1
:margin-left 4
:text-align-vertical :center})}
(case type
:text-only [text/text {:size :paragraph-1
:weight :semi-bold
:style {:color text-color}}
main-text]
:text-with-two-icons [rn/view {:style (assoc centrify-style :flex-direction :row)}
[icons/icon left-icon
(icon-props main-text-icon-color :big)]
[text/text {:size :paragraph-1
:weight :semi-bold
:style {:padding-horizontal 4
:color text-color}}
main-text]
[rn/touchable-opacity {:on-press on-press}
[rn/view {:style (merge
(if left-align?
align-left
centrify-style)
{:flex 1
:margin-left 4
:text-align-vertical :center})}
(case type
:text-only [text/text {:size :paragraph-1
:weight :semi-bold
:style {:color text-color}}
main-text]
:user-avatar [rn/view {:style (assoc centrify-style :flex-direction :row)}
[user-avatar/user-avatar avatar]
[text/text {:size :paragraph-1
:weight :semi-bold
:style {:padding-horizontal 4
:color text-color}}
main-text]]
:text-with-two-icons [rn/view {:style (assoc centrify-style :flex-direction :row)}
[icons/icon left-icon
(icon-props main-text-icon-color :big)]
[text/text {:size :paragraph-1
:weight :semi-bold
:style {:padding-horizontal 4
:color text-color}}
main-text]
[icons/icon right-icon
(icon-props main-text-icon-color :big)]]
:text-with-one-icon [rn/view {:style {:flex-direction :row}}
(if one-icon-align-left?
[rn/view {:style {:flex-direction :row
:align-items :center}}
(when horizontal-description?
[icons/icon left-icon
(icon-props main-text-icon-color :big)])
component-instance]
[rn/view {:style {:flex-direction :row
:align-items :center}}
component-instance
(when horizontal-description?
[icons/icon left-icon
(icon-props main-text-icon-color :big)])])]
:text-with-description component-instance)]]))
[icons/icon right-icon
(icon-props main-text-icon-color :big)]]
:text-with-one-icon [rn/view {:style {:flex-direction :row}}
(if one-icon-align-left?
[rn/view {:style {:flex-direction :row
:align-items :center}}
(when horizontal-description?
[icons/icon left-icon
(icon-props main-text-icon-color :big)])
component-instance]
[rn/view {:style {:flex-direction :row
:align-items :center}}
component-instance
(when horizontal-description?
[icons/icon left-icon
(icon-props main-text-icon-color :big)])])]
:text-with-description component-instance)]))
(defn- right-section-icon
[{:keys [background-color icon icon-color push-to-the-left?] :or {push-to-the-left? false}}]
[rn/view {:style (assoc
icon-styles
:background-color background-color
:width 32
:height 32
:margin-right (if push-to-the-left? 8 0))}
[icons/icon icon (icon-props icon-color :big)]])
(defn- render-right-section [right-section-buttons]
[rn/view {:style (assoc
centrify-style
:flex-direction :row
:flex 1
:justify-content :flex-end)}
(defn- right-section-view [right-section-buttons]
[rn/view {:style (assoc centrify-style
:flex-direction :row
:flex 1
:justify-content :flex-end)}
(let [last-icon-index (-> right-section-buttons count dec)]
(map-indexed (fn [index {:keys [icon background-color icon-color on-press]}]
(map-indexed (fn [index {:keys [icon on-press type] :or {type :grey}}]
^{:key index}
[rn/touchable-opacity {:on-press on-press}
[right-section-icon
{:icon icon
:background-color background-color
:icon-color icon-color
:push-to-the-left? (not= index last-icon-index)}]])
[rn/view {:style {:margin-right (if (not= index last-icon-index) 8 0)}}
[button/button {:on-press on-press :icon true :type type :size 32}
icon]])
right-section-buttons))])
(defn page-nav
@@ -154,7 +148,7 @@
:page-nav-color color
:page-nav-background-uri image-uri
:mid-section
{:type one-of :text-only :text-with-two-icons :text-with-one-icon :text-with-description
{:type one-of :text-only :text-with-two-icons :text-with-one-icon :text-with-description :user-avatar
:icon icon
:main-text string
:left-icon icon
@@ -163,16 +157,19 @@
:description-color color
:description-icon icon
:description-user-icon icon
:description-img a render prop which will be used in place of :description-user-icon
:main-text-icon-color color
}
:left-section
{:on-press event
{:type button-type
:on-press event
:icon icon
:icon-color color
:icon-background-color color
}
:right-section-buttons vector of
{:on-press event
{:type button-type
:on-press event
:icon icon
:icon-color color
:icon-background-color color
@@ -189,32 +186,35 @@
mid-section-props
{:type (:type mid-section)
:horizontal-description? horizontal-description?
:description-img (:description-img mid-section)
:main-text (:main-text mid-section)
:main-text-icon-color (:main-text-icon-color mid-section)
:one-icon-align-left? one-icon-align-left?
:right-icon (:right-icon mid-section)
:icon (:icon mid-section)
:left-icon (:left-icon mid-section)}]
:left-icon (:left-icon mid-section)
:avatar (:avatar mid-section)}]
[rn/view {:style (cond-> {:display :flex
:flex-direction :row
;; iPhone 11 Pro's height in Figma divided by Component height 56/1125
:align-items :center
:padding-horizontal 20
:height 56
:justify-content :space-between}
page-nav-background-uri (assoc :background-color page-nav-color)
page-nav-color (assoc :background page-nav-background-uri))}
[rn/view {:style {:flex 1
:flex-direction :row
:align-items :center}}
(render-left-section left-section put-middle-section-on-left?)
[left-section-view left-section put-middle-section-on-left?]
(when put-middle-section-on-left?
[render-mid-section (assoc mid-section-props
:left-align? true
:description (:description mid-section)
:description-color (:description-color mid-section)
:description-icon (:description-icon mid-section)
:align-mid? align-mid?
:description-user-icon (:description-user-icon mid-section))])]
[mid-section-view (assoc mid-section-props
:left-align? true
:description (:description mid-section)
:description-color (:description-color mid-section)
:description-icon (:description-icon mid-section)
:align-mid? align-mid?
:description-user-icon (:description-user-icon mid-section))])]
(when-not put-middle-section-on-left?
[render-mid-section mid-section-props])
(render-right-section right-section-buttons)]))
[mid-section-view mid-section-props])
[right-section-view right-section-buttons]]))
@@ -1,98 +0,0 @@
(ns quo2.components.navigation.top-nav
(:require [react-native.core :as rn]
[quo2.foundations.colors :as colors]
[quo2.components.counter.counter :as counter]
[quo2.components.buttons.button :as quo2.button]
[quo2.components.avatars.user-avatar :as user-avatar]
[react-native.hole-view :as hole-view]))
(defn- get-button-common-props [type]
(let [default? (= type :default)
dark? (colors/dark?)]
{:icon true
:size 32
:style {:margin-left 12}
:type (if default?
(if dark? :grey :dark-grey)
type)
:override-background-color (when (and dark? default?)
colors/neutral-90)}))
(defn- base-button [icon on-press accessibility-label button-common-props]
[quo2.button/button
(merge
{:on-press on-press
:accessibility-label accessibility-label}
button-common-props)
icon])
(defn top-nav
"[top-nav opts]
opts
{:type :default/:blurred/:shell
:new-notifications? true/false
:notification-indicator :unread-dot/:counter
:open-profile fn
:open-search fn
:open-scanner fn
:show-qr fn
:open-activity-center fn
:style override-style
:avatar user-avatar
:counter-label number}
"
[{:keys [type new-notifications? notification-indicator open-profile open-search
open-scanner show-qr open-activity-center style avatar counter-label]}]
(let [button-common-props (get-button-common-props type)]
[rn/view {:style (merge
{:height 56}
style)}
;; Left Section
[rn/touchable-without-feedback {:on-press open-profile}
[rn/view {:style {:position :absolute
:left 20
:top 12}}
[user-avatar/user-avatar
(merge
{:ring? true
:status-indicator? true
:size :small}
avatar)]]]
;; Right Section
[rn/view {:style {:position :absolute
:right 20
:top 12
:flex-direction :row}}
[base-button :i/search open-search :open-search-button button-common-props]
[base-button :i/scan open-scanner :open-scanner-button button-common-props]
[base-button :i/qr-code show-qr :show-qr-button button-common-props]
[rn/view ;; Keep view instead of "[:<>" to make sure relative
;; position is calculated from this view instead of its parent
[hole-view/hole-view {:key new-notifications? ;; Key is required to force removal of holes
:holes (cond
(not new-notifications?) ;; No new notifications, remove holes
[]
(= notification-indicator :unread-dot)
[{:x 37 :y -3 :width 10 :height 10 :borderRadius 5}]
:else
[{:x 33 :y -7 :width 18 :height 18 :borderRadius 7}])}
[base-button :i/activity-center open-activity-center
:open-activity-center-button button-common-props]]
(when new-notifications?
(if (= notification-indicator :counter)
[counter/counter {:outline false
:override-text-color colors/white
:override-bg-color colors/primary-50
:style {:position :absolute
:left 34
:top -6}}
counter-label]
[rn/view {:style {:width 8
:height 8
:border-radius 4
:top -2
:left 38
:position :absolute
:background-color colors/primary-50}}]))]]]))
@@ -0,0 +1,76 @@
(ns quo2.components.notifications.activity-log.style
(:require [quo2.foundations.colors :as colors]))
(def container
{:flex-direction :row
:flex-grow 1
:align-items :flex-start
:padding-top 8
:padding-horizontal 12
:padding-bottom 12})
(def icon
{:height 32
:width 32
:border-radius 100
:margin-top 10
:border-width 1
:border-color colors/white-opa-5
:align-items :center
:justify-content :center})
(def message-title
{:color colors/white-opa-40
:margin-bottom 2})
(def message-body
{:color colors/white})
(def message-container
{:border-radius 12
:margin-top 12
:padding-horizontal 12
:padding-vertical 8
:background-color colors/white-opa-5})
(def buttons-container
{:margin-top 12
:flex-direction :row
:align-items :flex-start})
(def status
{:margin-top 12
:align-items :flex-start
:flex 1})
(defn title [replying?]
{:color colors/white
:flex-shrink 1
:max-width (when-not replying? "60%")})
(def timestamp
{:text-transform :none
:flex-grow 1
:margin-left 8
:color colors/neutral-40})
(def unread-dot
{:background-color colors/primary-50
:border-radius 4
:width 8
:height 8})
(def unread-dot-container
{:margin-left 8
:padding-horizontal 12
:padding-vertical 7})
(def context-container
{:flex-direction :row
:align-items :center
:justify-content :flex-start
:flex-wrap :wrap})
(def top-section-container
{:align-items :center
:flex-direction :row})
@@ -0,0 +1,179 @@
(ns quo2.components.notifications.activity-log.view
(:require [clojure.string :as string]
[quo.core :as quo]
[quo2.components.buttons.button :as button]
[quo2.components.icon :as icon]
[quo2.components.markdown.text :as text]
[quo2.components.tags.status-tags :as status-tags]
[quo2.foundations.colors :as colors]
[quo2.components.notifications.activity-log.style :as style]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im.i18n.i18n :as i18n]))
(def ^:private max-reply-length
280)
(defn- valid-reply?
[reply]
(<= (count reply) max-reply-length))
(defn- activity-reply-text-input
[reply-input on-update-reply]
[rn/view
[rn/view {:style {:margin-top 16
:margin-bottom 8
:flex-direction :row}}
[text/text {:weight :medium
:style {:flex-grow 1
:color colors/neutral-40}}
(i18n/label :t/your-answer)]
[text/text {:style {:flex-shrink 1
:color (if (valid-reply? @reply-input)
colors/neutral-40
colors/danger-60)}}
(str (count @reply-input) "/" max-reply-length)]]
[rn/view
;; TODO(@ilmotta): Replace with quo2 component when available.
;; https://github.com/status-im/status-mobile/issues/14364
[quo/text-input
{:on-change-text #(do (reset! reply-input %)
(when on-update-reply
(on-update-reply %)))
:auto-capitalize :none
:auto-focus true
:accessibility-label :identity-verification-reply-text-input
:placeholder (i18n/label :t/type-something)
:return-key-type :none
:multiline false
:auto-correct false}]]])
(defn- activity-icon
[icon]
[rn/view {:style style/icon}
[icon/icon icon {:color colors/white}]])
(defn- activity-context
[context replying?]
(let [first-line-offset (if replying? 4 0)
gap-between-lines 4]
(into [rn/view {:style (assoc style/context-container :margin-top first-line-offset)}]
(mapcat (fn [detail]
^{:key (hash detail)}
(if (string? detail)
(map (fn [s]
[rn/view {:style {:margin-right 4
:margin-top 0}}
[text/text {:size :paragraph-2
:style {:color colors/white}}
s]])
(string/split detail #"\s+"))
[[rn/view {:margin-right 4
:margin-top gap-between-lines}
detail]]))
context))))
(defn- activity-message
[{:keys [title body]}]
[rn/view {:style style/message-container}
(when title
[text/text {:size :paragraph-2
:accessibility-label :activity-message-title
:style style/message-title}
title])
(if (string? body)
[text/text {:style style/message-body
:accessibility-label :activity-message-body
:size :paragraph-1}
body]
body)])
(defn- activity-buttons
[button-1 button-2 replying? reply-input]
(let [size (if replying? 40 24)
common-style (when replying?
{:padding-vertical 9
:flex-grow 1
:flex-basis 0})]
[rn/view style/buttons-container
(when button-1
[button/button (-> button-1
(assoc :size size)
(update :style merge common-style {:margin-right 8}))
(:label button-1)])
(when button-2
[button/button (-> button-2
(assoc :size size)
(assoc :disabled (and replying? (not (valid-reply? @reply-input))))
(update :style merge common-style))
(:label button-2)])]))
(defn- activity-status
[status]
[rn/view {:style style/status
:accessibility-label :activity-status}
[status-tags/status-tag {:size :small
:label (:label status)
:status status}]])
(defn- activity-title
[title replying?]
[text/text {:weight :semi-bold
:accessibility-label :activity-title
:style (style/title replying?)
:size (if replying? :heading-2 :paragraph-1)}
title])
(defn- activity-timestamp
[timestamp]
[text/text {:size :label
:accessibility-label :activity-timestamp
:style style/timestamp}
timestamp])
(defn- activity-unread-dot
[]
[rn/view {:accessibility-label :activity-unread-indicator
:style style/unread-dot-container}
[rn/view {:style style/unread-dot}]])
(defn- footer
[_]
(let [reply-input (reagent/atom "")]
(fn [{:keys [replying? on-update-reply status button-1 button-2]}]
[:<>
(when replying?
[activity-reply-text-input reply-input on-update-reply])
(cond (some? status)
[activity-status status]
(or button-1 button-2)
[activity-buttons button-1 button-2 replying? reply-input])])))
(defn view
[{:keys [icon
message
context
timestamp
title
replying?
unread?]
:as props}]
[rn/view {:accessibility-label :activity
:style style/container}
(when-not replying?
[activity-icon icon])
[rn/view {:style {:padding-left (when-not replying? 8)
:flex 1}}
[rn/view
[rn/view {:style style/top-section-container}
[activity-title title replying?]
(when-not replying?
[activity-timestamp timestamp])
(when (and unread? (not replying?))
[activity-unread-dot])]
(when context
[activity-context context replying?])]
(when message
[activity-message message])
[footer props]]])
@@ -1,157 +0,0 @@
(ns quo2.components.notifications.activity-logs
(:require [react-native.core :as rn]
[quo2.components.buttons.button :as button]
[quo2.components.icon :as icon]
[quo2.components.markdown.text :as text]
[quo2.components.tags.status-tags :as status-tags]
[quo2.foundations.colors :as colors]))
(defn- activity-icon
[icon]
[rn/view {:height 32
:width 32
:border-radius 100
:margin-top 10
:border-width 1
:border-color colors/white-opa-5
:flex-direction :column
:align-items :center
:justify-content :center}
[icon/icon icon {:color colors/white}]])
(defn- activity-unread-dot
[]
[rn/view {:margin-left 14
:margin-right 6
:background-color colors/primary-50
:width 8
:height 8
:border-radius 4}])
(defn- activity-context
[context]
(let [margin-top 4]
(into [rn/view {:flex 1
:flex-direction :row
:align-items :center
:flex-wrap :wrap
:margin-top (+ 4 (- margin-top))}]
(map-indexed (fn [index detail]
^{:key index}
[rn/view {:margin-right 4
:margin-top margin-top}
(if (string? detail)
[text/text {:size :paragraph-2}
detail]
detail)])
context))))
(defn- activity-message
[{:keys [title body]}]
[rn/view {:border-radius 12
:margin-top 12
:padding-horizontal 12
:padding-vertical 8
:background-color colors/white-opa-5
:flex 1
:flex-direction :column}
(when title
[text/text {:size :paragraph-2
:style {:color colors/white-opa-40}}
title])
(if (string? body)
[text/text {:style {:color colors/white}
:size :paragraph-1}
body]
body)])
(defn- activity-buttons
[button-1 button-2]
(let [size 24
common-style {:padding-top 3
:padding-right 8
:padding-bottom 4
:padding-left 8}]
[rn/view {:margin-top 12
:flex 1
:flex-direction :row
:align-items :flex-start}
(when button-1
[button/button (-> button-1
(assoc :size size)
(assoc-in [:style :margin-right] 8)
(update :style merge common-style))
(:label button-1)])
(when button-2
[button/button (-> button-2
(assoc :size size)
(update :style merge common-style))
(:label button-2)])]))
(defn- activity-status
[status]
[rn/view {:margin-top 12
:align-items :flex-start
:flex 1}
[status-tags/status-tag {:size :small
:label (:label status)
:status status}]])
(defn- activity-title
[title]
[text/text {:weight :semi-bold
:style {:color colors/white}
:size :paragraph-1}
title])
(defn- activity-timestamp
[timestamp]
[rn/view {:margin-left 8}
[text/text {:size :label
:style {:text-transform :none
:color colors/neutral-40}}
timestamp]])
(defn activity-log
[{:keys [button-1
button-2
icon
message
status
context
timestamp
title
unread?]}]
[rn/view {:flex-direction :row
:flex 1
:border-radius 16
:padding-top 8
:padding-horizontal 12
:padding-bottom 12
:background-color (when unread?
colors/primary-50-opa-10)}
[activity-icon icon]
[rn/view {:flex-direction :column
:padding-left 8
:flex 1}
[rn/view {:flex 1
:align-items :center
:flex-direction :row}
[rn/view {:flex 1
:align-items :center
:flex-direction :row}
[rn/view {:flex-shrink 1}
[activity-title title]]
[activity-timestamp timestamp]]
(when unread?
[activity-unread-dot])]
(when context
[activity-context context])
(when message
[activity-message message])
(cond
(some? status)
[activity-status status]
(or button-1 button-2)
[activity-buttons button-1 button-2])]])
@@ -10,6 +10,8 @@
:position :absolute
:right 22
:border-radius 6
:justify-content :center
:align-items :center
:background-color (colors/theme-colors colors/primary-50 colors/primary-60)}
style)}
[rn/text {:style (merge typography/font-medium typography/label {:color colors/white :text-align :center})} count]]))
@@ -0,0 +1,84 @@
(ns quo2.components.selectors.--tests--.selectors-component-spec
(:require ["@testing-library/react-native" :as rtl]
[quo2.components.selectors.selectors :as selectors]
[reagent.core :as reagent]))
(defn render-toggle
([]
(render-toggle {}))
([opts]
(rtl/render (reagent/as-element [selectors/toggle opts]))))
(defn render-checkbox
([]
(render-checkbox {}))
([opts]
(rtl/render (reagent/as-element [selectors/checkbox opts]))))
(defn render-checkbox-prefill
([]
(render-checkbox-prefill {}))
([opts]
(rtl/render (reagent/as-element [selectors/checkbox-prefill opts]))))
(defn render-radio
([]
(render-radio {}))
([opts]
(rtl/render (reagent/as-element [selectors/radio opts]))))
(js/global.test "default render of toggle component"
(fn []
(render-toggle)
(-> (js/expect (rtl/screen.getByTestId "toggle-component"))
(.toBeTruthy))))
(js/global.test "toggle component on change is working"
(let [mock-fn (js/jest.fn)]
(fn []
(render-toggle {:on-change mock-fn})
(rtl/fireEvent.press (rtl/screen.getByTestId "toggle-component"))
(-> (js/expect mock-fn)
(.toHaveBeenCalledTimes 1)))))
(js/global.test "default render of radio component"
(fn []
(render-radio)
(-> (js/expect (rtl/screen.getByTestId "radio-component"))
(.toBeTruthy))))
(js/global.test "radio component on change is working"
(let [mock-fn (js/jest.fn)]
(fn []
(render-radio {:on-change mock-fn})
(rtl/fireEvent.press (rtl/screen.getByTestId "radio-component"))
(-> (js/expect mock-fn)
(.toHaveBeenCalledTimes 1)))))
(js/global.test "default render of checkbox component"
(fn []
(render-checkbox)
(-> (js/expect (rtl/screen.getByTestId "checkbox-component"))
(.toBeTruthy))))
(js/global.test "checkbox component on change is working"
(let [mock-fn (js/jest.fn)]
(fn []
(render-checkbox {:on-change mock-fn})
(rtl/fireEvent.press (rtl/screen.getByTestId "checkbox-component"))
(-> (js/expect mock-fn)
(.toHaveBeenCalledTimes 1)))))
(js/global.test "default render of checkbox-prefill component"
(fn []
(render-checkbox-prefill)
(-> (js/expect (rtl/screen.getByTestId "checkbox-prefill-component"))
(.toBeTruthy))))
(js/global.test "checkbox-prefill component on change is working"
(let [mock-fn (js/jest.fn)]
(fn []
(render-checkbox-prefill {:on-change mock-fn})
(rtl/fireEvent.press (rtl/screen.getByTestId "checkbox-prefill-component"))
(-> (js/expect mock-fn)
(.toHaveBeenCalledTimes 1)))))
+8 -4
View File
@@ -43,7 +43,8 @@
(colors/alpha colors/neutral-20 (if disabled? 0.3 1))
(colors/alpha colors/neutral-70 (if disabled? 0.3 1))))})
:accessibility-label (str "checkbox-" (if @checked? "on" "off"))
:accessibility-role :checkbox}
:accessibility-role :checkbox
:testID "checkbox-prefill-component"}
(when @checked?
[rn/view {:style
{:height 20
@@ -82,7 +83,8 @@
:border-color (if @checked? :none
(get-color @checked? disabled? blurred-background?))}
:accessibility-label (str "checkbox-" (if @checked? "on" "off"))
:accessibility-role :checkbox}
:accessibility-role :checkbox
:testID "checkbox-component"}
(when @checked?
[rn/view {:style
{:height 20
@@ -108,7 +110,8 @@
(colors/theme-colors colors/white
(colors/alpha colors/neutral-80 0.4)))})
:accessibility-label (str "radio-" (if @checked? "on" "off"))
:accessibility-role :checkbox}
:accessibility-role :checkbox
:testID "radio-component"}
[rn/view {:style
{:margin-left :auto
@@ -133,7 +136,8 @@
:border-radius 20
:background-color (get-color @checked? disabled? blurred-background?)})
:accessibility-label (str "toggle-" (if @checked? "on" "off"))
:accessibility-role :checkbox}
:accessibility-role :checkbox
:testID "toggle-component"}
[rn/view {:style
{:margin-left (if @checked? 12 2)
:height 16
+21 -17
View File
@@ -3,7 +3,8 @@
[react-native.core :as rn]
[quo2.components.tabs.tab :as tab]
[reagent.core :as reagent]
[utils :as utils]
[utils.number :as utils.number]
[utils.collection :as utils.collection]
[quo2.foundations.colors :as colors]
[quo2.components.notifications.notification-dot :refer [notification-dot]]
[react-native.masked-view :as masked-view]
@@ -11,26 +12,29 @@
(def default-tab-size 32)
(defn indicator []
[rn/view {:position :absolute
:z-index 1
:right -2
:top -2
:width 10
:height 10
:border-radius 5
:justify-content :center
:align-items :center
:background-color (colors/theme-colors colors/neutral-5 colors/neutral-95)}
[notification-dot]])
(defn tabs [{:keys [default-active on-change style]}]
(let [active-tab-id (reagent/atom default-active)]
(fn [{:keys [data size] :or {size default-tab-size}}]
[rn/view (merge {:flex-direction :row} style)
(doall
(for [{:keys [label id new-info accessibility-label]} data]
(for [{:keys [label id notification-dot? accessibility-label]} data]
^{:key id}
[rn/view {:style {:margin-right (if (= size default-tab-size) 12 8)}}
(when new-info
[rn/view {:position :absolute
:z-index 1
:right -2
:top -2
:width 10
:height 10
:border-radius 5
:justify-content :center
:align-items :center
:background-color (colors/theme-colors colors/neutral-5 colors/neutral-95)}
[notification-dot]])
(when notification-dot?
[indicator])
[tab/tab
{:id id
:size size
@@ -50,7 +54,7 @@
;; Truncate to avoid unnecessary rendering.
(if (> fade-percentage 0.99)
0.99
(utils/naive-round fade-percentage 2))))
(utils.number/naive-round fade-percentage 2))))
(defn scrollable-tabs
"Just like the component `tabs`, displays horizontally scrollable tabs with
@@ -120,7 +124,7 @@
:scroll-on-press?
:size)
(when scroll-on-press?
{:initial-scroll-index (utils/first-index #(= @active-tab-id (:id %)) data)})
{:initial-scroll-index (utils.collection/first-index #(= @active-tab-id (:id %)) data)})
{:ref #(reset! flat-list-ref %)
:extra-data (str @active-tab-id)
:horizontal true
@@ -154,7 +158,7 @@
:on-press (fn [id]
(reset! active-tab-id id)
(when scroll-on-press?
(.scrollToIndex @flat-list-ref
(.scrollToIndex ^js @flat-list-ref
#js {:animated true
:index index
:viewPosition 0.5}))
+3 -2
View File
@@ -104,11 +104,12 @@
(defn tag
[_ _]
(fn [{:keys [locked tokens size background-color]
(fn [{:keys [locked tokens size background-color on-press]
:or {size 24}}]
[base-tag/base-tag {:background-color background-color
:size size
:type :permission}
:type :permission
:on-press on-press}
[rn/view {:flex-direction :row
:align-items :center
:justify-content :flex-end}
+25 -4
View File
@@ -15,8 +15,10 @@
quo2.components.community.community-list-view
quo2.components.community.community-view
quo2.components.community.discover-card
quo2.components.community.token-gating
quo2.components.counter.counter
quo2.components.dividers.divider-label
quo2.components.dividers.date
quo2.components.dividers.new-messages
quo2.components.drawers.action-drawers
quo2.components.dropdowns.dropdown
@@ -28,12 +30,19 @@
quo2.components.messages.gap
quo2.components.messages.system-message
quo2.components.reactions.reaction
quo2.components.notifications.activity-logs
quo2.components.notifications.activity-log.view
quo2.components.notifications.info-count
quo2.components.notifications.notification-dot
quo2.components.tags.tags
quo2.components.tags.context-tags
quo2.components.tabs.tabs
quo2.components.tabs.account-selector))
quo2.components.tabs.account-selector
quo2.components.navigation.floating-shell-button
quo2.components.tags.status-tags
quo2.components.navigation.page-nav
quo2.components.selectors.disclaimer
quo2.components.selectors.selectors
quo2.components.loaders.skeleton))
(def button quo2.components.buttons.button/button)
(def dynamic-button quo2.components.buttons.dynamic-button/dynamic-button)
@@ -50,9 +59,18 @@
(def system-message quo2.components.messages.system-message/system-message)
(def reaction quo2.components.reactions.reaction/reaction)
(def tags quo2.components.tags.tags/tags)
(def user-avatar-tag quo2.components.tags.context-tags/user-avatar-tag)
(def context-tag quo2.components.tags.context-tags/context-tag)
(def group-avatar-tag quo2.components.tags.context-tags/group-avatar-tag)
(def tabs quo2.components.tabs.tabs/tabs)
(def scrollable-tabs quo2.components.tabs.tabs/scrollable-tabs)
(def account-selector quo2.components.tabs.account-selector/account-selector)
(def floating-shell-button quo2.components.navigation.floating-shell-button/floating-shell-button)
(def status-tag quo2.components.tags.status-tags/status-tag)
(def page-nav quo2.components.navigation.page-nav/page-nav)
(def disclaimer quo2.components.selectors.disclaimer/disclaimer)
(def checkbox quo2.components.selectors.selectors/checkbox)
(def skeleton quo2.components.loaders.skeleton/skeleton)
;;;; AVATAR
(def account-avatar quo2.components.avatars.account-avatar/account-avatar)
@@ -70,11 +88,14 @@
(def community-stats quo2.components.community.community-view/community-stats)
(def community-tags quo2.components.community.community-view/community-tags)
(def community-title quo2.components.community.community-view/community-title)
(def permission-tag-container quo2.components.community.community-view/permission-tag-container)
(def discover-card quo2.components.community.discover-card/discover-card)
(def token-gating quo2.components.community.token-gating/token-gating)
;;;; DIVIDERS
(def divider-label quo2.components.dividers.divider-label/divider-label)
(def new-messages quo2.components.dividers.new-messages/new-messages)
(def divider-date quo2.components.dividers.date/date)
;;;; LIST ITEMS
(def channel-list-item quo2.components.list-items.channel/list-item)
@@ -82,6 +103,6 @@
(def preview-list quo2.components.list-items.preview-list/preview-list)
;;;; NOTIFICATIONS
(def activity-log quo2.components.notifications.activity-logs/activity-log)
(def activity-log quo2.components.notifications.activity-log.view/view)
(def info-count quo2.components.notifications.info-count/info-count)
(def notification-dot quo2.components.notifications.notification-dot/notification-dot)
(def notification-dot quo2.components.notifications.notification-dot/notification-dot)
+4
View File
@@ -0,0 +1,4 @@
(ns quo2.core-spec
(:require [quo2.components.selectors.--tests--.selectors-component-spec]
[quo2.components.counter.--tests--.counter-component-spec]
[quo2.components.drawers.--tests--.action-drawers-component-spec]))
+24 -2
View File
@@ -1,15 +1,19 @@
(ns react-native.core
(:require [reagent.core :as reagent]
["react-native" :as react-native]
[react-native.flat-list :as flat-list]))
["@react-native-community/blur" :as blur]
[react-native.flat-list :as flat-list]
[react-native.section-list :as section-list]
[react-native.platform :as platform]))
(def app-state ^js (.-AppState ^js react-native))
(def blur-view (reagent/adapt-react-class (.-BlurView blur)))
(def view (reagent/adapt-react-class (.-View ^js react-native)))
(def scroll-view (reagent/adapt-react-class (.-ScrollView ^js react-native)))
(def image (reagent/adapt-react-class (.-Image ^js react-native)))
(def text (reagent/adapt-react-class (.-Text ^js react-native)))
(def text-input (reagent/adapt-react-class (.-TextInput ^js react-native)))
(def text-input (reagent/adapt-react-class (.-TextInput ^js react-native)))
(def touchable-opacity (reagent/adapt-react-class (.-TouchableOpacity ^js react-native)))
(def touchable-highlight (reagent/adapt-react-class (.-TouchableHighlight ^js react-native)))
@@ -17,6 +21,8 @@
(def flat-list flat-list/flat-list)
(def section-list section-list/section-list)
(def activity-indicator (reagent/adapt-react-class (.-ActivityIndicator ^js react-native)))
(def modal (reagent/adapt-react-class (.-Modal ^js react-native)))
@@ -48,3 +54,19 @@
(defn get-window []
(js->clj (.get (.-Dimensions ^js react-native) "window") :keywordize-keys true))
(def status-bar (.-StatusBar ^js react-native))
(defn hw-back-add-listener [callback]
(.addEventListener (.-BackHandler ^js react-native) "hardwareBackPress" callback))
(defn hw-back-remove-listener [callback]
(.removeEventListener (.-BackHandler ^js react-native) "hardwareBackPress" callback))
(def keyboard-avoiding-view-class (reagent/adapt-react-class (.-KeyboardAvoidingView react-native)))
(defn keyboard-avoiding-view [props & children]
(into [keyboard-avoiding-view-class
(merge (when platform/ios? {:behavior :padding})
props)]
children))
+3
View File
@@ -27,6 +27,9 @@
(defn show-overlay [comp]
(.showOverlay Navigation (clj->js comp)))
(defn pop-to-root [tab]
(.popToRoot Navigation (clj->js tab)))
(defn dissmiss-overlay [comp]
(.catch (.dismissOverlay Navigation comp) #()))
+4
View File
@@ -5,5 +5,9 @@
(def os (when platform (.-OS ^js platform)))
(def version (when platform (.-Version ^js platform)))
(def android? (= os "android"))
(def ios? (= os "ios"))
(def low-device? (and android? (< version 29)))
+9 -1
View File
@@ -2,8 +2,9 @@
(:require ["react-native" :as rn]
[reagent.core :as reagent]
[clojure.string :as string]
["react-native-linear-gradient" :default LinearGradient]
["react-native-reanimated" :default reanimated
:refer (useSharedValue useAnimatedStyle withTiming withDelay withSpring Easing Keyframe)]))
:refer (useSharedValue useAnimatedStyle withTiming withDelay withSpring withRepeat Easing Keyframe)]))
;; Animated Components
(def create-animated-component (comp reagent/adapt-react-class (.-createAnimatedComponent reanimated)))
@@ -12,6 +13,8 @@
(def image (reagent/adapt-react-class (.-Image reanimated)))
(def touchable-opacity (create-animated-component (.-TouchableOpacity ^js rn)))
(def linear-gradient (create-animated-component LinearGradient))
;; Hooks
(def use-shared-value useSharedValue)
(def use-animated-style useAnimatedStyle)
@@ -21,6 +24,7 @@
(def with-delay withDelay)
(def with-spring withSpring)
(def key-frame Keyframe)
(def with-repeat withRepeat)
;; Easings
(def bezier (.-bezier ^js Easing))
@@ -68,3 +72,7 @@
(defn animate-shared-value-with-delay [anim val duration easing delay]
(set-shared-value anim (with-delay delay (with-timing val (js-obj "duration" duration
"easing" (get easings easing))))))
(defn animate-shared-value-with-repeat [anim val duration easing number-of-repetitions reverse?]
(set-shared-value anim (with-repeat (with-timing val (js-obj "duration" duration
"easing" (get easings easing))) number-of-repetitions reverse?)))
+1 -1
View File
@@ -7,7 +7,7 @@
(defn consumer [component]
[consumer-raw
(fn [insets]
(fn [^js insets]
(reagent/as-element
[component (js->clj insets :keywordize-keys true)]))])
+40
View File
@@ -0,0 +1,40 @@
(ns react-native.section-list
(:require [reagent.core :as reagent]
[react-native.flat-list :as flat-list]
["react-native" :as react-native]))
(def section-list-class (reagent/adapt-react-class (.-SectionList react-native)))
(def memo-wrap-render-fn
(memoize
(fn [f render-data]
(fn [^js data]
(reagent/as-element [f (.-item data) (.-index data) (.-separators data) render-data])))))
(defn- wrap-render-section-header-fn [f]
(fn [^js data]
(let [^js section (.-section data)]
(reagent/as-element [f {:title (.-title section)
:data (.-data section)}]))))
(defn- wrap-per-section-render-fn [props]
(update
(if-let [f (:render-fn props)]
(assoc (dissoc props :render-fn :render-data) :renderItem (memo-wrap-render-fn f (:render-data props)))
props)
:data to-array))
(defn section-list
"A wrapper for SectionList.
To render something on empty sections, use renderSectionFooter and conditionaly
render on empty data
See https://facebook.github.io/react-native/docs/sectionlist.html"
[{:keys [sections render-section-header-fn render-section-footer-fn style] :as props}]
[section-list-class
(merge (flat-list/base-list-props props)
props
(when render-section-footer-fn
{:renderSectionFooter (wrap-render-section-header-fn render-section-footer-fn)})
{:sections (clj->js (map wrap-per-section-render-fn sections))
:renderSectionHeader (wrap-render-section-header-fn render-section-header-fn)
:style style})])
-158
View File
@@ -1,158 +0,0 @@
(ns status-im.activity-center.core
(:require [re-frame.core :as rf]
[status-im.activity-center.notification-types :as types]
[status-im.data-store.activities :as data-store.activities]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.fx :as fx]
[taoensso.timbre :as log]))
;;;; Notification reconciliation
(defn- update-notifications
"Insert `new-notifications` in `db-notifications`.
Although correct, this is a naive implementation for reconciling notifications
because for every notification in `new-notifications`, linear scans will be
performed to remove it and sorting will be performed for every new insertion.
If the number of existing notifications cached in the app db becomes
~excessively~ big, this implementation will probably need to be revisited."
[db-notifications new-notifications]
(reduce (fn [acc {:keys [id type read] :as notification}]
(let [filter-status (if read :read :unread)
remove-notification (fn [data]
(remove #(= id (:id %)) data))
insert-and-sort (fn [data]
(->> notification
(conj data)
(sort-by (juxt :timestamp :id))
reverse))]
(as-> acc $
(update-in $ [type :read :data] remove-notification)
(update-in $ [type :unread :data] remove-notification)
(update-in $ [types/no-type :read :data] remove-notification)
(update-in $ [types/no-type :unread :data] remove-notification)
(if (or (:dismissed notification) (:accepted notification))
$
(-> $ (update-in [type filter-status :data] insert-and-sort)
(update-in [types/no-type filter-status :data] insert-and-sort))))))
db-notifications
new-notifications))
(fx/defn notifications-reconcile
{:events [:activity-center.notifications/reconcile]}
[{:keys [db]} new-notifications]
(when (seq new-notifications)
{:db (update-in db [:activity-center :notifications]
update-notifications new-notifications)}))
;;;; Contact verification
(fx/defn contact-verification-decline
{:events [:activity-center.contact-verification/decline]}
[_ contact-verification-id]
{::json-rpc/call [{:method "wakuext_declineContactVerificationRequest"
:params [contact-verification-id]
:on-success #(rf/dispatch [:activity-center.contact-verification/decline-success %])
:on-error #(rf/dispatch [:activity-center.contact-verification/decline-error contact-verification-id %])}]})
(fx/defn contact-verification-decline-success
{:events [:activity-center.contact-verification/decline-success]}
[cofx response]
(->> response
:activityCenterNotifications
(map data-store.activities/<-rpc)
(notifications-reconcile cofx)))
(fx/defn contact-verification-decline-error
{:events [:activity-center.contact-verification/decline-error]}
[_ contact-verification-id error]
(log/warn "Failed to decline contact verification"
{:contact-verification-id contact-verification-id
:error error})
nil)
;;;; Notifications fetching and pagination
(def defaults
{:filter-status :unread
:filter-type types/no-type
:notifications-per-page 10})
(def start-or-end-cursor
"")
(defn- valid-cursor?
[cursor]
(and (some? cursor)
(not= cursor start-or-end-cursor)))
(def ^:const status-read 1)
(def ^:const status-unread 2)
(def ^:const status-all 3)
(defn status [filter-status]
(case filter-status
:read status-read
:unread status-unread
:all status-all
99))
(fx/defn notifications-fetch
[{:keys [db]} {:keys [cursor filter-type filter-status reset-data?]}]
(when-not (get-in db [:activity-center :notifications filter-type filter-status :loading?])
{:db (assoc-in db [:activity-center :notifications filter-type filter-status :loading?] true)
::json-rpc/call [{:method "wakuext_activityCenterNotificationsBy"
:params [cursor (defaults :notifications-per-page) filter-type (status filter-status)]
:on-success #(rf/dispatch [:activity-center.notifications/fetch-success filter-type filter-status reset-data? %])
:on-error #(rf/dispatch [:activity-center.notifications/fetch-error filter-type filter-status %])}]}))
(fx/defn notifications-fetch-first-page
{:events [:activity-center.notifications/fetch-first-page]}
[{:keys [db] :as cofx} {:keys [filter-type filter-status]}]
(let [filter-type (or filter-type
(get-in db [:activity-center :filter :type]
(defaults :filter-type)))
filter-status (or filter-status
(get-in db [:activity-center :filter :status]
(defaults :filter-status)))]
(fx/merge cofx
{:db (-> db
(assoc-in [:activity-center :filter :type] filter-type)
(assoc-in [:activity-center :filter :status] filter-status))}
(notifications-fetch {:cursor start-or-end-cursor
:filter-type filter-type
:filter-status filter-status
:reset-data? true}))))
(fx/defn notifications-fetch-next-page
{:events [:activity-center.notifications/fetch-next-page]}
[{:keys [db] :as cofx}]
(let [{:keys [type status]} (get-in db [:activity-center :filter])
{:keys [cursor]} (get-in db [:activity-center :notifications type status])]
(when (valid-cursor? cursor)
(notifications-fetch cofx {:cursor cursor
:filter-type type
:filter-status status
:reset-data? false}))))
(fx/defn notifications-fetch-success
{:events [:activity-center.notifications/fetch-success]}
[{:keys [db]}
filter-type
filter-status
reset-data?
{:keys [cursor notifications]}]
(let [processed (map data-store.activities/<-rpc notifications)]
{:db (-> db
(assoc-in [:activity-center :notifications filter-type filter-status :cursor] cursor)
(update-in [:activity-center :notifications filter-type filter-status] dissoc :loading?)
(update-in [:activity-center :notifications filter-type filter-status :data]
(if reset-data?
(constantly processed)
#(concat %1 processed))))}))
(fx/defn notifications-fetch-error
{:events [:activity-center.notifications/fetch-error]}
[{:keys [db]} filter-type filter-status error]
(log/warn "Failed to load Activity Center notifications" error)
{:db (update-in db [:activity-center :notifications filter-type filter-status] dissoc :loading?)})
@@ -1,492 +0,0 @@
(ns status-im.activity-center.core-test
(:require [cljs.test :refer [deftest is testing]]
[day8.re-frame.test :as rf-test]
[re-frame.core :as rf]
[status-im.activity-center.notification-types :as types]
[status-im.ethereum.json-rpc :as json-rpc]
status-im.events
[status-im.test-helpers :as h]
[status-im.utils.config :as config]))
(defn setup []
(h/register-helper-events)
(rf/dispatch [:setup/app-started]))
;;;; Contact verification
(deftest contact-verification-decline-test
(with-redefs [config/new-activity-center-enabled? true]
(testing "successfully declines and reconciles returned notification"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])
contact-verification-id 24
expected-notification {:accepted false
:author "0x04d03f"
:chat-id "0x04d03f"
:contact-verification-status 3
:dismissed false
:id 24
:last-message nil
:message {:command-parameters nil
:content {:chat-id nil
:ens-name nil
:image nil
:line-count nil
:links nil
:parsed-text nil
:response-to nil
:rtl? nil
:sticker nil
:text nil}
:outgoing false
:outgoing-status nil
:quoted-message nil}
:name "0x04d03f"
:read true
:reply-message nil
:timestamp 1666647286000
:type types/contact-verification}]
(h/stub-fx-with-callbacks
::json-rpc/call
:on-success (constantly {:activityCenterNotifications
[{:accepted false
:author "0x04d03f"
:chatId "0x04d03f"
:contactVerificationStatus 3
:dismissed false
:id contact-verification-id
:message {}
:name "0x04d03f"
:read true
:timestamp 1666647286000
:type types/contact-verification}]}))
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:activity-center.contact-verification/decline contact-verification-id])
(is (= {:method "wakuext_declineContactVerificationRequest"
:params [contact-verification-id]}
(-> @spy-queue
(get-in [0 :args 0])
(select-keys [:method :params]))))
(is (= {types/no-type
{:read {:data [expected-notification]}
:unread {:data []}}
types/contact-verification
{:read {:data [expected-notification]}
:unread {:data []}}}
(get-in (h/db) [:activity-center :notifications]))))))
(testing "logs failure"
(rf-test/run-test-sync
(setup)
(let [contact-verification-id 666]
(h/using-log-test-appender
(fn [logs]
(h/stub-fx-with-callbacks ::json-rpc/call :on-error (constantly :fake-error))
(rf/dispatch [:activity-center.contact-verification/decline contact-verification-id])
(is (= {:args ["Failed to decline contact verification"
{:contact-verification-id contact-verification-id
:error :fake-error}]
:level :warn}
(last @logs))))))))))
;;;; Notification reconciliation
(deftest notifications-reconcile-test
(with-redefs [config/new-activity-center-enabled? true]
(testing "does nothing when there are no new notifications"
(rf-test/run-test-sync
(setup)
(let [notifications {types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type types/one-to-one-chat}
{:id "0x2"
:read true
:type types/one-to-one-chat}]}
:unread {:cursor ""
:data [{:id "0x3"
:read false
:type types/one-to-one-chat}]}}
types/private-group-chat
{:unread {:cursor ""
:data [{:id "0x4"
:read false
:type types/private-group-chat}]}}}]
(rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications])
(rf/dispatch [:activity-center.notifications/reconcile nil])
(is (= notifications (get-in (h/db) [:activity-center :notifications]))))))
(testing "removes dismissed or accepted notifications"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1" :read true :type types/one-to-one-chat}
{:id "0x2" :read true :type types/one-to-one-chat}]}
:unread {:cursor ""
:data [{:id "0x3" :read false :type types/one-to-one-chat}]}}
types/private-group-chat
{:unread {:cursor ""
:data [{:id "0x4" :read false :type types/private-group-chat}
{:id "0x6" :read false :type types/private-group-chat}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1"
:read true
:type types/one-to-one-chat
:dismissed true}
{:id "0x3"
:read false
:type types/one-to-one-chat
:accepted true}
{:id "0x4"
:read false
:type types/private-group-chat
:dismissed true}
{:id "0x5"
:read false
:type types/private-group-chat
:accepted true}]])
(is (= {types/no-type
{:read {:data []}
:unread {:data []}}
types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x2"
:read true
:type types/one-to-one-chat}]}
:unread {:cursor ""
:data []}}
types/private-group-chat
{:read {:data []}
:unread {:cursor ""
:data [{:id "0x6"
:read false
:type types/private-group-chat}]}}}
(get-in (h/db) [:activity-center :notifications])))))
(testing "replaces old notifications with newly arrived ones"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/no-type
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type types/one-to-one-chat}]}
:unread {:cursor ""
:data [{:id "0x4"
:read false
:type types/private-group-chat}
{:id "0x6"
:read false
:type types/private-group-chat}]}}
types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type types/one-to-one-chat}]}}
types/private-group-chat
{:unread {:cursor ""
:data [{:id "0x4"
:read false
:type types/private-group-chat}
{:id "0x6"
:read false
:type types/private-group-chat}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1"
:read true
:type types/one-to-one-chat
:last-message {}}
{:id "0x4"
:read false
:type types/private-group-chat
:author "0xabc"}
{:id "0x6"
:read false
:type types/private-group-chat}]])
(is (= {types/no-type
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type types/one-to-one-chat
:last-message {}}]}
:unread {:cursor ""
:data [{:id "0x6"
:read false
:type types/private-group-chat}
{:id "0x4"
:read false
:type types/private-group-chat
:author "0xabc"}]}}
types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type types/one-to-one-chat
:last-message {}}]}
:unread {:data []}}
types/private-group-chat
{:read {:data []}
:unread {:cursor ""
:data [{:id "0x6"
:read false
:type types/private-group-chat}
{:id "0x4"
:read false
:type types/private-group-chat
:author "0xabc"}]}}}
(get-in (h/db) [:activity-center :notifications])))))
(testing "reconciles notifications that switched their read/unread status"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type types/one-to-one-chat}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1"
:read false
:type types/one-to-one-chat}]])
(is (= {types/no-type
{:read {:data []}
:unread {:data [{:id "0x1"
:read false
:type types/one-to-one-chat}]}}
types/one-to-one-chat
{:read {:cursor ""
:data []}
:unread {:data [{:id "0x1"
:read false
:type types/one-to-one-chat}]}}}
(get-in (h/db) [:activity-center :notifications])))))
;; Sorting by timestamp and ID is compatible with what the backend does when
;; returning paginated results.
(testing "sorts notifications by timestamp and id in descending order"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1" :read true :type types/one-to-one-chat :timestamp 1}
{:id "0x2" :read true :type types/one-to-one-chat :timestamp 1}]}
:unread {:cursor ""
:data [{:id "0x3" :read false :type types/one-to-one-chat :timestamp 50}
{:id "0x4" :read false :type types/one-to-one-chat :timestamp 100}
{:id "0x5" :read false :type types/one-to-one-chat :timestamp 100}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1" :read true :type types/one-to-one-chat :timestamp 1 :last-message {}}
{:id "0x4" :read false :type types/one-to-one-chat :timestamp 100 :last-message {}}]])
(is (= {types/no-type
{:read {:data [{:id "0x1"
:read true
:type types/one-to-one-chat
:timestamp 1
:last-message {}}]}
:unread {:data [{:id "0x4"
:read false
:type types/one-to-one-chat
:timestamp 100
:last-message {}}]}}
types/one-to-one-chat
{:read {:cursor ""
:data [{:id "0x2"
:read true
:type types/one-to-one-chat
:timestamp 1}
{:id "0x1"
:read true
:type types/one-to-one-chat
:timestamp 1
:last-message {}}]}
:unread {:cursor ""
:data [{:id "0x5"
:read false
:type types/one-to-one-chat
:timestamp 100}
{:id "0x4"
:read false
:type types/one-to-one-chat
:timestamp 100
:last-message {}}
{:id "0x3"
:read false
:type types/one-to-one-chat
:timestamp 50}]}}}
(get-in (h/db) [:activity-center :notifications])))))))
;;;; Notifications fetching and pagination
(deftest notifications-fetch-test
(with-redefs [config/new-activity-center-enabled? true]
(testing "fetches first page"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks
::json-rpc/call
:on-success (constantly {:cursor "10"
:notifications [{:id "0x1"
:type types/one-to-one-chat
:read false
:chatId "0x9"}]}))
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:activity-center.notifications/fetch-first-page
{:filter-type types/one-to-one-chat}])
(is (= :unread (get-in (h/db) [:activity-center :filter :status])))
(is (= "" (get-in @spy-queue [0 :args 0 :params 0]))
"Should be called with empty cursor when fetching first page")
(is (= {types/one-to-one-chat
{:unread {:cursor "10"
:data [{:chat-id "0x9"
:chat-name nil
:chat-type types/one-to-one-chat
:group-chat false
:id "0x1"
:public? false
:last-message nil
:message nil
:read false
:reply-message nil
:type types/one-to-one-chat}]}}}
(get-in (h/db) [:activity-center :notifications]))))))
(testing "does not fetch next page when pagination cursor reached the end"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/one-to-one-chat :unread :cursor]
""])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= [] @spy-queue)))))
;; The cursor can be nil sometimes because the reconciliation doesn't care
;; about updating the cursor value, but we have to make sure the next page is
;; only fetched if the current cursor is valid.
(testing "does not fetch next page when cursor is nil"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/one-to-one-chat :unread :cursor]
nil])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= [] @spy-queue)))))
(testing "fetches next page when pagination cursor is not empty"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks
::json-rpc/call
:on-success (constantly {:cursor ""
:notifications [{:id "0x1"
:type types/mention
:read false
:chatId "0x9"}]}))
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/mention])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/mention :unread :cursor]
"10"])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= "wakuext_activityCenterNotificationsBy" (get-in @spy-queue [0 :args 0 :method])))
(is (= "10" (get-in @spy-queue [0 :args 0 :params 0]))
"Should be called with current cursor")
(is (= {types/mention
{:unread {:cursor ""
:data [{:chat-id "0x9"
:chat-name nil
:chat-type 3
:id "0x1"
:last-message nil
:message nil
:read false
:reply-message nil
:type types/mention}]}}}
(get-in (h/db) [:activity-center :notifications]))))))
(testing "does not fetch next page while it is still loading"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:read])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/one-to-one-chat :read :cursor]
"10"])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/one-to-one-chat :read :loading?]
true])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= [] @spy-queue)))))
(testing "resets loading flag after an error"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks ::json-rpc/call :on-error (constantly :fake-error))
(h/spy-event-fx spy-queue :activity-center.notifications/fetch-error)
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/one-to-one-chat :unread :cursor]
""])
(rf/dispatch [:activity-center.notifications/fetch-first-page])
(is (nil? (get-in (h/db) [:activity-center :notifications types/one-to-one-chat :unread :loading?])))
(is (= [:activity-center.notifications/fetch-error
types/one-to-one-chat
:unread
:fake-error]
(:args (last @spy-queue)))))))))
+6 -6
View File
@@ -6,7 +6,7 @@
[status-im.add-new.db :as db]
[status-im.utils.random :as random]
[status-im.utils.utils :as utils]
[status-im.utils.fx :as fx]
[utils.re-frame :as rf]
[status-im.chat.models :as chat]
[status-im.i18n.i18n :as i18n]
[status-im.contact.core :as contact]
@@ -24,7 +24,7 @@
;;NOTE we want to handle only last resolve
(def resolve-last-id (atom nil))
(fx/defn new-chat-set-new-identity
(rf/defn new-chat-set-new-identity
{:events [:new-chat/set-new-identity]}
[{db :db} new-identity-raw new-ens-name id]
(let [ens-error (and (= new-identity-raw "0x") (not (string/blank? new-ens-name)))]
@@ -61,12 +61,12 @@
new-identity
@resolve-last-id])}})))))))
(fx/defn clear-new-identity
(rf/defn clear-new-identity
{:events [::clear-new-identity ::new-chat-focus]}
[{:keys [db]}]
{:db (dissoc db :contacts/new-identity)})
(fx/defn qr-code-handled
(rf/defn qr-code-handled
{:events [::qr-code-handled]}
[{:keys [db] :as cofx} {:keys [type public-key chat-id data ens-name]} {:keys [new-contact? nickname] :as opts}]
(let [public-key? (and (string? data)
@@ -79,7 +79,7 @@
validation-result (db/validate-pub-key db chat-key)]
(if-not validation-result
(if new-contact?
(fx/merge cofx
(rf/merge cofx
(contact/add-contact chat-key nickname ens-name)
(navigation/navigate-to-cofx :contacts-list {}))
(chat/start-chat cofx chat-key ens-name))
@@ -91,7 +91,7 @@
(i18n/label :t/can-not-add-yourself))
:on-dismiss #(re-frame/dispatch [:pop-to-root-tab :chat-stack])}})))
(fx/defn qr-code-scanned
(rf/defn qr-code-scanned
{:events [:contact/qr-code-scanned]}
[{:keys [db]} data opts]
{::router/handle-uri {:chain (ethereum/chain-keyword db)
+2 -2
View File
@@ -1,6 +1,6 @@
(ns status-im.async-storage.transit
(:require [cognitect.transit :as transit]
[taoensso.timbre :as timbre]))
[taoensso.timbre :as log]))
(def reader (transit/reader :json))
(def writer (transit/writer :json))
@@ -8,4 +8,4 @@
(defn clj->transit [o] (transit/write writer o))
(defn transit->clj [o] (try (transit/read reader o)
(catch :default e
(timbre/error e))))
(log/error e))))
+4 -4
View File
@@ -2,19 +2,19 @@
(:require [re-frame.core :as re-frame]
[taoensso.timbre :as log]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.fx :as fx]))
[utils.re-frame :as rf]))
(fx/defn handle-backup-failed
(rf/defn handle-backup-failed
{:events [::backup-failed]}
[{:keys [db]}]
{:db (dissoc db :backup/performing-backup)})
(fx/defn handle-backup-perfomed
(rf/defn handle-backup-perfomed
{:events [::backup-performed]}
[{:keys [db]}]
{:db (dissoc db :backup/performing-backup)})
(fx/defn handle-perform-backup-pressed
(rf/defn handle-perform-backup-pressed
{:events [:multiaccounts.ui/perform-backup-pressed]}
[{:keys [db]}]
{:db (assoc db :backup/performing-backup true)

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