Compare commits

...
Author SHA1 Message Date
Siddarth Kumar f93a27f18b stage: maestro basic experiment 2024-01-10 13:55:07 +05:30
Parvesh Monu 425ef64901 Refactor compose (#18339)
- Removed show-floating-scroll-down-button? reagent atom
- Created event for scroll-to-bottom-fn
- Renamed chat.ui/close-chat
2024-01-10 13:53:35 +05:30
Omar Basem 0e4c1b3c17 refactor: kotlinify log (#18337)
refactor: kotlinify log
2024-01-10 09:05:35 +04:00
Ajay Sivan 1eb9e87383 Move selected addresses for permissions to app db (#18430) 2024-01-09 19:45:17 -08:00
yqrashawn 61b0a2ecac fix: profile screen initial avatar alignment (#18426)
https://github.com/status-im/status-go/compare/debfe1ca...debfe1ca

Signed-off-by: yqrashawn <namy.19@gmail.com>
2024-01-10 10:34:54 +08:00
frank 78576f4aeb fix: mark all notifications as read not work (#18107) 2024-01-10 10:04:49 +08:00
Ibrahem Khalil 9ff70ed990 Add internal-link component for #17116 (#18109) 2024-01-09 14:31:00 +02:00
Ibrahem Khalil 6d5ef17a31 Drop clj-kondo dir from linting (#18328) 2024-01-09 14:19:30 +02:00
Omar Basem 504d92fd57 Wallet: suggested routes list (#18388)
* wallet: suggested routes list
2024-01-09 15:12:11 +04:00
Omar Basem 3ff6013127 kotlinify thread pool (#18366)
chore: Kotlinify thread pool
2024-01-09 14:35:39 +04:00
Lungu Cristian fb13c3016d Biometrics in new settings (#18258)
* feat: added biometrics setting to new-settings

* fix: fix renaming issues from status-im2

* ref: addressed @cammellos' review comments

* fix: open password settings in a modal

* ref: addressed review comments

* fix: disabling biometric clears auth-method from keychain

* chore: quo/overlay seqs the childrend so need to add keys

* fix: don't pass the password unmasked between events to avoid leaks
2024-01-09 10:25:40 +02:00
Yevheniia Berdnyk 4c4ba97308 e2e: added check for deleted 1-1 chat history 2024-01-08 21:49:20 +02:00
Siddarth Kumar f992f4addd fix: boost checksum issue due to jfrog (#18434)
I tried doing a clean pod install and was faced with this : 

```
Installing Yoga (1.14.0)
Installing boost (1.76.0)

[!] Error installing boost
Verification checksum was incorrect, expected 
f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41, got 
5e89103d9b70bba5c91a794126b169cb67654be2051f90cf7c22ba6893ede0ff
```

related issue in react-native repo : https://github.com/facebook/react-native/issues/42180 

In this commit we patch the pod specs provided by react native to rely on `sourceforge` instead `jfrog` to get boost.
2024-01-08 23:55:36 +05:30
Mohsen b4d27d287a feat: implement edit profile and change name (#18270) 2024-01-08 18:27:07 +03:00
Parvesh Monu fb58d7205e Add dispatch-and-chill to navigation events (#18372) 2024-01-08 19:59:01 +05:30
Parvesh Monu 0208224741 fix delayed display of top bar in chat after sending messages (#18300) 2024-01-08 18:12:07 +05:30
Shivek Khurana b3a2f397f9 🔗 Enable explorer links in wallet->about (#18406) 2024-01-08 12:00:41 +00:00
jakub 5915dd42bd ci: upgrade CI hosts to Xcode 15.1
https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes
https://developer.apple.com/documentation/xcode-release-notes/xcode-15_0_1-release-notes
https://developer.apple.com/documentation/xcode-release-notes/xcode-15_1-release-notes

Signed-off-by: Jakub Sokołowski <jakub@status.im>
2024-01-07 22:24:35 +01:00
Mohamed Javid a00e44d056 Show aggregated tokens and balance in Wallet home (#18275)
This commit adds the feature to show aggregated tokens and balances in the Wallet home.

---------

Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
2024-01-06 00:36:54 +05:30
Icaro Motta b4640dabb9 Run integration tests separately from unit tests (#18304)
- make test still exists, so if you have been using it, as well as make
  test-watch, they should all work exactly the same.
- [Changed] As part of the check stage, Jenkins will run Lint and Unit Tests
  in parallel. Integration Tests run later because running them at the same
  time as Unit Tests caused errors.
- [Added] "make unit-test" and "make unit-test-watch" run unit tests only.
  Watching all unit tests is faster now because we ignore integration tests and
  we only compile shadow-cljs :mock target once. We are at approximately 10-15s
  to re-run all unit tests after saving a watched file, depending on your
  hardware. If you change mocks.js_dependencies.cljs, you must re-run the make
  target.
- [Added] "make integration-test" and "make integration-test-watch" run
  integration tests only. These are much slower than the unit tests.

Fixes https://github.com/status-im/status-mobile/issues/18112
2024-01-05 15:47:03 -03:00
Jamie Caprani 9c62e63035 chore: organise chats context (#18373) 2024-01-05 09:39:01 -08:00
Brian Sztamfater fc72ba6a1e fix: fix height on account card component (#18329)
Signed-off-by: Brian Sztamfater <brian@status.im>
2024-01-05 14:11:11 -03:00
BalogunofAfrica 1919094b46 fix: community buttons (#18392) 2024-01-05 17:17:51 +01:00
Brian Sztamfater e7b92dcf25 fix: increase numbered keyboard touchable area (#18330)
Signed-off-by: Brian Sztamfater <brian@status.im>
2024-01-05 12:55:23 -03:00
Brian Sztamfater 4b3bc73239 fix: always navigate back to shell stack when closing watch account screen (#18324)
Signed-off-by: Brian Sztamfater <brian@status.im>
2024-01-05 12:38:58 -03:00
Jamie Caprani 947a1ef29c feat(wallet): add ability to send a token (#18242) 2024-01-05 07:04:39 -08:00
Ajay Sivan 4edb14bb5f Communities: Share Selective Account (#18382) 2024-01-05 05:34:43 -08:00
Omar Basem c6a7808049 Wallet: Utility Tests (#18371)
* Wallet: utility tests
2024-01-05 16:29:26 +04:00
yqrashawn be2d0feda3 fix: generate_profile_url crash when login (#18364)
* fix: generate_profile_url.cljs$core$IFn$_invoke$arity$2 crash when login

'status_im.common.universal_links.generate_profile_url.cljs$core$IFn$_invoke$arity$2' is undefined

update the function to a fixed arity function

* fix: schema/=> macro for :function schema

Signed-off-by: yqrashawn <namy.19@gmail.com>

---------

Signed-off-by: yqrashawn <namy.19@gmail.com>
2024-01-05 20:08:13 +08:00
Ajay Sivan c91b4339dc Use text-combinations/page-top component in request to join sheet. (#18384) 2024-01-05 02:09:58 -08:00
Yevheniia Berdnyk 5a832871fe e2e: added checks for non-token gated community channels 2024-01-05 00:34:56 +02:00
Brian Sztamfater 205962f582 fix: error is shown when asset is selected on select assets screen (#18326)
Signed-off-by: Brian Sztamfater <brian@status.im>
2024-01-04 11:58:35 -03:00
Siddarth Kumar d64508a74a chore: convert RNSelectableTextInput to Kotlin (#18320)
### Summary

This commit adds `Kotlin` plugin to native module `react-native-status` and converts these files to `Kotlin` :
- `modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputViewManager.java`
- `modules/react-native-status/android/src/main/java/im/status/ethereum/module/RNSelectableTextInputModule.java`

### Platforms
- Android
2024-01-04 17:09:24 +05:30
Yevheniia Berdnyk 85c928f862 e2e: enabled tests for pinning messages 2024-01-04 11:22:58 +02:00
Ulises Manuel 4ff5e4da8b [#17938] Collectibles per account (#18277)
* Update wallet events and subs to handle collectibles per account

Additionally,
 - Move collectibles related events to a new events namespace (`status-im.contexts.wallet.events.collectibles`).
 - Update tests to consider collectibles per account.

* Update collectibles tab to handle a current viewing account
2024-01-03 18:41:12 -06:00
Parvesh Monu fc591f303c fix unable to open add-new-contact screen if hardware button is pressed while qr scanner is open (#18296) 2024-01-03 18:24:58 +05:30
Omar Basem f2c69ab9a8 Wallet: crypto max decimals (#18267)
* wallet: crypto max decimals
2024-01-03 15:48:04 +04:00
Omar Basem 006b637b1b Quo: quiz item (#18306)
* Quo: quiz item
2024-01-03 15:36:17 +04:00
Jamie Caprani f016a9c33c chore: clean up contexts folder to only include larger feature sets (#18286) 2024-01-03 02:25:49 -08:00
Omar Basem 1ce76a4af6 Wallet: network preferences text description (#18319)
Wallet: network preferences text description
2024-01-02 13:45:21 +04:00
Alexander 4a9ca80bc2 Share screen design review (#18148)
* Share screen design review

* Small update

* Update

* Style fix
2024-01-02 10:23:38 +01:00
Ibrahem Khalil e392608697 Fix no message preview on long press on the reaction (#18292) 2023-12-30 11:25:01 +02:00
Siddarth Kumar d495bb6b57 Kotlinify StatusOkHttpClientFactory.java (#18318)
This commit converts  
`android/app/src/main/java/im/status/ethereum/StatusOkHttpClientFactory.java` to
`android/app/src/main/java/im/status/ethereum/StatusOkHttpClientFactory.kt`
2023-12-29 16:57:27 +05:30
Parvesh Monu 7af62c04eb Keep chat screen loaded (#17839) 2023-12-29 14:55:12 +05:30
466 changed files with 4292 additions and 2311 deletions
-5
View File
@@ -194,8 +194,3 @@ test/appium/tests/users.py
## git hooks
lefthook.yml
## clj-kondo
/.clj-kondo/taoensso/*
/.clj-kondo/babashka/*
/.clj-kondo/rewrite-clj/rewrite-clj/config.edn
+37 -19
View File
@@ -305,7 +305,7 @@ endif
# Get all clojure files, including untracked, excluding removed
define find_all_clojure_files
$$(comm -23 <(sort <(git ls-files --cached --others --exclude-standard)) <(sort <(git ls-files --deleted)) | grep -e \.clj$$ -e \.cljs$$ -e \.cljc$$ -e \.edn)
$$(comm -23 <(sort <(git ls-files --cached --others --exclude-standard)) <(sort <(git ls-files --deleted)) | grep -E '((\.clj-kondo\/status-im)|(src).*\.clj[sc]?$$)|((\.clj-kondo\/(status-im|config))|(src).*\.edn$$)|shadow-cljs\.edn')
endef
lint: export TARGET := clojure
@@ -335,26 +335,44 @@ shadow-server: export TARGET := clojure
shadow-server:##@ Start shadow-cljs in server mode for watching
yarn shadow-cljs server
test-watch: export TARGET := clojure
test-watch: ##@ Watch tests and re-run no changes to cljs files
yarn install
nodemon --exec 'yarn shadow-cljs compile mocks && yarn shadow-cljs compile test && node --require ./test-resources/override.js target/test/test.js' -e cljs
test-watch-for-repl: export TARGET := clojure
test-watch-for-repl: ##@ Watch tests and support REPL connections
yarn install
rm -f target/test/test.js
concurrently --kill-others --prefix-colors 'auto' --names 'build,repl' \
'yarn shadow-cljs compile mocks && yarn shadow-cljs watch test --verbose' \
'until [ -f ./target/test/test.js ] ; do sleep 1 ; done ; node --require ./test-resources/override.js ./target/test/test.js --repl'
test: export TARGET := clojure
test: ##@test Run tests once in NodeJS
# Here we create the gyp bindings for nodejs
yarn install
_test-clojure: export TARGET := clojure
_test-clojure: export WATCH ?= false
_test-clojure:
ifeq ($(WATCH), true)
yarn install && \
yarn shadow-cljs compile mocks && \
nodemon --exec "yarn shadow-cljs compile test && node --require ./test-resources/override.js $$SHADOW_OUTPUT_TO" -e cljs
else
yarn install && \
yarn shadow-cljs compile mocks && \
yarn shadow-cljs compile test && \
node --require ./test-resources/override.js target/test/test.js
node --require ./test-resources/override.js "$$SHADOW_OUTPUT_TO"
endif
test: export SHADOW_OUTPUT_TO := target/test/test.js
test: export SHADOW_NS_REGEXP := .*-test$$
test: ##@test Run all Clojure tests
test: _test-clojure
test-watch-for-repl: export SHADOW_OUTPUT_TO := target/test/test.js
test-watch-for-repl: export SHADOW_NS_REGEXP := .*-test$$
test-watch-for-repl: ##@test Watch all Clojure tests and support REPL connections
yarn install
rm -f target/test/test.js
yarn shadow-cljs compile mocks && \
concurrently --kill-others --prefix-colors 'auto' --names 'build,repl' \
'yarn shadow-cljs watch test --verbose' \
"until [ -f $$SHADOW_OUTPUT_TO ] ; do sleep 1 ; done ; node --require ./test-resources/override.js $$SHADOW_OUTPUT_TO --repl"
test-unit: export SHADOW_OUTPUT_TO := target/unit_test/test.js
test-unit: export SHADOW_NS_REGEXP := ^(?!status-im\.integration-test).*-test$$
test-unit: ##@test Run unit tests
test-unit: _test-clojure
test-integration: export SHADOW_OUTPUT_TO := target/integration_test/test.js
test-integration: export SHADOW_NS_REGEXP := ^status-im\.integration-test.*$$
test-integration: ##@test Run integration tests
test-integration: _test-clojure
android-test: jsbundle
android-test: export TARGET := android
@@ -0,0 +1,88 @@
package im.status.ethereum
import android.util.Log
import com.facebook.react.modules.network.OkHttpClientFactory
import com.facebook.react.modules.network.OkHttpClientProvider
import okhttp3.OkHttpClient
import okhttp3.tls.HandshakeCertificates
import java.io.ByteArrayInputStream
import java.lang.RuntimeException
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import im.status.ethereum.module.StatusPackage
class StatusOkHttpClientFactory : OkHttpClientFactory {
companion object {
private const val TAG = "StatusOkHttpClientFactory"
private const val SLEEP_DURATION = 500L // milliseconds
}
override fun createNewNetworkModuleClient(): OkHttpClient? {
val certPem = getCertificatePem().takeIf { it.isNotEmpty() }
?: return logAndReturnNull("Certificate is empty, cannot create OkHttpClient without a valid certificate")
val cert = try {
induceSleep()
// Convert PEM certificate string to X509Certificate object
CertificateFactory.getInstance("X.509")
.generateCertificate(ByteArrayInputStream(certPem.toByteArray())) as? X509Certificate
?: return logAndReturnNull("Certificate could not be parsed as non-null")
} catch (e: Exception) {
return logAndReturnNull("Could not parse certificate", e)
}
val clientCertificates = try {
induceSleep()
// Create HandshakeCertificates object with our certificate
HandshakeCertificates.Builder()
.addPlatformTrustedCertificates()
.addTrustedCertificate(cert)
.build()
} catch (e: Exception) {
return logAndReturnNull("Could not build HandshakeCertificates", e)
}
return try {
OkHttpClientProvider.createClientBuilder()
.sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager)
.build()
} catch (e: Exception) {
logAndReturnNull("Could not create OkHttpClient", e)
}
}
private fun getCertificatePem(): String {
return try {
// Create OkHttpClient with custom SSL socket factory and trust manager
StatusPackage.getImageTLSCert().takeIf { !it.isNullOrBlank() }
?: logAndReturnEmpty("Certificate PEM string is null or empty")
} catch (e: Exception) {
logAndReturnEmpty("Could not getImageTLSCert", e)
}
}
private fun induceSleep() {
try {
// induce half second sleep because sometimes a cert is not immediately available
// TODO : remove sleep if App no longer crashes on Android 10 devices with
// java.lang.RuntimeException: Could not invoke WebSocketModule.connect
Thread.sleep(SLEEP_DURATION)
} catch (e: InterruptedException) {
Log.e(TAG, "Sleep interrupted", e)
}
}
private fun logAndReturnNull(message: String, e: Exception? = null): Nothing? {
Log.e(TAG, message, e)
return null
}
private fun logAndReturnEmpty(message: String, e: Exception? = null): String {
Log.e(TAG, message, e)
return ""
}
}
@@ -1,95 +0,0 @@
package im.status.ethereum;
import android.util.Log;
import com.facebook.react.modules.network.OkHttpClientFactory;
import com.facebook.react.modules.network.OkHttpClientProvider;
import okhttp3.OkHttpClient;
import okhttp3.Interceptor;
import okhttp3.tls.HandshakeCertificates;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.RuntimeException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import im.status.ethereum.module.StatusPackage;
class StatusOkHttpClientFactory implements OkHttpClientFactory {
private static final String TAG = "StatusOkHttpClientFactory";
public OkHttpClient createNewNetworkModuleClient() {
X509Certificate cert = null;
HandshakeCertificates clientCertificates;
String certPem = "";
// Get TLS PEM certificate from status-go
try {
// induce half second sleep because sometimes a cert is not immediately available
// TODO : remove sleep if App no longer crashes on Android 10 devices with
// java.lang.RuntimeException: Could not invoke WebSocketModule.connect
Thread.sleep(500);
certPem = getCertificatePem();
} catch(Exception e) {
Log.e(TAG, "Could not getImageTLSCert",e);
}
if (certPem.isEmpty()) {
Log.e(TAG, "Certificate is empty, cannot create OkHttpClient without a valid certificate");
return null;
}
// Convert PEM certificate string to X509Certificate object
try {
// induce half second sleep because sometimes a cert is not immediately available
// TODO : remove sleep if App no longer crashes on Android 10 devices
// java.lang.RuntimeException: Could not invoke WebSocketModule.connect
Thread.sleep(500);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certPem.getBytes()));
} catch(Exception e) {
Log.e(TAG, "Could not parse certificate",e);
}
// Create HandshakeCertificates object with our certificate
try {
// induce half second sleep because sometimes a cert is not immediately available
// TODO : remove sleep if App no longer crashes on Android 10 devices
// java.lang.RuntimeException: Could not invoke WebSocketModule.connect
Thread.sleep(500);
clientCertificates = new HandshakeCertificates.Builder()
.addPlatformTrustedCertificates()
.addTrustedCertificate(cert)
.build();
} catch(Exception e) {
Log.e(TAG, "Could not build HandshakeCertificates", e);
return null;
}
// Create OkHttpClient with custom SSL socket factory and trust manager
try {
return OkHttpClientProvider.createClientBuilder()
.sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager())
.build();
} catch(Exception e) {
Log.e(TAG, "Could not create OkHttpClient", e);
return null;
}
}
private String getCertificatePem() {
try {
String certPem = StatusPackage.getImageTLSCert();
if (certPem == null || certPem.trim().isEmpty()) {
Log.e(TAG, "Certificate PEM string is null or empty");
return "";
}
return certPem;
} catch (Exception e) {
Log.e(TAG, "Could not getImageTLSCert", e);
return "";
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ library 'status-jenkins-lib@v1.8.4'
def isPRBuild = utils.isPRBuild()
pipeline {
agent { label 'macos && arm64 && nix-2.14 && xcode-14.3' }
agent { label 'macos && arm64 && nix-2.14 && xcode-15.1' }
parameters {
string(
+10 -2
View File
@@ -52,16 +52,24 @@ pipeline {
"""
}
}
stage('Tests') {
stage('Unit Tests') {
steps {
sh """#!/bin/bash
set -eo pipefail
make test 2>&1 | tee -a ${LOG_FILE}
make test-unit 2>&1 | tee -a ${LOG_FILE}
"""
}
}
}
}
stage('Integration Tests') {
steps {
sh """#!/bin/bash
set -eo pipefail
make test-integration 2>&1 | tee -a ${LOG_FILE}
"""
}
}
stage('Component Tests') {
steps {
sh """#!/bin/bash
+3 -3
View File
@@ -5,7 +5,7 @@
To run tests:
```
make test
make test
```
@@ -13,10 +13,10 @@ To run tests:
Also test watcher can be launched. It will re-run the entire test suite when any file is modified
```
make test-watch
make test WATCH=true
```
Developers can also manually change the shadow-cljs option `:ns-regex` to control which namespaces the test runner should pick.
Developers can also manually change the shadow-cljs option `:ns-regex` to control which namespaces the test runner should pick.
## Testing with REPL
+1 -1
View File
@@ -793,7 +793,7 @@ CHECKOUT OPTIONS:
:submodules: true
SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
boost: 64032b9e9b938fda23325e68a3771f0fabf414dc
BVLinearGradient: 612a04ff38e8480291f3379ee5b5a2c571f03fe0
CryptoSwift: c4f2debceb38bf44c80659afe009f71e23e4a082
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
@@ -1,4 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: "org.jetbrains.kotlin.android"
def getStatusGoSHA1 = { ->
def statusgoOverridePath = System.getenv("STATUS_GO_SRC_OVERRIDE")
@@ -1,78 +0,0 @@
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,72 @@
package im.status.ethereum.module
import android.view.ActionMode
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.uimanager.NativeViewHierarchyManager
import com.facebook.react.uimanager.UIBlock
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.views.textinput.ReactEditText
class RNSelectableTextInputModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private var lastActionMode: ActionMode? = null
override fun getName(): String {
return "RNSelectableTextInputManager"
}
@ReactMethod
fun setupMenuItems(selectableTextViewReactTag: Int?, textInputReactTag: Int?) {
val reactContext = reactApplicationContext
val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
selectableTextViewReactTag?.let { selectableTag ->
textInputReactTag?.let { inputTag ->
uiManager?.addUIBlock(UIBlock { nvhm: NativeViewHierarchyManager ->
val rnSelectableTextManager = nvhm.resolveViewManager(selectableTag) as RNSelectableTextInputViewManager
val reactTextView = nvhm.resolveView(inputTag) as ReactEditText
rnSelectableTextManager.registerSelectionListener(reactTextView)
})
}
}
}
@ReactMethod
fun startActionMode(textInputReactTag: Int?) {
val reactContext = reactApplicationContext
val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
textInputReactTag?.let { inputTag ->
uiManager?.addUIBlock(UIBlock { nvhm: NativeViewHierarchyManager ->
val reactTextView = nvhm.resolveView(inputTag) as ReactEditText
lastActionMode = reactTextView.startActionMode(reactTextView.customSelectionActionModeCallback, ActionMode.TYPE_FLOATING)
})
}
}
@ReactMethod
fun hideLastActionMode() {
val reactContext = reactApplicationContext
val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
uiManager?.addUIBlock(UIBlock { _ ->
lastActionMode?.finish()
lastActionMode = null
})
}
@ReactMethod
fun setSelection(textInputReactTag: Int?, start: Int?, end: Int?) {
val reactContext = reactApplicationContext
val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
textInputReactTag?.let { inputTag ->
start?.let { s ->
end?.let { e ->
uiManager?.addUIBlock(UIBlock { nvhm: NativeViewHierarchyManager ->
val reactTextView = nvhm.resolveView(inputTag) as ReactEditText
reactTextView.setSelection(s, e)
})
}
}
}
}
}
@@ -1,102 +0,0 @@
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();
}
}
@@ -0,0 +1,85 @@
package im.status.ethereum.module
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTEventEmitter
import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
class RNSelectableTextInputViewManager : ReactViewManager() {
companion object {
const val REACT_CLASS = "RNSelectableTextInput"
}
private var _menuItems = arrayOf<String>()
override fun getName(): String {
return REACT_CLASS
}
override fun createViewInstance(context: ThemedReactContext): ReactViewGroup {
return ReactViewGroup(context)
}
@ReactProp(name = "menuItems")
fun setMenuItems(reactViewGroup: ReactViewGroup, items: ReadableArray?) {
_menuItems = items?.let {
Array(items.size()) { i -> items.getString(i) }
} ?: arrayOf()
}
fun registerSelectionListener(view: ReactEditText) {
view.customSelectionActionModeCallback = object : ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
menu.clear()
_menuItems.forEachIndexed { i, item ->
menu.add(0, i, 0, item)
}
return true
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return true
}
override fun onDestroyActionMode(mode: ActionMode) {}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val selectionStart = view.selectionStart
val selectionEnd = view.selectionEnd
val selectedText = view.text.toString().substring(selectionStart, selectionEnd)
onSelectNativeEvent(view, item.itemId, selectedText, selectionStart, selectionEnd)
mode.finish()
return true
}
}
}
private fun onSelectNativeEvent(view: ReactEditText, eventType: Int, content: String, selectionStart: Int, selectionEnd: Int) {
val event: WritableMap = Arguments.createMap().apply {
putInt("eventType", eventType)
putString("content", content)
putInt("selectionStart", selectionStart)
putInt("selectionEnd", selectionEnd)
}
val reactContext = view.context as ReactContext
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(view.id, "topSelection", event)
}
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
return MapBuilder.builder<String, Any>()
.put("topSelection", MapBuilder.of("registrationName", "onSelection"))
.build()
}
}
@@ -1,44 +0,0 @@
package im.status.ethereum.module;
import java.util.concurrent.*;
/** Uses an unbounded queue, but allows timeout of core threads
* (modified case 2 in
* https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html ) */
public class StatusThreadPoolExecutor {
private static final int NUMBER_OF_CORES =
Runtime.getRuntime().availableProcessors();
private static final int THREADS_TO_CORES_RATIO = 100;
private static final int KEEP_ALIVE_TIME = 1;
private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
private final BlockingQueue<Runnable> mQueue;
private final ThreadPoolExecutor mThreadPool;
private StatusThreadPoolExecutor() {
mQueue = new LinkedBlockingQueue<>();
mThreadPool = new ThreadPoolExecutor(
THREADS_TO_CORES_RATIO * NUMBER_OF_CORES,
THREADS_TO_CORES_RATIO * NUMBER_OF_CORES,
KEEP_ALIVE_TIME,
KEEP_ALIVE_TIME_UNIT,
mQueue);
// Allow pool to drain
mThreadPool.allowCoreThreadTimeOut(true);
}
/** Pugh singleton */
private static class Holder {
private static StatusThreadPoolExecutor instance = new StatusThreadPoolExecutor();
}
public static StatusThreadPoolExecutor getInstance() {
return Holder.instance;
}
public void execute(final Runnable r) {
mThreadPool.execute(r);
}
}
@@ -0,0 +1,47 @@
package im.status.ethereum.module
import java.util.concurrent.*
/**
* Uses an unbounded queue but allows timeout of core threads
* (modified case 2 in
* https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html )
*/
class StatusThreadPoolExecutor private constructor() {
private val NUMBER_OF_CORES: Int = Runtime.getRuntime().availableProcessors()
private val THREADS_TO_CORES_RATIO: Int = 100
private val KEEP_ALIVE_TIME: Int = 1
private val KEEP_ALIVE_TIME_UNIT: TimeUnit = TimeUnit.SECONDS
private val mQueue: BlockingQueue<Runnable> = LinkedBlockingQueue()
private val mThreadPool: ThreadPoolExecutor
init {
mThreadPool = ThreadPoolExecutor(
THREADS_TO_CORES_RATIO * NUMBER_OF_CORES,
THREADS_TO_CORES_RATIO * NUMBER_OF_CORES,
KEEP_ALIVE_TIME.toLong(),
KEEP_ALIVE_TIME_UNIT,
mQueue
)
// Allow pool to drain
mThreadPool.allowCoreThreadTimeOut(true)
}
/** Singleton holder */
private object Holder {
val instance = StatusThreadPoolExecutor()
}
companion object {
@JvmStatic
fun getInstance(): StatusThreadPoolExecutor {
return Holder.instance
}
}
fun execute(r: Runnable) {
mThreadPool.execute(r)
}
}
@@ -1,23 +0,0 @@
package android.util;
public class Log {
public static int d(String tag, String msg) {
System.out.println("DEBUG: " + tag + ": " + msg);
return 0;
}
public static int i(String tag, String msg) {
System.out.println("INFO: " + tag + ": " + msg);
return 0;
}
public static int w(String tag, String msg) {
System.out.println("WARN: " + tag + ": " + msg);
return 0;
}
public static int e(String tag, String msg) {
System.out.println("ERROR: " + tag + ": " + msg);
return 0;
}
}
@@ -0,0 +1,27 @@
package android.util
object Log {
@JvmStatic
fun d(tag: String, msg: String): Int {
println("DEBUG: $tag: $msg")
return 0
}
@JvmStatic
fun i(tag: String, msg: String): Int {
println("INFO: $tag: $msg")
return 0
}
@JvmStatic
fun w(tag: String, msg: String): Int {
println("WARN: $tag: $msg")
return 0
}
@JvmStatic
fun e(tag: String, msg: String): Int {
println("ERROR: $tag: $msg")
return 0
}
}
+9
View File
@@ -12,6 +12,7 @@ stdenv.mkDerivation {
"patchBuildIdPhase"
"patchKeyChainPhase"
"patchGlogPhase"
"patchBoostPodSpec"
"installPhase"
];
@@ -77,6 +78,14 @@ stdenv.mkDerivation {
--replace 'export CXX="' '#export CXX="'
'';
# to fix pod checksum issue : https://github.com/facebook/react-native/issues/42180
# TODO remove this patch after upgrading to react-native 0.73.2
patchBoostPodSpec = ''
substituteInPlace ./node_modules/react-native/third-party-podspecs/boost.podspec \
--replace 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2' \
'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2' \
'';
# The ELF types are incompatible with the host platform, so let's not even try
# TODO: Use Android NDK to strip binaries manually
dontPatchELF = true;
+1 -1
View File
@@ -69,7 +69,7 @@ in {
yarn = super.yarn.override { nodejs = super.nodejs-18_x; };
openjdk = super.openjdk11_headless;
xcodeWrapper = super.xcodeenv.composeXcodeWrapper {
version = "14.0";
version = "15.0";
allowHigher = true;
};
go = super.go_1_19;
+1 -1
View File
@@ -19,7 +19,7 @@
ios = {targets ? [ "ios/arm64" "iossimulator/amd64"]}: callPackage ./build.nix {
platform = "ios";
platformVersion = "8.0";
platformVersion = "11.0";
outputFileName = "Statusgo.xcframework";
inherit meta source goBuildLdFlags targets;
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+2 -2
View File
@@ -108,13 +108,13 @@
;; produced by the target :mocks below and redefines node require
;; function to use the mocks instead of the rn libraries
:test
{:output-to "target/test/test.js"
{:output-to #shadow/env "SHADOW_OUTPUT_TO"
:output-dir "target/test"
:optimizations :simple
:target :node-test
:dev {:devtools {:preloads [status-im.setup.schema-preload]}}
;; Uncomment line below to `make test-watch` a specific file
;; :ns-regexp "status-im.subs.messages-test$"
:ns-regexp #shadow/env "SHADOW_NS_REGEXP"
:main legacy.status-im.test-runner/main
;; set :ui-driven to true to let shadow-cljs inject node-repl
:ui-driven true
+30 -3
View File
@@ -39,14 +39,16 @@ export function interpolateNavigationViewOpacity(props) {
});
}
export function messagesListOnScroll(distanceFromListTop, callback) {
export function messagesListOnScroll(distanceFromListTop, chatListScrollY, callback) {
return function (event) {
'worklet';
const currentY = event.contentOffset.y;
const layoutHeight = event.layoutMeasurement.height;
const contentSizeY = event.contentSize.height - layoutHeight;
distanceFromListTop.value = contentSizeY - currentY;
runOnJS(callback)(currentY, layoutHeight);
const newDistance = contentSizeY - currentY;
distanceFromListTop.value = newDistance;
chatListScrollY.value = currentY;
runOnJS(callback)(layoutHeight, newDistance);
};
}
@@ -63,3 +65,28 @@ export function placeholderZIndex(isCalculationsComplete) {
return isCalculationsComplete.value ? 0 : 2;
});
}
export function scrollDownButtonOpacity(chatListScrollY, isComposerFocused, windowHeight) {
return useDerivedValue(function () {
'worklet';
if (isComposerFocused.value) {
return 0;
} else {
return chatListScrollY.value > windowHeight * 0.75 ? 1 : 0;
}
});
}
export function jumpToButtonOpacity(scrollDownButtonOpacity, isComposerFocused) {
return useDerivedValue(function () {
'worklet';
return withTiming(scrollDownButtonOpacity.value == 1 || isComposerFocused.value ? 0 : 1);
});
}
export function jumpToButtonPosition(scrollDownButtonOpacity, isComposerFocused) {
return useDerivedValue(function () {
'worklet';
return withTiming(scrollDownButtonOpacity.value == 1 || isComposerFocused.value ? 35 : 0);
});
}
@@ -4,7 +4,7 @@
[legacy.status-im.data-store.messages :as data-store.messages]
[re-frame.core :as re-frame]
[status-im.constants :as constants]
[status-im.contexts.chat.messages.list.events :as message-list]
[status-im.contexts.chat.messenger.messages.list.events :as message-list]
[taoensso.timbre :as log]
[utils.re-frame :as rf]))
@@ -6,9 +6,9 @@
[legacy.status-im.utils.deprecated-types :as types]
[re-frame.core :as re-frame]
[react-native.platform :as platform]
[status-im.contexts.chat.messages.delete-message.events :as delete-message]
[status-im.contexts.chat.messages.list.events :as message-list]
[status-im.contexts.chat.messages.list.state :as view.state]
[status-im.contexts.chat.messenger.messages.delete-message.events :as delete-message]
[status-im.contexts.chat.messenger.messages.list.events :as message-list]
[status-im.contexts.chat.messenger.messages.list.state :as view.state]
[utils.re-frame :as rf]))
(defn- message-loaded?
@@ -3,7 +3,7 @@
[cljs.test :refer-macros [deftest is testing]]
[legacy.status-im.chat.models.loading :as loading]
[legacy.status-im.chat.models.message :as message]
[status-im.contexts.chat.messages.list.state :as list.state]))
[status-im.contexts.chat.messenger.messages.list.state :as list.state]))
(deftest add-received-message-test
(with-redefs [message/add-message #(identity %1)]
+2 -2
View File
@@ -4,8 +4,8 @@
[legacy.status-im.data-store.chats :as chats-store]
[legacy.status-im.utils.deprecated-types :as types]
[re-frame.core :as re-frame]
[status-im.contexts.chat.messages.list.events :as message-list]
[status-im.contexts.contacts.events :as contacts-store]
[status-im.contexts.chat.contacts.events :as contacts-store]
[status-im.contexts.chat.messenger.messages.list.events :as message-list]
[status-im.contexts.shell.activity-center.events :as activity-center]
[status-im.navigation.events :as navigation]
[utils.re-frame :as rf]))
@@ -6,7 +6,7 @@
[legacy.status-im.multiaccounts.update.core :as multiaccounts.update]
[legacy.status-im.utils.mobile-sync :as utils]
[legacy.status-im.wallet.core :as wallet]
[status-im.contexts.add-new-contact.events :as add-new-contact]
[status-im.contexts.chat.home.add-new-contact.events :as add-new-contact]
[status-im.navigation.events :as navigation]
[taoensso.timbre :as log]
[utils.re-frame :as rf]))
@@ -9,8 +9,8 @@
[quo.foundations.typography :as typography]
[react-native.core :as rn]
[status-im.constants :as constants]
[status-im.contexts.chat.messages.delete-message-for-me.events]
[status-im.contexts.chat.messages.delete-message.events]
[status-im.contexts.chat.messenger.messages.delete-message-for-me.events]
[status-im.contexts.chat.messenger.messages.delete-message.events]
[utils.i18n :as i18n]
[utils.re-frame :as rf])
(:require-macros [legacy.status-im.utils.views :refer [defview letsubs]]))
@@ -8,7 +8,7 @@
[legacy.status-im.ui.components.react :as react]
[legacy.status-im.ui.screens.link-previews-settings.styles :as styles]
[re-frame.core :as re-frame]
[status-im.contexts.chat.messages.link-preview.events]
[status-im.contexts.chat.messenger.messages.link-preview.events]
[utils.i18n :as i18n]))
(defn prepare-urls-items-data
@@ -8,7 +8,7 @@
utils.money))
(defn view-internal
[{:keys [value icon theme style accessibility-label]}]
[{:keys [value icon theme style accessibility-label text-size]}]
[rn/view
{:style (merge style/container style)
:accessibility-label accessibility-label}
@@ -19,7 +19,7 @@
:resize-mode :center
:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}]
[quo.text/text
{:size :paragraph-1
{:size (or text-size :paragraph-1)
:weight :regular
:style (style/text theme)} (utils.money/format-amount value)]])
+2 -2
View File
@@ -61,7 +61,7 @@
:container-style])
(defn- base-input
[{:keys [on-change-text on-char-limit-reach weight]}]
[{:keys [on-change-text on-char-limit-reach weight default-value]}]
(let [status (reagent/atom :default)
internal-on-focus #(reset! status :focus)
internal-on-blur #(reset! status :default)
@@ -72,7 +72,7 @@
(if (> height min-height)
(reset! multiple-lines? true)
(reset! multiple-lines? false)))
char-count (reagent/atom 0)
char-count (reagent/atom (count default-value))
update-char-limit! (fn [new-text char-limit]
(when on-change-text (on-change-text new-text))
(let [amount-chars (count new-text)]
@@ -0,0 +1,72 @@
(ns quo.components.links.internal-link-card.channel.style
(:require [quo.foundations.colors :as colors]))
(defn loading-circle
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:margin-right 4
:width 16
:height 16
:border-radius 16})
(defn loading-first-line-bar
[theme margin-right?]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:width 72
:margin-right (when margin-right? 4)
:height 16
:border-radius 6})
(defn loading-second-line-bar
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:width 112
:height 8
:border-radius 6
:margin-bottom 17})
(defn loading-thumbnail-box
[theme size]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:height (if (= :message size) 139 160)
:border-radius 12})
(defn thumbnail
[size]
{:width "100%"
:height (if (= :message size) 139 160)
:margin-top 8
:border-radius 12})
(defn container
[size theme]
{:border-width 1
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)
:background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme)
:border-radius 16
:padding-horizontal 12
:padding-top 10
:padding-bottom 12
:height (if (= :message size) 215 236)
:width (if (= :message size) 295 335)})
(def header-container
{:flex-direction :row
:align-items :center})
(def title
{:margin-bottom 2})
(def logo
{:margin-right 6
:width 16
:height 16
:border-radius 8})
(defn channel-chevron-props
[theme]
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)})
(def row-spacing
{:flex-direction :row
:margin-bottom 13})
@@ -0,0 +1,92 @@
(ns quo.components.links.internal-link-card.channel.view
(:require
[quo.components.icon :as icon]
[quo.components.links.internal-link-card.channel.style :as style]
[quo.components.links.internal-link-card.schema :as component-schema]
[quo.components.markdown.text :as text]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[schema.core :as schema]))
(defn- description-comp
[description]
[rn/view {:style {:margin-bottom 4}}
[text/text
{:size :paragraph-2
:number-of-lines 3
:accessibility-label :description}
description]])
(defn- title-comp
[title channel-name theme]
[rn/view
{:style {:flex-direction :row
:align-items :center}}
[text/text
{:size :paragraph-1
:number-of-lines 1
:weight :semi-bold
:style style/title
:accessibility-label :title}
title]
[icon/icon :i/chevron-right (style/channel-chevron-props theme)]
[text/text
{:size :paragraph-1
:number-of-lines 1
:weight :semi-bold
:style style/title
:accessibility-label :title}
channel-name]])
(defn- banner-comp
[thumbnail size]
[rn/image
{:style (style/thumbnail size)
:source thumbnail
:accessibility-label :banner}])
(defn- logo-comp
[logo]
[rn/image
{:accessibility-label :logo
:source logo
:style (assoc style/logo :margin-bottom 2)}])
(defn- loading-view
[theme size]
[rn/view
{:accessibility-label :loading-channel-link-view
:style {:height 215}}
[rn/view {:style {:flex-direction :row}}
[rn/view {:style style/row-spacing}
[rn/view {:style (style/loading-circle theme)}]
[rn/view {:style (style/loading-first-line-bar theme true)}]]
[rn/view {:style style/row-spacing}
[rn/view {:style (style/loading-circle theme)}]
[rn/view {:style (style/loading-first-line-bar theme false)}]]]
[rn/view {:style (style/loading-second-line-bar theme)}]
[rn/view {:style (style/loading-thumbnail-box theme size)}]])
(defn view-internal
[{:keys [title description loading? icon banner
theme on-press channel-name size]
:or {channel-name "empty name"}}]
[rn/pressable
{:style (style/container size theme)
:accessibility-label :internal-link-card
:on-press on-press}
(if loading?
[loading-view theme size]
[:<>
[rn/view {:style style/header-container}
(when icon
[logo-comp icon])
[title-comp title channel-name theme]]
(when description
[description-comp description])
(when banner
[banner-comp banner size])])])
(def view
(quo.theme/with-theme
(schema/instrument #'view-internal component-schema/?schema)))
@@ -0,0 +1,87 @@
(ns quo.components.links.internal-link-card.community.style
(:require [quo.foundations.colors :as colors]))
(defn loading-circle
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:margin-right 4
:width 16
:height 16
:border-radius 16})
(defn loading-stat
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:margin-right 4
:width 32
:height 8
:border-radius 16})
(def loading-stat-container
{:flex-direction :row
:align-items :center
:margin-right 12
:margin-bottom -6})
(defn loading-first-line-bar
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:width 145
:height 16
:border-radius 6})
(defn loading-second-line-bar
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:width 112
:height 8
:border-radius 6
:margin-bottom 17})
(defn loading-thumbnail-box
[theme size]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:height (if (= :message size) 139 160)
:border-radius 12})
(defn thumbnail
[size]
{:width "100%"
:height (if (= :message size) 139 160)
:margin-top 6
:border-radius 12})
(defn container
[size theme]
{:border-width 1
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)
:background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme)
:border-radius 16
:padding-horizontal 12
:padding-top 10
:padding-bottom 12
:height (if (= :message size) 245 266)
:width (if (= :message size) 295 335)})
(def header-container
{:flex-direction :row
:align-items :center})
(def title
{:margin-bottom 2})
(def logo
{:width 16
:height 16
:border-radius 8
:margin-right 4
:margin-bottom 2})
(def row-spacing
{:flex-direction :row
:margin-bottom 12
:margin-top 4})
(def stat-container
{:flex-direction :row
:margin-top 12})
@@ -0,0 +1,96 @@
(ns quo.components.links.internal-link-card.community.view
(:require
[quo.components.community.community-stat.view :as community-stat]
[quo.components.links.internal-link-card.community.style :as style]
[quo.components.links.internal-link-card.schema :as component-schema]
[quo.components.markdown.text :as text]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[schema.core :as schema]))
(defn- description-comp
[description members-count active-members-count]
[rn/view
[text/text
{:size :paragraph-2
:number-of-lines 3
:accessibility-label :description}
description]
[rn/view {:style style/stat-container}
[community-stat/view
{:value members-count
:icon :i/members
:accessibility-label :members-count
:style {:margin-right 12}
:text-size :paragraph-2}]
(when active-members-count
[community-stat/view
{:value active-members-count
:icon :i/active-members
:accessibility-label :active-members-count
:text-size :paragraph-2}])]])
(defn- title-comp
[title]
[text/text
{:size :paragraph-1
:number-of-lines 1
:weight :semi-bold
:style style/title
:accessibility-label :title}
title])
(defn- thumbnail-comp
[thumbnail size]
[rn/image
{:style (style/thumbnail size)
:source thumbnail
:accessibility-label :thumbnail}])
(defn- logo-comp
[logo]
[rn/image
{:accessibility-label :logo
:source logo
:style style/logo}])
(defn- stat-loading
[theme]
[rn/view {:style style/loading-stat-container}
[rn/view {:style (style/loading-circle theme)}]
[rn/view {:style (style/loading-stat theme)}]])
(defn- loading-view
[theme size]
[rn/view {:accessibility-label :loading-community-link-view}
[rn/view {:style style/row-spacing}
[rn/view {:style (style/loading-circle theme)}]
[rn/view {:style (style/loading-first-line-bar theme)}]]
[rn/view {:style (style/loading-second-line-bar theme)}]
[rn/view {:style style/row-spacing}
[stat-loading theme]
[stat-loading theme]]
[rn/view {:style (style/loading-thumbnail-box theme size)}]])
(defn- view-internal
[{:keys [title description loading? icon banner members-count active-members-count
theme on-press size]}]
[rn/pressable
{:style (style/container size theme)
:accessibility-label :internal-link-card
:on-press on-press}
(if loading?
[loading-view theme size]
[:<>
[rn/view {:style style/header-container}
(when icon
[logo-comp icon])
[title-comp title]]
(when description
[description-comp description members-count active-members-count])
(when banner
[thumbnail-comp banner size])])])
(def view
(quo.theme/with-theme
(schema/instrument #'view-internal component-schema/?schema)))
@@ -0,0 +1,81 @@
(ns quo.components.links.internal-link-card.component-spec
(:require
[quo.components.links.internal-link-card.view :as view]
[test-helpers.component :as h]))
(defn- render
[component]
(h/render-with-theme-provider component :light))
(def user-props
{:title "Some title"
:subtitle "Some description"
:loading? false
:icon "data:image/png,logo-x"
:type :user
:customization-color "#ff0000"
:emoji-hash "🌟🚀🐠🌈🏰🔮🦉🐼🍉🎨🚲🌙🍔🌵"})
(h/describe "Internal link card - User"
(h/test "renders with most common props"
(render [view/view user-props])
(h/is-truthy (h/query-by-text (:title user-props)))
(h/is-truthy (h/query-by-text (:subtitle user-props))))
(h/test "does not render logo if prop is not present"
(render [view/view (dissoc user-props :icon)])
(h/is-null (h/query-by-label-text :logo))))
(def community-props
{:title "Some title"
:description "Some description"
:icon "data:image/png,logo-x"
:banner "data:image/png,whatever"
:members-count "20"
:loading? false
:active-members-count "15"
:type :community})
(h/describe "Internal link card - Community"
(h/test "renders with most common props"
(render [view/view community-props])
(h/is-truthy (h/query-by-text (:title community-props)))
(h/is-truthy (h/query-by-text (:description community-props)))
(h/is-truthy (h/query-by-text (:members-count community-props)))
(h/is-truthy (h/query-by-text (:active-members-count community-props)))
(h/is-truthy (h/query-by-label-text :logo))
(h/is-truthy (h/query-by-label-text :thumbnail)))
(h/test "does not render thumbnail if prop is not present"
(render [view/view (dissoc community-props :banner)])
(h/is-null (h/query-by-label-text :thumbnail)))
(h/test "does not render logo if prop is not present"
(render [view/view (dissoc community-props :icon)])
(h/is-null (h/query-by-label-text :logo))))
(def channel-props
{:title "Doodles"
:description "Coloring the world with joy • ᴗ •"
:icon "data:image/png,logo-x"
:banner "data:image/png,whatever"
:loading? false
:channel-name "#general"
:type :channel})
(h/describe "Internal link card - Channel"
(h/test "renders with most common props"
(render [view/view channel-props])
(h/is-truthy (h/query-by-text (:title channel-props)))
(h/is-truthy (h/query-by-text (:description channel-props)))
(h/is-truthy (h/query-by-text (:channel-name channel-props)))
(h/is-truthy (h/query-by-label-text :logo))
(h/is-truthy (h/query-by-label-text :banner)))
(h/test "does not render banner if prop is not present"
(render [view/view (dissoc channel-props :banner)])
(h/is-null (h/query-by-label-text :banner)))
(h/test "does not render logo if prop is not present"
(render [view/view (dissoc channel-props :icon)])
(h/is-null (h/query-by-label-text :logo))))
@@ -0,0 +1,23 @@
(ns quo.components.links.internal-link-card.schema)
(def ?schema
[:=>
[:catn
[:props
[:map {:closed true}
[:title {:optional true} [:maybe :string]]
[:description {:optional true} [:maybe :string]]
[:channel-name {:optional true} [:maybe :string]]
[:loading? {:optional true} [:maybe :boolean]]
[:subtitle {:optional true} [:maybe :string]]
[:icon {:optional true} [:maybe [:or :string :int]]]
[:banner {:optional true} [:maybe [:or :string :int]]]
[:type {:optional true} [:maybe :keyword]]
[:on-press {:optional true} [:maybe fn?]]
[:members-count {:optional true} [:maybe [:or :int :string]]]
[:active-members-count {:optional true} [:maybe [:or :int :string]]]
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
[:emoji-hash {:optional true} [:maybe :string]]
[:size {:optional true} [:maybe :keyword]]
[:theme :schema.common/theme]]]]
:any])
@@ -0,0 +1,66 @@
(ns quo.components.links.internal-link-card.user.style
(:require [quo.foundations.colors :as colors]))
(defn loading-circle
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:margin-right 4
:width 16
:height 16
:border-radius 16})
(defn loading-first-line-bar
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:width 145
:height 16
:border-radius 6})
(defn loading-second-line-bar
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:width 112
:height 8
:border-radius 6
:margin-bottom 17})
(defn last-bar-line-bar
[theme]
{:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)
:width 271
:height 16
:border-radius 6})
(defn gradient-start-color
[customization-color theme]
(colors/theme-colors (colors/resolve-color customization-color theme 10)
(colors/resolve-color customization-color theme 20)
theme))
(defn container
[loading? theme size]
{:border-width 1
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)
:background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme)
:border-radius 16
:padding-horizontal 12
:padding-top 10
:padding-bottom 12
:height (if loading? 92 110)
:width (if (= :message size) 295 335)})
(def header-container
{:flex-direction :row
:align-items :center})
(def title
{:margin-bottom 2})
(def logo
{:margin-right 6
:width 16
:height 16
:border-radius 8
:margin-bottom 2})
(def row-spacing {:flex-direction :row :margin-bottom 12})
@@ -0,0 +1,83 @@
(ns quo.components.links.internal-link-card.user.view
(:require
[quo.components.links.internal-link-card.schema :as component-schema]
[quo.components.links.internal-link-card.user.style :as style]
[quo.components.markdown.text :as text]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[react-native.linear-gradient :as linear-gradient]
[schema.core :as schema]))
(defn- subtitle-comp
[subtitle emoji-hash]
[rn/view
[text/text
{:size :paragraph-2
:number-of-lines 2
:accessibility-label :subtitle
:style {:margin-bottom 12}}
subtitle]
[rn/view {:style {:flex-direction :row}}
[text/text
{:size :paragraph-2
:number-of-lines 1
:weight :regular
:accessibility-label :emoji-hash}
emoji-hash]]])
(defn- title-comp
[title]
[text/text
{:size :paragraph-1
:number-of-lines 1
:weight :semi-bold
:style style/title
:accessibility-label :title}
title])
(defn- logo-comp
[logo]
[rn/image
{:accessibility-label :logo
:source logo
:style style/logo}])
(defn- loading-view
[theme]
[rn/view {:accessibility-label :loading-user-link-view}
[rn/view {:style style/row-spacing}
[rn/view {:style (style/loading-circle theme)}]
[rn/view {:style (style/loading-first-line-bar theme)}]]
[rn/view {:style (style/loading-second-line-bar theme)}]
[rn/view {:style (style/last-bar-line-bar theme)}]])
(defn- linear-gradient-props
[theme customization-color]
[(style/gradient-start-color customization-color theme) :transparent])
(defn view-internal
[{:keys [title loading? icon
theme on-press subtitle emoji-hash customization-color size]}]
(if loading?
[rn/pressable
{:accessibility-label :internal-link-card
:on-press on-press
:style (style/container loading? theme size)}
[loading-view theme]]
[linear-gradient/linear-gradient
(assoc {:style (style/container loading? theme size)}
:colors
(linear-gradient-props theme customization-color))
[rn/pressable
{:accessibility-label :internal-link-card
:on-press on-press}
[rn/view {:style style/header-container}
(when icon
[logo-comp icon])
[title-comp title]]
(when subtitle
[subtitle-comp subtitle emoji-hash])]]))
(def view
(quo.theme/with-theme
(schema/instrument #'view-internal component-schema/?schema)))
@@ -0,0 +1,11 @@
(ns quo.components.links.internal-link-card.view
(:require [quo.components.links.internal-link-card.channel.view :as channel.view]
[quo.components.links.internal-link-card.community.view :as community.view]
[quo.components.links.internal-link-card.user.view :as user.view]))
(defn view
[{card-type :type :as props}]
(case card-type
:community [community.view/view props]
:channel [channel.view/view props]
:user [user.view/view props]))
@@ -0,0 +1,24 @@
(ns quo.components.list-items.quiz-item.component-spec
(:require
[quo.components.list-items.quiz-item.view :as quiz-item]
[test-helpers.component :as h]))
(h/describe "List Items: Token Value"
(h/test "Number label renders"
(h/render [quiz-item/view
{:state :empty
:word "collapse"
:number 2}])
(h/is-truthy (h/get-by-label-text :number-container)))
(h/test "Success icon renders"
(h/render [quiz-item/view
{:state :success
:word "collapse"
:number 2}])
(h/is-truthy (h/get-by-label-text :success-icon)))
(h/test "Error icon renders"
(h/render [quiz-item/view
{:state :error
:word "collapse"
:number 2}])
(h/is-truthy (h/get-by-label-text :error-icon))))
@@ -0,0 +1,45 @@
(ns quo.components.list-items.quiz-item.style
(:require [quo.foundations.colors :as colors]))
(defn container
[{:keys [blur? theme state]}]
{:flex 1
:flex-direction :row
:justify-content :space-between
:align-items :center
:max-height 56
:padding 12
:border-radius 12
:opacity (if (= state :disabled) 0.3 1)
:border-width (if (and blur? (or (= state :empty) (= state :disabled))) 0 1)
:border-color (case state
:success colors/success-50-opa-20
:error colors/danger-50-opa-20
(colors/theme-colors colors/neutral-20 colors/neutral-80 theme))
:background-color (case state
:empty (if blur?
colors/white-opa-5
(colors/theme-colors colors/white colors/neutral-80-opa-40 theme))
:disabled (if blur?
colors/white-opa-5
(colors/theme-colors colors/neutral-5 colors/neutral-80-opa-40 theme))
:success (colors/resolve-color :success theme 10)
:error (colors/resolve-color :danger theme 10))})
(defn num-container
[{:keys [blur? theme]}]
{:width 32
:height 32
:justify-content :center
:align-items :center
:border-radius 10
:border-width 1
:border-color (if blur?
colors/white-opa-10
(colors/theme-colors colors/neutral-20 colors/neutral-70 theme))})
(defn text
[{:keys [theme state]}]
{:color (case state
:success (colors/theme-colors colors/success-50 colors/success-60 theme)
:error (colors/theme-colors colors/danger-50 colors/danger-60 theme))})
@@ -0,0 +1,30 @@
(ns quo.components.list-items.quiz-item.view
(:require
[quo.components.icon :as icon]
[quo.components.list-items.quiz-item.style :as style]
[quo.components.markdown.text :as text]
[quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[utils.i18n :as i18n]))
(defn- view-internal
[{:keys [state theme number word] :as props}]
[rn/view {:style (style/container props)}
(if (or (= state :empty) (= state :disabled))
[rn/view
{:style (style/num-container props)
:accessibility-label :number-container}
[text/text {:weight :semi-bold} number]]
[text/text {:style (style/text props)}
(if (= state :success) word (i18n/label :t/oops-wrong-word))])
(when (= state :success)
[icon/icon :i/check
{:color (colors/theme-colors colors/success-50 colors/success-60 theme)
:accessibility-label :success-icon}])
(when (= state :error)
[icon/icon :i/incorrect
{:color (colors/theme-colors colors/danger-50 colors/danger-60 theme)
:accessibility-label :error-icon}])])
(def view (quo.theme/with-theme view-internal))
@@ -26,6 +26,7 @@
(on-press label)))
:on-press-in #(reset! pressed? true)
:on-press-out #(reset! pressed? false)
:hit-slop {:top 8 :bottom 8 :left 25 :right 25}
:style (style/container background-color)}
(case type
:key [icons/icon
+3 -3
View File
@@ -5,7 +5,7 @@
[react-native.core :as rn]))
(defn view
[{:keys [type]} & children]
[{:keys [type container-style]} & children]
[rn/view {:style (style/overlay-background type)}
(if (= type :shell)
[blur/view
@@ -14,7 +14,7 @@
:blur-type :transparent
:overlay-color :transparent
:style style/container}
[rn/view {:style style/blur-container}
[rn/view {:style (merge style/blur-container container-style)}
children]]
[rn/view {:style style/container}
[rn/view {:style (merge style/container container-style)}
children])])
@@ -7,8 +7,8 @@
[react-native.core :as rn]))
(defn- category-internal
[{:keys [label data] :as props}]
[rn/view {:style (style/container label)}
[{:keys [label data container-style] :as props}]
[rn/view {:style (merge (style/container label) container-style)}
(when label
[text/text
{:weight :medium
@@ -19,10 +19,12 @@
(defn sub-container
[align-action]
{:flex-direction :row
:padding-right 0.5
:align-items (or align-action :center)})
(def left-container
{:margin-horizontal 12
(defn left-container
[image?]
{:margin-horizontal (if image? 12 0)
:flex 1
:height "100%"
:justify-content :flex-start})
@@ -57,6 +59,7 @@
{:width 15
:height 15
:border-radius 12
:margin-right 4
:background-color background-color})
(def status-tag-container
@@ -109,7 +109,7 @@
:accessibility-label accessibility-label}
[rn/view {:style (style/left-sub-container props)}
[image-component props]
[rn/view {:style style/left-container}
[rn/view {:style (style/left-container (:image props))}
[text/text
{:weight :medium
:style {:color (when blur? colors/white)}} title]
+8 -7
View File
@@ -37,13 +37,14 @@
(def ^:private b64-png-image-prefix "data:image/png;base64,")
(defn temp-empty-symbol
[token size]
[token size style]
[rn/view
{:style (token-style {:justify-content :center
:align-items :center
:border-radius 20
:border-width 1
:border-color :grey}
{:style (token-style (merge {:justify-content :center
:align-items :center
:border-radius 20
:border-width 1
:border-color :grey}
style)
size)}
[quo/text {:style {:color :grey}}
(some-> token
@@ -73,6 +74,6 @@
[rn/image
{:style (token-style style size)
:source source}]
[temp-empty-symbol token size])))
[temp-empty-symbol token size style])))
(def view (schema/instrument #'view-internal ?schema))
@@ -11,8 +11,9 @@
colors/white))
(defn card
[{:keys [customization-color type theme pressed?]}]
{:width 162
[{:keys [customization-color type theme pressed? metrics?]}]
{:width 161
:height (if metrics? 88 68)
:background-color (when (not= :watch-only type)
(colors/theme-colors
(colors/resolve-color customization-color
@@ -21,7 +21,8 @@
:style (style/card {:customization-color customization-color
:type type
:theme theme
:pressed? false})}
:pressed? false
:metrics? metrics?})}
[rn/view {:style style/loader-container}
[rn/view
{:style (assoc (style/loader-view {:width 16
@@ -113,7 +114,8 @@
:style (style/card {:customization-color customization-color
:type type
:theme theme
:pressed? @pressed?})
:pressed? @pressed?
:metrics? metrics?})
:on-press on-press}
(when (and customization-color (and (not watch-only?) (not missing-keypair?)))
[customization-colors/overlay
@@ -29,6 +29,7 @@
[:checked? {:optional true} [:maybe :boolean]]
[:disabled? {:optional true} [:maybe :boolean]]
[:on-change {:optional true} [:maybe fn?]]
[:container-style {:optional true} [:maybe :map]]
[:theme :schema.common/theme]]]]
:any])
@@ -58,11 +59,11 @@
(defn- view-internal
[{:keys
[checked? disabled? on-change token-details keycard? theme]
[checked? disabled? on-change token-details keycard? theme container-style]
{:keys
[name address emoji customization-color]} :account}]
[rn/view
{:style (style/container theme)
{:style (merge (style/container theme) container-style)
:accessibility-label :wallet-account-permissions}
[rn/view {:style style/row1}
[account-avatar/view
@@ -2,6 +2,7 @@
(:require
[quo.components.avatars.account-avatar.view :as account-avatar]
[quo.components.avatars.user-avatar.view :as user-avatar]
[quo.components.avatars.wallet-user-avatar.view :as wallet-user-avatar]
[quo.components.markdown.text :as text]
[quo.components.wallet.summary-info.style :as style]
[quo.foundations.colors :as colors]
@@ -26,24 +27,29 @@
(defn networks
[values theme]
(let [{:keys [ethereum optimism arbitrum]} values]
(let [{:keys [ethereum optimism arbitrum]} values
show-optimism? (pos? optimism)
show-arbitrum? (pos? arbitrum)]
[rn/view
{:style style/networks-container
:accessibility-label :networks}
[network-amount
{:network :ethereum
:amount (str ethereum " ETH")
:divider? true
:theme theme}]
[network-amount
{:network :optimism
:amount (str optimism " ETH")
:divider? true
:theme theme}]
[network-amount
{:network :arbitrum
:amount (str arbitrum " ETH")
:theme theme}]]))
(when (pos? ethereum)
[network-amount
{:network :ethereum
:amount (str ethereum " ETH")
:divider? (or show-arbitrum? show-optimism?)
:theme theme}])
(when show-optimism?
[network-amount
{:network :optimism
:amount (str optimism " OPT")
:divider? show-arbitrum?
:theme theme}])
(when show-arbitrum?
[network-amount
{:network :arbitrum
:amount (str arbitrum " ARB")
:theme theme}])]))
(defn- view-internal
[{:keys [theme type account-props networks? values]}]
@@ -51,8 +57,13 @@
{:style (style/container networks? theme)}
[rn/view
{:style style/info-container}
(if (= type :status-account)
[account-avatar/view account-props]
(case type
:status-account [account-avatar/view account-props]
:saved-account [wallet-user-avatar/wallet-user-avatar (assoc account-props :size :size-32)]
:account [wallet-user-avatar/wallet-user-avatar
(assoc account-props
:size :size-32
:neutral? true)]
[user-avatar/user-avatar account-props])
[rn/view {:style {:margin-left 8}}
(when (not= type :account) [text/text {:weight :semi-bold} (:name account-props)])
+6 -2
View File
@@ -69,6 +69,7 @@
quo.components.inputs.title-input.view
quo.components.ios.drawer-bar.view
quo.components.keycard.view
quo.components.links.internal-link-card.view
quo.components.links.link-preview.view
quo.components.links.url-preview-list.view
quo.components.links.url-preview.view
@@ -80,6 +81,7 @@
quo.components.list-items.dapp.view
quo.components.list-items.menu-item
quo.components.list-items.preview-list.view
quo.components.list-items.quiz-item.view
quo.components.list-items.saved-address.view
quo.components.list-items.saved-contact-address.view
quo.components.list-items.token-network.view
@@ -287,6 +289,7 @@
(def numbered-keyboard quo.components.numbered-keyboard.numbered-keyboard.view/view)
;;;; Links
(def internal-link-card quo.components.links.internal-link-card.view/view)
(def link-preview quo.components.links.link-preview.view/view)
(def url-preview quo.components.links.url-preview.view/view)
(def url-preview-list quo.components.links.url-preview-list.view/view)
@@ -296,15 +299,16 @@
(def account-list-card quo.components.list-items.account-list-card.view/view)
(def address quo.components.list-items.address.view/view)
(def channel quo.components.list-items.channel.view/view)
(def community-list-item quo.components.list-items.community.view/view)
(def dapp quo.components.list-items.dapp.view/view)
(def menu-item quo.components.list-items.menu-item/menu-item)
(def preview-list quo.components.list-items.preview-list.view/view)
(def user-list quo.components.list-items.user-list/user-list)
(def community-list-item quo.components.list-items.community.view/view)
(def quiz-item quo.components.list-items.quiz-item.view/view)
(def saved-address quo.components.list-items.saved-address.view/view)
(def saved-contact-address quo.components.list-items.saved-contact-address.view/view)
(def token-network quo.components.list-items.token-network.view/view)
(def token-value quo.components.list-items.token-value.view/view)
(def user-list quo.components.list-items.user-list/user-list)
;;;; Loaders
(def skeleton-list quo.components.loaders.skeleton-list.view/view)
+2
View File
@@ -39,6 +39,7 @@
quo.components.inputs.recovery-phrase.component-spec
quo.components.inputs.title-input.component-spec
quo.components.keycard.component-spec
quo.components.links.internal-link-card.component-spec
quo.components.links.link-preview.component-spec
quo.components.links.url-preview-list.component-spec
quo.components.links.url-preview.component-spec
@@ -47,6 +48,7 @@
quo.components.list-items.channel.component-spec
quo.components.list-items.community.component-spec
quo.components.list-items.dapp.component-spec
quo.components.list-items.quiz-item.component-spec
quo.components.list-items.saved-address.component-spec
quo.components.list-items.saved-contact-address.component-spec
quo.components.list-items.token-network.component-spec
+3
View File
@@ -203,6 +203,9 @@
(def danger-50-opa-30 (alpha danger-50 0.3))
(def danger-50-opa-40 (alpha danger-50 0.4))
;;60 with transparency
(def danger-60-opa-10 (alpha danger-60 0.1))
;;;;Warning
(def warning-50 "#FF7D46")
(def warning-60 "#CC6438")
+8 -2
View File
@@ -5,11 +5,17 @@
in non-debug environments.
`value` is first transformed via `malli.core/schema` to make sure we fail fast
and only register valid schemas."
and only register valid schemas.
Pass raw schema `value` instead of schema instance when it's `:function` schema
https://github.com/status-im/status-mobile/pull/18364#issuecomment-1875543794"
[sym value]
`(if ^boolean js/goog.DEBUG
(try
(malli.core/=> ~sym (malli.core/schema ~value))
(let [schema-instance# (malli.core/schema ~value)]
(if (and (seq ~value) (= :function (first ~value)))
(malli.core/=> ~sym ~value)
(malli.core/=> ~sym schema-instance#)))
(catch js/Error e#
(taoensso.timbre/error "Failed to instrument function"
{:symbol ~sym :error e#})
+30 -1
View File
@@ -24,11 +24,17 @@
(defn get-label-by-type
[biometric-type]
(case biometric-type
(condp = biometric-type
:fingerprint (i18n/label :t/biometric-fingerprint)
:FaceID (i18n/label :t/biometric-faceid)
(i18n/label :t/biometric-touchid)))
(defn get-icon-by-type
[biometric-type]
(condp = biometric-type
:FaceID :i/face-id
:i/touch-id))
(re-frame/reg-fx
:biometric/get-supported-biometric-type
(fn []
@@ -126,3 +132,26 @@
{:events [:biometric/authenticate]}
[_ opts]
{:biometric/authenticate opts})
(rf/reg-event-fx
:biometric/on-enable-success
(fn [{:keys [db]} [password]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-biometric)
:dispatch [:keychain/save-password-and-auth-method
{:key-uid key-uid
:masked-password password}]})))
(rf/reg-event-fx
:biometric/enable
(fn [_ [password]]
{:dispatch [:biometric/authenticate
{:on-success #(rf/dispatch [:biometric/on-enable-success password])
:on-fail #(rf/dispatch [:biometric/show-message %])}]}))
(rf/reg-event-fx
:biometric/disable
(fn [{:keys [db]}]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-none)
:keychain/clear-user-password key-uid})))
@@ -1,4 +1,4 @@
(ns status-im.contexts.emoji-picker.constants
(ns status-im.common.emoji-picker.constants
(:require
[react-native.core :as rn]))
@@ -1,6 +1,6 @@
(ns status-im.contexts.emoji-picker.data
(ns status-im.common.emoji-picker.data
(:require
[status-im.contexts.emoji-picker.constants :as constants]
[status-im.common.emoji-picker.constants :as constants]
[utils.transforms :as transforms]))
;; Emoji data is pulled from the `emojibase` (https://emojibase.dev).
@@ -1,4 +1,4 @@
(ns status-im.contexts.emoji-picker.events
(ns status-im.common.emoji-picker.events
(:require
[utils.re-frame :as rf]))
@@ -1,8 +1,8 @@
(ns status-im.contexts.emoji-picker.style
(ns status-im.common.emoji-picker.style
(:require
[quo.foundations.colors :as colors]
[react-native.safe-area :as safe-area]
[status-im.contexts.emoji-picker.constants :as constants]))
[status-im.common.emoji-picker.constants :as constants]))
(def flex-spacer {:flex 1})
@@ -1,8 +1,8 @@
(ns status-im.contexts.emoji-picker.utils
(ns status-im.common.emoji-picker.utils
(:require
[clojure.string :as string]
[status-im.contexts.emoji-picker.constants :as constants]
[status-im.contexts.emoji-picker.data :refer [emoji-data]]))
[status-im.common.emoji-picker.constants :as constants]
[status-im.common.emoji-picker.data :refer [emoji-data]]))
(defn search-emoji
[search-query]
@@ -1,7 +1,7 @@
(ns status-im.contexts.emoji-picker.utils-test
(ns status-im.common.emoji-picker.utils-test
(:require
[cljs.test :refer [deftest is testing]]
[status-im.contexts.emoji-picker.utils :as utils]))
[status-im.common.emoji-picker.utils :as utils]))
(deftest emoji-search-test
(testing "search for emojis with name"
@@ -1,4 +1,4 @@
(ns status-im.contexts.emoji-picker.view
(ns status-im.common.emoji-picker.view
(:require
[clojure.string :as string]
[oops.core :as oops]
@@ -10,10 +10,10 @@
[react-native.gesture :as gesture]
[react-native.platform :as platform]
[reagent.core :as reagent]
[status-im.contexts.emoji-picker.constants :as constants]
[status-im.contexts.emoji-picker.data :as emoji-picker.data]
[status-im.contexts.emoji-picker.style :as style]
[status-im.contexts.emoji-picker.utils :as emoji-picker.utils]
[status-im.common.emoji-picker.constants :as constants]
[status-im.common.emoji-picker.data :as emoji-picker.data]
[status-im.common.emoji-picker.style :as style]
[status-im.common.emoji-picker.utils :as emoji-picker.utils]
[utils.debounce :as debounce]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
+3 -2
View File
@@ -8,8 +8,8 @@
[status-im.config :as config]
[status-im.constants :as constants]
[status-im.contexts.chat.actions.view :as chat-actions]
[status-im.contexts.chat.contacts.drawers.nickname-drawer.view :as nickname-drawer]
[status-im.contexts.communities.actions.chat.view :as communities-chat-actions]
[status-im.contexts.contacts.drawers.nickname-drawer.view :as nickname-drawer]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -95,7 +95,8 @@
:button-text (i18n/label :t/confirm)
:close-button-text (i18n/label :t/cancel)
:on-press (fn []
(hide-sheet-and-dispatch [:chat.ui/close-chat chat-id])
(hide-sheet-and-dispatch [:chat.ui/close-and-remove-chat
chat-id])
(when inside-chat?
(rf/dispatch [:navigate-back])))}])}]))
@@ -131,6 +131,7 @@
:keychain/clear-user-password
(fn [key-uid]
(keychain/reset-credentials (password-migration-key-name key-uid))
(keychain/reset-credentials (str key-uid "-auth"))
(keychain/reset-credentials key-uid)))
(re-frame/reg-fx
@@ -142,6 +143,11 @@
(.then #(when on-success (on-success)))
(.catch #(when on-error (on-error %))))))
(re-frame/reg-event-fx
:keychain/save-password-and-auth-method
(fn [_ [opts]]
{:keychain/save-password-and-auth-method opts}))
;; NOTE: migrating the plaintext password in the keychain
;; with the hashed one. Added due to the sync onboarding
;; flow, where the password arrives already hashed.
+12 -6
View File
@@ -180,6 +180,11 @@
app-state-listener (.addEventListener rn/app-state "change" set-torch-off-fn)]
#(.remove app-state-listener)))
(defn- navigate-back-handler
[]
(rf/dispatch [:navigate-back])
true)
(defn f-view-internal
[{:keys [title subtitle validate-fn on-success-scan error-message]}]
(let [insets (safe-area/get-insets)
@@ -197,15 +202,16 @@
(boolean (not-empty @qr-view-finder)))
camera-ready-to-scan? (and show-camera?
(not @qr-code-succeed?))]
(rn/use-effect
#(set-listener-torch-off-on-app-inactive torch?))
(rn/use-effect
(fn []
(rn/hw-back-add-listener navigate-back-handler)
(set-listener-torch-off-on-app-inactive torch?)
(when-not @camera-permission-granted?
(permissions/permission-granted? :camera
#(reset! camera-permission-granted? %)
#(reset! camera-permission-granted? false)))))
(permissions/permission-granted?
:camera
#(reset! camera-permission-granted? %)
#(reset! camera-permission-granted? false)))
#(rn/hw-back-remove-listener navigate-back-handler)))
[:<>
[rn/view {:style style/background}]
(when camera-ready-to-scan?
+3 -3
View File
@@ -5,11 +5,11 @@
[legacy.status-im.mailserver.core :as mailserver]
[legacy.status-im.visibility-status-updates.core :as visibility-status-updates]
[status-im.common.pairing.events :as pairing]
[status-im.contexts.chat.messages.link-preview.events :as link-preview]
[status-im.contexts.chat.messages.transport.events :as messages.transport]
[status-im.contexts.chat.messenger.messages.link-preview.events :as link-preview]
[status-im.contexts.chat.messenger.messages.transport.events :as messages.transport]
[status-im.contexts.communities.discover.events]
[status-im.contexts.profile.login.events :as profile.login]
[status-im.contexts.push-notifications.local.events :as local-notifications]
[status-im.contexts.profile.push-notifications.local.events :as local-notifications]
[taoensso.timbre :as log]
[utils.re-frame :as rf]
[utils.transforms :as transforms]))
@@ -6,3 +6,8 @@
(fn [{:keys [db]} [callback]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:keychain/get-user-password [key-uid callback]]]})))
(rf/reg-event-fx
:standard-auth/reset-login-password
(fn [{:keys [db]}]
{:db (update db :profile/login dissoc :password :error)}))
@@ -1,6 +1,5 @@
(ns status-im.common.standard-authentication.standard-auth.authorize
(:require
[native-module.core :as native-module]
[react-native.touch-id :as biometric]
[status-im.common.standard-authentication.enter-password.view :as enter-password]
[taoensso.timbre :as log]
@@ -18,11 +17,11 @@
auth-button-label theme blur? auth-button-icon-left]}]
(let [handle-auth-success (fn [biometric?]
(fn [entered-password]
(let [sha3-pwd (if biometric?
(str (security/safe-unmask-data entered-password))
(native-module/sha3 (str (security/safe-unmask-data
entered-password))))]
(on-auth-success sha3-pwd))))
(let [sha3-masked-password (if biometric?
entered-password
(security/hash-masked-password
entered-password))]
(on-auth-success sha3-masked-password))))
password-login (fn [{:keys [on-press-biometrics]}]
(rf/dispatch [:show-bottom-sheet
{:on-close on-close
@@ -53,10 +52,12 @@
(password-login {:on-press-biometrics
#(on-press-biometrics
on-press-biometrics)}))}))]
(biometric/get-supported-type
(fn [biometric-type]
(if (and biometric-auth? biometric-type)
(biometrics-login biometrics-login)
(do
(reset-password)
(password-login {})))))))
(if biometric-auth?
(biometric/get-supported-type
(fn [biometric-type]
(if biometric-type
(biometrics-login biometrics-login)
(do
(reset-password)
(password-login {})))))
(password-login {}))))
@@ -5,6 +5,7 @@
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im.common.standard-authentication.standard-auth.authorize :as authorize]
[status-im.constants :as constants]
[utils.re-frame :as rf]))
(defn- view-internal
@@ -15,7 +16,7 @@
#(reset! reset-slider? true)
200))
auth-method (rf/sub [:auth-method])
biometric-auth? (= auth-method "biometric")]
biometric-auth? (= auth-method constants/auth-method-biometric)]
(fn [{:keys [track-text
customization-color
auth-button-label
@@ -25,11 +26,11 @@
size
theme
blur?
container-style]}]
[rn/view {:style {:flex 1}}
container-style]
:or {container-style {:flex 1}}}]
[rn/view {:style container-style}
[quo/slide-button
{:size size
:container-style container-style
:customization-color customization-color
:on-reset (when @reset-slider? #(reset! reset-slider? false))
:on-complete #(authorize/authorize {:on-close on-close
+17 -18
View File
@@ -200,24 +200,23 @@
(handle-url url))))
(defn generate-profile-url
([cofx] (generate-profile-url cofx nil))
([{:keys [db]} [{:keys [public-key cb]}]]
(let [profile-public-key (get-in db [:profile/profile :public-key])
profile? (or (not public-key) (= public-key profile-public-key))
ens-name? (if profile?
(get-in db [:profile/profile :ens-name?])
(get-in db [:contacts/contacts public-key :ens-name]))
public-key (if profile? profile-public-key public-key)]
(when public-key
{:json-rpc/call
[{:method (if ens-name? "wakuext_shareUserURLWithENS" "wakuext_shareUserURLWithData")
:params [public-key]
:on-success (fn [url]
(rf/dispatch [:universal-links/save-profile-url public-key url])
(when (fn? cb) (cb)))
:on-error #(log/error "failed to wakuext_shareUserURLWithData"
{:error %
:public-key public-key})}]}))))
[{:keys [db]} [{:keys [public-key cb]}]]
(let [profile-public-key (get-in db [:profile/profile :public-key])
profile? (or (not public-key) (= public-key profile-public-key))
ens-name? (if profile?
(get-in db [:profile/profile :ens-name?])
(get-in db [:contacts/contacts public-key :ens-name]))
public-key (if profile? profile-public-key public-key)]
(when public-key
{:json-rpc/call
[{:method (if ens-name? "wakuext_shareUserURLWithENS" "wakuext_shareUserURLWithData")
:params [public-key]
:on-success (fn [url]
(rf/dispatch [:universal-links/save-profile-url public-key url])
(when (fn? cb) (cb)))
:on-error #(log/error "failed to wakuext_shareUserURLWithData"
{:error %
:public-key public-key})}]})))
(schema/=> generate-profile-url
[:=>
+31 -47
View File
@@ -5,11 +5,14 @@
[re-frame.core :as re-frame]
[status-im.common.universal-links :as links]))
(def pubkey
"0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073")
(deftest handle-url-test
(testing "the user is not logged in"
(testing "it stores the url for later processing"
(is (= {:db {:universal-links/url "some-url"}}
(links/handle-url {:db {}} "some-url")))))
(is (match? {:db {:universal-links/url "some-url"}}
(links/handle-url {:db {}} "some-url")))))
(testing "the user is logged in"
(let [db {:profile/profile {:public-key "pk"}
:app-state "active"
@@ -18,9 +21,9 @@
(is (nil? (get-in (links/handle-url {:db db} "some-url")
[:db :universal-links/url]))))
(testing "Handle a custom string"
(is (= (get-in (links/handle-url {:db db} "https://status.app/u#statuse2e")
[:router/handle-uri :uri])
"https://status.app/u#statuse2e"))))))
(is (match? (get-in (links/handle-url {:db db} "https://status.app/u#statuse2e")
[:router/handle-uri :uri])
"https://status.app/u#statuse2e"))))))
(deftest url-event-listener
(testing "the url is not nil"
@@ -28,77 +31,58 @@
(let [actual (atom nil)]
(with-redefs [re-frame/dispatch #(reset! actual %)]
(links/url-event-listener #js {:url "some-url"})
(is (= [:universal-links/handle-url "some-url"] @actual))))))
(is (match? [:universal-links/handle-url "some-url"] @actual))))))
(testing "the url is nil"
(testing "it does not dispatches the url"
(let [actual (atom nil)]
(with-redefs [re-frame/dispatch #(reset! actual %)]
(links/url-event-listener #js {})
(is (= nil @actual)))))))
(is (match? nil @actual)))))))
(deftest generate-profile-url
(testing "user has ens name"
(testing "it calls the ens rpc method with ens name as param"
(let [pubkey "pubkey"
db {:profile/profile {:ens-name? true :public-key pubkey}}
rst (links/generate-profile-url {:db db})]
(are [result expected] (= result expected)
(let [db {:profile/profile {:ens-name? true :public-key pubkey}}
rst (links/generate-profile-url {:db db} [])]
(are [result expected] (match? result expected)
"wakuext_shareUserURLWithENS" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first)))))
(testing "user has no ens name"
(testing "it calls the ens rpc method with public keyas param"
(let [pubkey "pubkey"
db {:profile/profile {:public-key pubkey}}
rst (links/generate-profile-url {:db db})]
(are [result expected] (= result expected)
(let [db {:profile/profile {:public-key pubkey}}
rst (links/generate-profile-url {:db db} [])]
(are [result expected] (match? result expected)
"wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first)))))
(testing "contact has ens name"
(testing "it calls the ens rpc method with ens name as param"
(let [pubkey "pubkey"
ens "ensname.eth"
db {:contacts/contacts {pubkey {:ens-name ens}}}
rst (links/generate-profile-url {:db db} [{:public-key pubkey}])]
(are [result expected] (= result expected)
(let [ens "ensname.eth"
db {:contacts/contacts {pubkey {:ens-name ens}}}
rst (links/generate-profile-url {:db db} [{:public-key pubkey}])]
(are [result expected] (match? result expected)
"wakuext_shareUserURLWithENS" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first)))))
(testing "contact has no ens name"
(testing "it calls the ens rpc method with public keyas param"
(let [pubkey "pubkey"
db {:contacts/contacts {pubkey {:public-key pubkey}}}
rst (links/generate-profile-url {:db db} [{:public-key pubkey}])]
(are [result expected] (= result expected)
(let [db {:contacts/contacts {pubkey {:public-key pubkey}}}
rst (links/generate-profile-url {:db db} [{:public-key pubkey}])]
(are [result expected] (match? result expected)
"wakuext_shareUserURLWithData" (-> rst :json-rpc/call first :method)
pubkey (-> rst :json-rpc/call first :params first))))))
(deftest save-profile-url
(testing "given a contact public key and profile url"
(testing "it updates the contact in db"
(let [pubkey "pubkey"
url "url"
db {:contacts/contacts {pubkey {:public-key pubkey}}}
rst (links/save-profile-url {:db db} [pubkey url])]
(is (= (get-in rst [:db :contacts/contacts pubkey :universal-profile-url]) url)))))
(let [url "url"
db {:contacts/contacts {pubkey {:public-key pubkey}}}
rst (links/save-profile-url {:db db} [pubkey url])]
(is (match? (get-in rst [:db :contacts/contacts pubkey :universal-profile-url]) url)))))
(testing "given a user public key and profile url"
(testing "it updates the user profile in db"
(let [pubkey "pubkey"
url "url"
db {:profile/profile {:public-key pubkey}}
rst (links/save-profile-url {:db db} [pubkey url])]
(is (= (get-in rst [:db :profile/profile :universal-profile-url]) url)))))
(testing "given a invalid url"
(testing "it returns the db untouched"
(let [pubkey "pubkey"
url "url"
db {:profile/profile {:public-key pubkey}}
rst (links/save-profile-url {:db db} ["invalid pubkey" url])]
(is (= (:db rst) db)))))
(testing "given a nil as url"
(testing "it returns nil"
(let [pubkey "pubkey"
db {:profile/profile {:public-key pubkey}}
rst (links/save-profile-url {:db db} ["invalid pubkey"])]
(is (nil? rst))))))
(let [url "url"
db {:profile/profile {:public-key pubkey}}
rst (links/save-profile-url {:db db} [pubkey url])]
(is (match? (get-in rst [:db :profile/profile :universal-profile-url]) url))))))
(deftest universal-link-test
(testing "universal-link?"
@@ -0,0 +1,41 @@
(ns status-im.common.validation.profile
(:require [clojure.string :as string]
[utils.i18n :as i18n]))
;; NOTE - validation should match with Desktop
;; https://github.com/status-im/status-desktop/blob/2ba96803168461088346bf5030df750cb226df4c/ui/imports/utils/Constants.qml#L468
(def min-length 5)
(def max-length 24)
(def emoji-regex
#"(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])")
(def status-regex #"^[a-zA-Z0-9\-_ ]+$")
(def common-names ["Ethereum" "Bitcoin"])
(defn has-emojis? [s] (boolean (re-find emoji-regex s)))
(defn has-common-names? [s] (pos? (count (filter #(string/includes? s %) common-names))))
(defn has-special-characters? [s] (not (re-find status-regex s)))
(defn name-too-short? [s] (< (count (string/trim (str s))) min-length))
(defn name-too-long? [s] (> (count (string/trim (str s))) max-length))
(defn validation-name
[s]
(cond
(or (= s nil) (= s "")) nil
(string/ends-with? s "-eth") (i18n/label :t/ending-not-allowed {:ending "-eth"})
(string/ends-with? s "_eth") (i18n/label :t/ending-not-allowed {:ending "_eth"})
(string/ends-with? s ".eth") (i18n/label :t/ending-not-allowed {:ending ".eth"})
(string/starts-with? s " ") (i18n/label :t/start-with-space)
(string/ends-with? s " ") (i18n/label :t/ends-with-space)
(has-common-names? s) (i18n/label :t/are-not-allowed {:check (i18n/label :t/common-names)})
(has-emojis? s) (i18n/label :t/are-not-allowed {:check (i18n/label :t/emojis)})
(has-special-characters? s) (i18n/label :t/are-not-allowed
{:check (i18n/label :t/special-characters)})
(name-too-short? s) (i18n/label :t/minimum-characters {:min-chars min-length})
(name-too-long? s) (i18n/label :t/profile-name-is-too-long)))
@@ -0,0 +1,52 @@
(ns status-im.common.validation.profile-test
(:require
[cljs.test :refer-macros [deftest are]]
[status-im.common.validation.profile :as profile-validator]
[utils.i18n :as i18n]))
(deftest has-emojis-test
(are [arg expected]
(expected (profile-validator/has-emojis? arg))
"Hello 😊" true?
"Hello" false?))
(deftest has-common-names-test
(are [arg expected]
(expected (profile-validator/has-common-names? arg))
"Ethereum" true?
"Hello" false?))
(deftest has-special-characters-test
(are [arg expected]
(expected (profile-validator/has-special-characters? arg))
"@name" true?
"name" false?))
(deftest name-too-short-test
(are [arg expected]
(expected (profile-validator/name-too-short? arg))
"abc" true?
"abcdef" false?))
(deftest name-too-long-test
(are [arg expected]
(expected (profile-validator/name-too-long? arg))
(apply str (repeat 25 "a")) true?
"abcdef" false?))
(deftest validation-name-test
(are [arg expected]
(= (profile-validator/validation-name arg) expected)
nil nil
"" nil
"@name" (i18n/label :t/are-not-allowed
{:check (i18n/label :t/special-characters)})
"name-eth" (i18n/label :t/ending-not-allowed {:ending "-eth"})
"name_eth" (i18n/label :t/ending-not-allowed {:ending "_eth"})
"name.eth" (i18n/label :t/ending-not-allowed {:ending ".eth"})
" name" (i18n/label :t/start-with-space)
"name " (i18n/label :t/ends-with-space)
"Ethereum" (i18n/label :t/are-not-allowed {:check (i18n/label :t/common-names)})
"Hello 😊" (i18n/label :t/are-not-allowed {:check (i18n/label :t/emojis)})
"abc" (i18n/label :t/minimum-characters {:min-chars 5})
(apply str (repeat 25 "a")) (i18n/label :t/profile-name-is-too-long)))
+4 -1
View File
@@ -19,6 +19,9 @@
(def mainnet-rpc-url (str "https://eth-archival.gateway.pokt.network/v1/lb/" POKT_TOKEN))
(def goerli-rpc-url (str "https://goerli-archival.gateway.pokt.network/v1/lb/" POKT_TOKEN))
(def mainnet-chain-explorer-link "https://etherscan.io/address/")
(def optimism-mainnet-chain-explorer-link "https://optimistic.etherscan.io/address/")
(def arbitrum-mainnet-chain-explorer-link "https://arbiscan.io/address/")
(def opensea-api-key OPENSEA_API_KEY)
(def bootnodes-settings-enabled? (enabled? (get-config :BOOTNODES_SETTINGS_ENABLED "1")))
(def mailserver-confirmations-enabled? (enabled? (get-config :MAILSERVER_CONFIRMATIONS_ENABLED)))
@@ -91,7 +94,7 @@
(def mainnet-networks
[{:id "mainnet_rpc"
:chain-explorer-link "https://etherscan.io/address/"
:chain-explorer-link mainnet-chain-explorer-link
:name "Mainnet with upstream RPC"
:config {:NetworkId (chain/chain-keyword->chain-id :mainnet)
:DataDir "/ethereum/mainnet_rpc"
@@ -1,6 +1,7 @@
(ns status-im.contexts.chat.actions.view
(:require
[quo.core :as quo]
[utils.debounce :as debounce]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -22,10 +23,14 @@
:label (i18n/label :t/new-chat)
:on-press (fn []
(rf/dispatch [:group-chat/clear-contacts])
(rf/dispatch [:open-modal :start-a-new-chat]))}
(debounce/dispatch-and-chill
[:open-modal :start-a-new-chat]
1000))}
{:icon :i/add-user
:accessibility-label :add-a-contact
:label (i18n/label :t/add-a-contact)
:sub-label (i18n/label :t/enter-a-chat-key)
:add-divider? true
:on-press #(rf/dispatch [:open-modal :new-contact])}]]])
:on-press #(debounce/dispatch-and-chill
[:open-modal :new-contact]
1000)}]]])
@@ -1,55 +0,0 @@
(ns status-im.contexts.chat.composer.sub-view
(:require
[quo.core :as quo]
[react-native.blur :as blur]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[status-im.contexts.chat.composer.style :as style]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn bar
[theme]
[rn/view {:style style/bar-container}
[rn/view {:style (style/bar theme)}]])
(defn f-blur-view
[{:keys [layout-height focused? theme]}]
[reanimated/view {:style (style/blur-container layout-height focused?)}
[blur/view (style/blur-view theme)]])
(defn blur-view
[props]
[:f> f-blur-view props])
(defn- f-shell-button
[{:keys [focused?]} scroll-to-bottom-fn show-floating-scroll-down-button?]
(let [customization-color (rf/sub [:profile/customization-color])
hide-shell? (or @focused? @show-floating-scroll-down-button?)
y-shell (reanimated/use-shared-value (if hide-shell? 35 0))
opacity (reanimated/use-shared-value (if hide-shell? 0 1))]
(rn/use-effect
(fn []
(reanimated/animate opacity (if hide-shell? 0 1))
(reanimated/animate y-shell (if hide-shell? 35 0)))
[@focused? @show-floating-scroll-down-button?])
[:<>
[reanimated/view
{:style (style/shell-button y-shell opacity)}
[quo/floating-shell-button
{:jump-to
{:on-press (fn []
(rf/dispatch [:shell/navigate-to-jump-to]))
:customization-color customization-color
:label (i18n/label :t/jump-to)
:style {:align-self :center}}}
{}]]
(when (and (not @focused?)
@show-floating-scroll-down-button?)
[quo/floating-shell-button
{:scroll-to-bottom {:on-press scroll-to-bottom-fn}}
style/scroll-to-bottom-button])]))
(defn shell-button
[state scroll-to-bottom-fn show-floating-scroll-down-button?]
[:f> f-shell-button state scroll-to-bottom-fn show-floating-scroll-down-button?])
@@ -1,4 +1,4 @@
(ns status-im.contexts.contacts.drawers.nickname-drawer.style
(ns status-im.contexts.chat.contacts.drawers.nickname-drawer.style
(:require
[quo.foundations.colors :as colors]
[react-native.platform :as platform]))
@@ -1,4 +1,4 @@
(ns status-im.contexts.contacts.drawers.nickname-drawer.view
(ns status-im.contexts.chat.contacts.drawers.nickname-drawer.view
(:require
[clojure.string :as string]
[quo.core :as quo]
@@ -7,7 +7,7 @@
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im.constants :as constants]
[status-im.contexts.contacts.drawers.nickname-drawer.style :as style]
[status-im.contexts.chat.contacts.drawers.nickname-drawer.style :as style]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -1,4 +1,4 @@
(ns status-im.contexts.contacts.events
(ns status-im.contexts.chat.contacts.events
(:require
[oops.core :as oops]
[status-im.constants :as constants]
+7
View File
@@ -1,6 +1,7 @@
(ns status-im.contexts.chat.effects
(:require
[react-native.async-storage :as async-storage]
[status-im.contexts.chat.messenger.messages.list.state :as chat.state]
[status-im.contexts.shell.jump-to.constants :as shell.constants]
[utils.re-frame :as rf]))
@@ -16,3 +17,9 @@
(when (= stored-key-uid key-uid)
(rf/dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id
shell.constants/open-screen-without-animation])))))))))
(rf/reg-fx :effects.chat/scroll-to-bottom
(fn []
(some-> ^js @chat.state/messages-list-ref
(.scrollToOffset #js
{:animated true}))))
+14 -9
View File
@@ -7,14 +7,14 @@
[reagent.core :as reagent]
[status-im.common.muting.helpers :refer [format-mute-till]]
[status-im.constants :as constants]
[status-im.contexts.chat.composer.link-preview.events :as link-preview]
[status-im.contexts.chat.contacts.events :as contacts-store]
status-im.contexts.chat.effects
status-im.contexts.chat.lightbox.events
status-im.contexts.chat.messages.content.reactions.events
[status-im.contexts.chat.messages.delete-message-for-me.events :as delete-for-me]
[status-im.contexts.chat.messages.delete-message.events :as delete-message]
[status-im.contexts.chat.messages.list.state :as chat.state]
[status-im.contexts.contacts.events :as contacts-store]
[status-im.contexts.chat.messenger.composer.link-preview.events :as link-preview]
status-im.contexts.chat.messenger.lightbox.events
status-im.contexts.chat.messenger.messages.content.reactions.events
[status-im.contexts.chat.messenger.messages.delete-message-for-me.events :as delete-for-me]
[status-im.contexts.chat.messenger.messages.delete-message.events :as delete-message]
[status-im.contexts.chat.messenger.messages.list.state :as chat.state]
[status-im.navigation.events :as navigation]
[taoensso.timbre :as log]
[utils.datetime :as datetime]
@@ -281,7 +281,7 @@
(rf/defn close-and-remove-chat
"Closes the chat and removes it from chat list while retaining history, producing all necessary effects for that"
{:events [:chat.ui/close-chat]}
{:events [:chat.ui/close-and-remove-chat]}
[{:keys [db now] :as cofx} chat-id]
(rf/merge cofx
{:effects/push-notifications-clear-message-notifications [chat-id]
@@ -386,7 +386,7 @@
:confirm-button-text (i18n/label :t/delete)
:on-accept #(do
(rf/dispatch [:hide-bottom-sheet])
(rf/dispatch [:chat.ui/close-chat chat-id]))}})
(rf/dispatch [:chat.ui/close-and-remove-chat chat-id]))}})
(rf/defn navigate-to-user-pinned-messages
"Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data"
@@ -447,3 +447,8 @@
:params [{:id chat-id}]
:on-success #()
:on-error #(log/error "failed to fetch messages for chat" chat-id %)}]})
(rf/defn scroll-to-bottom
{:events [:chat.ui/scroll-to-bottom]}
[_]
{:effects.chat/scroll-to-bottom nil})
@@ -1,4 +1,4 @@
(ns status-im.contexts.add-new-contact.effects
(ns status-im.contexts.chat.home.add-new-contact.effects
(:require
[legacy.status-im.ethereum.ens :as ens]
[native-module.core :as native-module]
@@ -1,9 +1,9 @@
(ns status-im.contexts.add-new-contact.events
(ns status-im.contexts.chat.home.add-new-contact.events
(:require
[clojure.string :as string]
[status-im.common.validators :as validators]
status-im.contexts.add-new-contact.effects
[status-im.contexts.contacts.events :as data-store.contacts]
[status-im.contexts.chat.contacts.events :as data-store.contacts]
status-im.contexts.chat.home.add-new-contact.effects
[status-im.navigation.events :as navigation]
[utils.ens.stateofus :as stateofus]
[utils.ethereum.chain :as chain]
@@ -1,8 +1,8 @@
(ns status-im.contexts.add-new-contact.events-test
(ns status-im.contexts.chat.home.add-new-contact.events-test
(:require
[cljs.test :refer-macros [deftest are]]
matcher-combinators.test
[status-im.contexts.add-new-contact.events :as events]))
[status-im.contexts.chat.home.add-new-contact.events :as events]))
(def user-ukey
"0x04ca27ed9c7c4099d230c6d8853ad0cfaf084a019c543e9e433d3c04fac6de9147cf572b10e247cfe52f396b5aa10456b56dd1cf1d8a681e2b93993d44594b2e85")
@@ -1,4 +1,4 @@
(ns status-im.contexts.add-new-contact.scan.scan-profile-qr-page
(ns status-im.contexts.chat.home.add-new-contact.scan.scan-profile-qr-page
(:require [react-native.core :as rn]
[react-native.hooks :as hooks]
[status-im.common.scan-qr-code.view :as scan-qr-code]
@@ -1,4 +1,4 @@
(ns status-im.contexts.add-new-contact.style
(ns status-im.contexts.chat.home.add-new-contact.style
(:require [quo.foundations.colors :as colors]
[react-native.safe-area :as safe-area]))
@@ -1,10 +1,10 @@
(ns status-im.contexts.add-new-contact.views
(ns status-im.contexts.chat.home.add-new-contact.views
(:require [clojure.string :as string]
[quo.core :as quo]
[react-native.clipboard :as clipboard]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im.contexts.add-new-contact.style :as style]
[status-im.contexts.chat.home.add-new-contact.style :as style]
[utils.address :as address]
[utils.debounce :as debounce]
[utils.i18n :as i18n]
@@ -1,4 +1,4 @@
(ns status-im.contexts.chat.new-chat.styles
(ns status-im.contexts.chat.home.new-chat.styles
(:require
[react-native.safe-area :as safe-area]))
@@ -1,4 +1,4 @@
(ns status-im.contexts.chat.new-chat.view
(ns status-im.contexts.chat.home.new-chat.view
(:require
[quo.core :as quo]
[quo.foundations.colors :as colors]
@@ -10,7 +10,7 @@
[status-im.common.contact-list.view :as contact-list]
[status-im.common.resources :as resources]
[status-im.constants :as constants]
[status-im.contexts.chat.new-chat.styles :as style]
[status-im.contexts.chat.home.new-chat.styles :as style]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -1,4 +1,4 @@
(ns status-im.contexts.chat.camera.style
(ns status-im.contexts.chat.messenger.camera.style
(:require
[quo.foundations.colors :as colors]
[react-native.platform :as platform]
@@ -1,4 +1,4 @@
(ns status-im.contexts.chat.camera.view
(ns status-im.contexts.chat.messenger.camera.view
(:require
[oops.core :refer [oget]]
[quo.core :as quo]
@@ -11,7 +11,7 @@
[react-native.reanimated :as reanimated]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im.contexts.chat.camera.style :as style]
[status-im.contexts.chat.messenger.camera.style :as style]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -1,7 +1,7 @@
(ns status-im.contexts.chat.composer.actions.style
(ns status-im.contexts.chat.messenger.composer.actions.style
(:require
[react-native.reanimated :as reanimated]
[status-im.contexts.chat.composer.constants :as constants]))
[status-im.contexts.chat.messenger.composer.constants :as constants]))
(def actions-container
{:height constants/actions-container-height
@@ -1,4 +1,4 @@
(ns status-im.contexts.chat.composer.actions.view
(ns status-im.contexts.chat.messenger.composer.actions.view
(:require
[quo.core :as quo]
[react-native.core :as rn]
@@ -9,8 +9,8 @@
[status-im.common.alert.effects :as alert.effects]
[status-im.common.device-permissions :as device-permissions]
[status-im.constants :as constants]
[status-im.contexts.chat.composer.actions.style :as style]
[status-im.contexts.chat.composer.constants :as comp-constants]
[status-im.contexts.chat.messenger.composer.actions.style :as style]
[status-im.contexts.chat.messenger.composer.constants :as comp-constants]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -20,8 +20,7 @@
{:keys [text-value focused? maximized?]}
{:keys [height saved-height last-height opacity background-y container-opacity]}
window-height
edit
scroll-to-bottom-fn]
edit]
(reanimated/animate height comp-constants/input-height)
(reanimated/set-shared-value saved-height comp-constants/input-height)
(reanimated/set-shared-value last-height comp-constants/input-height)
@@ -39,11 +38,11 @@
(reset! text-value "")
(reset! sending-links? false)
(reset! sending-images? false)
(when (and (not (some? edit)) scroll-to-bottom-fn)
(scroll-to-bottom-fn)))
(when-not (some? edit)
(rf/dispatch [:chat.ui/scroll-to-bottom])))
(defn f-send-button
[props state animations window-height images? btn-opacity scroll-to-bottom-fn z-index edit]
[props state animations window-height images? btn-opacity z-index edit]
(let [{:keys [text-value]} state
customization-color (rf/sub [:profile/customization-color])]
(rn/use-effect (fn []
@@ -61,19 +60,17 @@
[reanimated/view
{:style (style/send-button btn-opacity @z-index)}
[quo/button
{:icon-only? true
:size 32
{:icon-only? true
:size 32
:customization-color customization-color
:accessibility-label :send-message-button
:on-press #(send-message props state animations window-height edit scroll-to-bottom-fn)}
:on-press #(send-message props state animations window-height edit)}
:i/arrow-up]]))
(defn send-button
[props {:keys [text-value] :as state} animations window-height images? edit btn-opacity
scroll-to-bottom-fn]
[props {:keys [text-value] :as state} animations window-height images? edit btn-opacity]
(let [z-index (reagent/atom (if (and (empty? @text-value) (not images?)) 0 1))]
[:f> f-send-button props state animations window-height images? btn-opacity scroll-to-bottom-fn
z-index edit]))
[:f> f-send-button props state animations window-height images? btn-opacity z-index edit]))
(defn disabled-audio-button
[opacity]
@@ -231,7 +228,7 @@
:icon :i/format}])
(defn view
[props state animations window-height insets scroll-to-bottom-fn {:keys [edit images]}]
[props state animations window-height insets {:keys [edit images]}]
(let [send-btn-opacity (reanimated/use-shared-value 0)
audio-btn-opacity (reanimated/interpolate send-btn-opacity [0 1] [1 0])]
[rn/view {:style style/actions-container}
@@ -242,8 +239,7 @@
[image-button props animations insets edit]
[reaction-button]
[format-button]]
[:f> send-button props state animations window-height images edit send-btn-opacity
scroll-to-bottom-fn]
[:f> send-button props state animations window-height images edit send-btn-opacity]
(when (and (not edit) (not images))
;; TODO(alwx): needs to be replaced with an `audio-button` later. See
;; https://github.com/status-im/status-mobile/issues/16084 for more details.
@@ -1,4 +1,4 @@
(ns status-im.contexts.chat.composer.constants
(ns status-im.contexts.chat.messenger.composer.constants
(:require
[quo.foundations.typography :as typography]
[react-native.platform :as platform]))
@@ -1,4 +1,4 @@
(ns status-im.contexts.chat.composer.edit.style)
(ns status-im.contexts.chat.messenger.composer.edit.style)
(def container
{:flex-direction :row

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