Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a40a7e3bb | ||
|
|
e6068fd4c1 | ||
|
|
579b382c44 | ||
|
|
859cb19886 | ||
|
|
2d92b515b8 | ||
|
|
972e1eee01 | ||
|
|
6c2b437e62 | ||
|
|
94a3e266a9 | ||
|
|
716007dc3c | ||
|
|
233d1b3261 | ||
|
|
25381b601f | ||
|
|
4a874ce48d | ||
|
|
ca822ff51d | ||
|
|
88b3d7d2e6 | ||
|
|
90d3a5a39d | ||
|
|
dd8beeb848 | ||
|
|
abbf29d4ca | ||
|
|
cebec10aa5 | ||
|
|
55b233c528 | ||
|
|
68ad4fe086 | ||
|
|
06c31f3a12 | ||
|
|
954373105d | ||
|
|
2aeafa30bd | ||
|
|
5920c35677 | ||
|
|
1f71883fa4 | ||
|
|
c9b3196203 | ||
|
|
6ebd5b421c | ||
|
|
901644e023 | ||
|
|
df256bf2b4 | ||
|
|
56492949f1 | ||
|
|
ab2ad0ec12 | ||
|
|
fe2272b703 | ||
|
|
c0b21aa315 | ||
|
|
876b020a32 | ||
|
|
8ed7059aa9 | ||
|
|
125c117bac | ||
|
|
078976806a |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"clojure.format.enable": false,
|
||||
"[clojure]": {
|
||||
"editor.defaultFormatter": "rflagreca.vscode-clj-zprint"
|
||||
},
|
||||
"vscode-clj-zprint.IgnoreExternalFiles": false,
|
||||
"vscode-clj-zprint.CommunityFormatting": false,
|
||||
"vscode-clj-zprint.OptionsMap": "",
|
||||
"vscode-clj-zprint.width": "",
|
||||
"vscode-clj-zprint.Styles.UseOnlyTheseStyles": false
|
||||
}
|
||||
@@ -362,6 +362,7 @@ component-test-watch: export TARGET := clojure
|
||||
component-test-watch: export COMPONENT_TEST := true
|
||||
component-test-watch: export BABEL_ENV := test
|
||||
component-test-watch: ##@ Watch tests and re-run no changes to cljs files
|
||||
@@scripts/check-metro-shadow-process.sh
|
||||
rm -rf ./component-spec
|
||||
yarn install
|
||||
nodemon --exec 'yarn shadow-cljs compile component-test && jest --config=test/jest/jest.config.js' -e cljs
|
||||
@@ -370,6 +371,7 @@ component-test: export TARGET := clojure
|
||||
component-test: export COMPONENT_TEST := true
|
||||
component-test: export BABEL_ENV := test
|
||||
component-test: ##@test Run component tests once in NodeJS
|
||||
@scripts/check-metro-shadow-process.sh
|
||||
rm -rf ./component-spec
|
||||
yarn install
|
||||
yarn shadow-cljs compile component-test && \
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
[Get it on Github](https://github.com/status-im/status-mobile/releases)
|
||||
|
||||
Join us in creating a browser, messenger, and gateway to a decentralized world. Status is a free (libre) open source mobile client targeting Android & iOS built entirely on [Ethereum](https://ethereum.org/) technologies. That's right, no middle men and `go-ethereum` running directly on your device.
|
||||
Join us in creating a browser, messenger, and gateway to a decentralized world. Status is a free (libre) open source mobile client targeting Android & iOS built entirely on [Ethereum](https://ethereum.org/) technologies. That's right, no middlemen and `go-ethereum` running directly on your device.
|
||||
|
||||
[](https://www.youtube.com/watch?v=oDCSEmS9c3o "Getting started with Status")
|
||||
|
||||
@@ -14,9 +14,9 @@ Join us in creating a browser, messenger, and gateway to a decentralized world.
|
||||
|
||||
We believe in a medium of pure free trade, economies with fair, permission-less access and a world without intermediaries. We want to create policies that can exist between friends or scale globally, we want to communicate securely and be uninhibited by legacy systems.
|
||||
|
||||
We want to take responsibility for our data, the way we conduct ourselves privately and promote this way of life to a mass audience.
|
||||
We want to take responsibility for our data, and the way we conduct ourselves privately and promote this way of life to a mass audience.
|
||||
|
||||
We want deep insights into our own economies so we can make informed, data-driven decisions on how to make our lives better. The Ethereum blockchain, Smart Contracts, Swarm and Whisper provide us a path forward.
|
||||
We want deep insights into our own economies so we can make informed, data-driven decisions on how to make our lives better. The Ethereum blockchain, Smart Contracts, Swarm and Whisper provide us with a path forward.
|
||||
|
||||
If this interests you, **help us make Status a reality** - anyone can contribute and we need everyone at any skill level to participate.
|
||||
|
||||
@@ -37,26 +37,26 @@ project dependencies, coding guidelines and testing procedures.
|
||||
Check out our [coding guidelines](doc/new-guidelines.md).
|
||||
|
||||
- **Community Management**
|
||||
Metcalfe's law states that the value of a network is proportional to the square of the number of connected users of the system - without community Status is meaningless. We're looking to create a positive, fun environment to explore new ideas, experiment and grow the Status community. Building a community takes a lot of work but the people you'll meet and long lasting relationships you form will be well worth it, check out our [Mission and Community Principles](https://status.im/about)
|
||||
Metcalfe's law states that the value of a network is proportional to the square of the number of connected users of the system - without community Status is meaningless. We're looking to create a positive, fun environment to explore new ideas, experiment and grow the Status community. Building a community takes a lot of work but the people you'll meet and the long-lasting relationships you form will be well worth it, check out our [Mission and Community Principles](https://status.im/about)
|
||||
|
||||
- **Specification / Documentation**
|
||||
John Dewey once said "Education is not preparation for life; education is life *itself* ". Developers and Designers need guidance and it all starts from documentation and specifications. Our software is only as good as its documentation, check out our [docs](doc/) and see how you can improve what we have.
|
||||
John Dewey once said, "Education is not preparation for life; education is life *itself* ". Developers and Designers need guidance and it all starts from documentation and specifications. Our software is only as good as its documentation, check out our [docs](doc/) and see how you can improve what we have.
|
||||
|
||||
- **Blog Writing**
|
||||
Content is King, keeping our blog up to date and informing the community of news helps keep everyone on the same page.
|
||||
|
||||
- **Testers**
|
||||
It's bug hunting season! Status is currently under active development and there is sure to be a bunch of learning, [build status from scratch](doc/starting-guide.md) or if an android user check out our [nightly builds](https://status.im/nightly). You can shake your phone to submit bug reports, or start browsing our [Github Issues](https://github.com/status-im/status-mobile/issues). Every bug you find brings Status closer to stable, usable software for everyone to enjoy!
|
||||
It's bug-hunting season! Status is currently under active development and there is sure to be a bunch of learning, [build status from scratch](doc/starting-guide.md) or if an android user checks out our [nightly builds](https://status.im/nightly). You can shake your phone to submit bug reports, or start browsing our [Github Issues](https://github.com/status-im/status-mobile/issues). Every bug you find brings Status closer to stable, usable software for everyone to enjoy!
|
||||
|
||||
- **Security**
|
||||
Status is a visual interface to make permanent changes on the Blockchain, it handles crypto-tokens that have real value and allows 3rd party code execution. Security is paramount to its success. You are given permission to break Status as hard as you can, as long as you share your findings with the community!
|
||||
|
||||
- **Evangelism**
|
||||
Help us spread the word! Tell a friend *right now*, in fact tell **everyone** - yell from a mountain if you have to, every person counts! If you've got a great story to tell or have some interesting way you've spread the word about Status let us know about it in our [chat](https://join.status.im/chat/public/status)
|
||||
Help us spread the word! Tell a friend *right now*, in fact, tell **everyone** - yell from a mountain if you have to, every person counts! If you've got a great story to tell or have some interesting way you've spread the word about Status let us know about it in our [chat](https://join.status.im/chat/public/status)
|
||||
|
||||
## Give me Binaries!
|
||||
|
||||
You can get our Beta builds for both Android and iOS on our [website](https://status.im), through our [nightly builds](https://status.im/nightly/) or by [building it yourself](https://status.im/technical/build_status/).
|
||||
You can get our Beta builds for both Android and iOS on our [website](https://status.im), through our [nightly builds](https://status.im/nightly/), or by [building it yourself](https://status.im/technical/build_status/).
|
||||
|
||||
## Core Contributors
|
||||
|
||||
|
||||
@@ -99,3 +99,38 @@ Also to inspect logs in a more flexible manner, instead of the strict output fro
|
||||
adb logcat | grep 'ReactNativeJS\|StatusModule\|GoLog'
|
||||
```
|
||||
|
||||
### From @clauxx (visual debugging):
|
||||
|
||||
When building/fixing/refactoring components, it can be annoying to set-up the component for instant visual feedback (i.e. finding it in the UI when fixing/refactoring; finding a place for it when building from scratch). Very often if the location of the screen containing your component nested deeply in the app, you might have to do a lot of repeated navigation, nevermind logging in every time the app reloads (even with fast refresh disabled this happens quite often).
|
||||
|
||||
To alleviate this discomfort and make the development cycle more *pleasant*, you can use one of the following options (*_both of which involve the ... REPL ༼ つ ╹ ╹ ༽つ, so make sure you have that set-up with your IDE/editor_*)
|
||||
|
||||
#### REPL-ing to a screen
|
||||
|
||||
This one is straightforward, just evaluate a navigation dispatch form from anywhere:
|
||||
|
||||
```cljs
|
||||
(comment (rf/dispatch [:navigate-to :your-favorite-buggy-screen]))
|
||||
```
|
||||
|
||||
> TIP: you can run this from an untracked user namespace `(ns user)`, from which you can experiment or interact with the REPL. This is where I usually keep my development navigation forms.
|
||||
|
||||
#### REPL-ing a component
|
||||
|
||||
This is also straighforward, but there are some small differences. Just like above, we only have to evaluate a dispatch form, as follows:
|
||||
|
||||
```cljs
|
||||
;; example debugging a quo component, although it could be any other component e.g. from the `status-im2` ns.
|
||||
(comment (re-frame/dispatch [:dev/preview-component
|
||||
[quo/slide-button
|
||||
{:track-icon :face-id
|
||||
:track-text "preview component example"
|
||||
:customization-color :blue
|
||||
:on-complete identity}]]))
|
||||
```
|
||||
|
||||
When evaluated, a full-screen bottom sheet will appear with your component inside. You can make changes to the props or the component internals and re-evaluate the changed forms, after which the component will be re-mounted without running a reload (unless you save the file). Other times it's quicker to just hit save and the component will be reloaded as usual (except for the props you evaluated, which have to be re-evaluated when changed).
|
||||
|
||||
> TIP: You can have multiple versions of the component as separate comments to check for the component variations
|
||||
|
||||
> TIP: You can leave `comment` blocks at the end of component files (other than quo, since can be previewed in other ways) with usages of components, which other developers can use for visualisation/testing.
|
||||
|
||||
@@ -18,7 +18,7 @@ While neat in theory, it has some serious downsides:
|
||||
As a result of that, more dynamic/state requiring things (like the live tx detail in `/send` command messages) were very hard to do,
|
||||
so instead of "eating our own dogfood", we decided to side-step the API and implement such things as hard-coded logic in the app, while partly
|
||||
retaining the js code for "easier" things (like parameter declaration).
|
||||
Needles to say, such efforts produced code of very poor quality, riddling our app with hard-coded "magic" everywhere in the codebase, completly
|
||||
Needles to say, such efforts produced code of very poor quality, riddling our app with hard-coded "magic" everywhere in the codebase, completely
|
||||
defeating the point of "dogfooding" while still requiring more effort and being much more error prone (no way to unit test jail logic) because
|
||||
of the need to asynchronously communicate with jail for leftover logic in command messages (the parts not hardcoded in app).
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ accepted
|
||||
|
||||
There was a generally dissatisfaction with our PR flow process from multiple stakeholders, including devs, QA and design. These largely centered around size, speed of integration and quality of PRs.
|
||||
|
||||
For more details, please see pain points in the meeting notes held end of February here: https://notes.status.im/C5pj8g7gQOu9Wo8PtDZsMw?edit# as well as the preceeding Discuss thread: https://discuss.status.im/t/better-pull-requests-process/1044
|
||||
For more details, please see pain points in the meeting notes held end of February here: https://notes.status.im/C5pj8g7gQOu9Wo8PtDZsMw?edit# as well as the preceding Discuss thread: https://discuss.status.im/t/better-pull-requests-process/1044
|
||||
|
||||
Also see conversations in Core Dev Call #12 and #13: https://github.com/status-im/pm/
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ The whitepaper also proposes that the deposit is only forfeited to the recipient
|
||||
|
||||
Considering:
|
||||
- the absence of efficient ways to perform anonymous transactions (zk-snarks could be used in the future for that)
|
||||
- the impossibility to prove that a recipient has made an actual reply and not some kind of automated reply (captcha solution was proposed, but wouldn't be pratical until we can use a solution such as swarm feeds that allow users to make free updates to their captcha without on-chain transactions)
|
||||
- the impossibility to prove that a recipient has made an actual reply and not some kind of automated reply (captcha solution was proposed, but wouldn't be practical until we can use a solution such as swarm feeds that allow users to make free updates to their captcha without on-chain transactions)
|
||||
- the limited time to develop the feature
|
||||
|
||||
We opted for a solution that:
|
||||
@@ -28,8 +28,8 @@ We opted for a solution that:
|
||||
|
||||
## Manifests
|
||||
|
||||
Since TtT related informations aren't stored on chain, they need to be stored somewhere else.
|
||||
For the first iteration of TtT we opted for IPFS, which is already used accross the app for other forms of content, through Infura IPFS gateway.
|
||||
Since TtT related information aren't stored on chain, they need to be stored somewhere else.
|
||||
For the first iteration of TtT we opted for IPFS, which is already used across the app for other forms of content, through Infura IPFS gateway.
|
||||
|
||||
On IPFS we store what we call a ttt manifest, which is a json file with the following format:
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ tests (otherwise some PRs could wait their turn of the scheduled Jenkins job til
|
||||
|
||||
## Analysing test results (and why test fails to pass)
|
||||
|
||||
After automated test run finished test results could be found in GH comment (if the test suite ran agaist PR) and TestRail. There are two states of the test: Passed and Failed. Test failure happens when certain condition of test step has not met or automated test can not proceed execution because it can not find the respective element on screen it expects should be there.
|
||||
After automated test run finished test results could be found in GH comment (if the test suite ran against PR) and TestRail. There are two states of the test: Passed and Failed. Test failure happens when certain condition of test step has not met or automated test can not proceed execution because it can not find the respective element on screen it expects should be there.
|
||||
|
||||
Several examples of when test fails to succeed:
|
||||
|
||||
|
||||
@@ -17,9 +17,10 @@ You can use any Clojure enabled editor. Here are instructions for developing the
|
||||
- [REPL!](#repl)
|
||||
- [Connecting to REPL to IntelliJ](#connecting-to-repl-to-intellij)
|
||||
- [Connecting REPL and IntelliJ to `status-mobile`](#connecting-repl-and-intellij-to-status-mobile)
|
||||
- [Using Calva](#using-calva)
|
||||
- [Start and connect the REPL](#start-and-connect-the-repl)
|
||||
- [Use the REPL](#use-the-repl)
|
||||
- [Visual Studio Code](#visual-studio-code)
|
||||
- [Set up the REPL](#set-up-the-repl)
|
||||
- [Auto-format files on save](#auto-format-files-on-save)
|
||||
- [Using Emacs with CIDER](#using-emacs-with-cider)
|
||||
|
||||
## Using Cursive
|
||||
|
||||
@@ -160,24 +161,24 @@ Following the above should give you the below result:
|
||||
|
||||
For additional details on issues you may face when setting up REPL with Cursive [see this document](https://notes.status.im/9Gr7kqF8SzC_SmYK0eB7uQ?view#Connecting-Cursive--IntelliJ-IDEA-to-REPL-Problems)
|
||||
|
||||
## Using Calva
|
||||
## Visual Studio Code
|
||||
|
||||
For VS Code users.
|
||||
### Set up the REPL
|
||||
|
||||
0. Install Calva.
|
||||
1. Install Calva extension.
|
||||
2. Open the `status-mobile` folder.
|
||||
3. Start [Status development](starting-guide.md#development) (Starting the
|
||||
`run-clojure` and `run-metro` jobs in split view in the VS Code integrated
|
||||
terminal works great).
|
||||
4. Run the VS Code command: **Calva: Connect to a running REPL Server in the
|
||||
project**.
|
||||
- Select the project type `shadow-cljs`.
|
||||
- Accept the suggested connection `host:port`.
|
||||
- Select to connect to the `:mobile` build.
|
||||
|
||||
### Start and connect the REPL
|
||||
|
||||
1. Open the `status-mobile` folder.
|
||||
1. Start [Status development](starting-guide.md#development) (Starting the `run-clojure` and `run-metro` jobs in split view in the VS Code integrated terminal works great.)
|
||||
1. Run the VS Code command: **Calva: Connect to a running REPL Server in the project**
|
||||
1. Select the project type `shadow-cljs`
|
||||
1. Accept the suggested connection `host:port`
|
||||
1. Select to connect to the `:mobile` build
|
||||
|
||||
### Use the REPL
|
||||
|
||||
Open any `.cljs` file in the project and evaluate forms in it. See https://calva.io/try-first/ for some starter tips and links. Confirm that your REPL is connected to the app by evaluating:
|
||||
Open any `.cljs` file in the project and evaluate forms in it. See
|
||||
https://calva.io/try-first/ for some starter tips and links. Confirm that your
|
||||
REPL is connected to the app by evaluating:
|
||||
|
||||
```clojure
|
||||
(js/alert "Hello from Status App!")
|
||||
@@ -185,9 +186,35 @@ Open any `.cljs` file in the project and evaluate forms in it. See https://calva
|
||||
|
||||
🎉 Tada! You are ready to use the REPL to improve Status.im! 🎉
|
||||
|
||||
Please consider bookmarking [calva.io](https://calva.io/) for quick access to the Calva documentation.
|
||||
Consider bookmarking [calva.io](https://calva.io/) for quick access to the Calva
|
||||
documentation.
|
||||
|
||||
## Using Emacs/Cider
|
||||
### Auto-format files on save
|
||||
|
||||
We format Clojure code with [zprint](https://github.com/kkinnear/zprint). We
|
||||
recommend configuring VSCode to auto-format Clojure files on every save,
|
||||
otherwise you'll need to constantly rely on the much slower `make lint-fix`
|
||||
command.
|
||||
|
||||
1. Create a file `~/.zprintrc` and enable the `:search-config?` option. This is
|
||||
necessary so the VSCode extension finds the project's `.zprintrc`.
|
||||
|
||||
```bash
|
||||
echo "{:search-config? true}" > ~/.zprintrc
|
||||
```
|
||||
|
||||
2. Install the extension
|
||||
[vscode-clj-zprint](https://github.com/rflagreca/vscode-clj-zprint). By the
|
||||
way, did you know this extension is written in ClojureScript?
|
||||
3. Copy the recommended settings from our repository in
|
||||
[.vscode/settings.example.json](/.vscode/settings.example.json) and merge it
|
||||
with your workspace settings in `.vscode/settings.json`.
|
||||
|
||||
The extension `vscode-clj-zprint` is only available for VSCode. If you are using
|
||||
VSCodium, you'll need to build the `vsix` (extension) yourself (it's easy, only
|
||||
`yarn` and `vsce` are required).
|
||||
|
||||
## Using Emacs with CIDER
|
||||
|
||||
1. Install Emacs/Cider/etc. (there is a lot of variability in how to manage things in emacs, so please google for help with this)
|
||||
2. Add a local ~/.shadow-cljs/config.edn file like below (corresponding to the version numbers of the packages you are using):
|
||||
@@ -198,5 +225,3 @@ Please consider bookmarking [calva.io](https://calva.io/) for quick access to th
|
||||
[cider/cider-nrepl "0.28.4"]
|
||||
[cider/piggieback "0.5.2"]]}
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Patching
|
||||
|
||||
## Libraries
|
||||
If 3rd party library has an issue and fix is not yet released (or we can't switch to a new release), we use forks. Fix should be commited to the fork, tagged and referenced from package.json.
|
||||
If 3rd party library has an issue and fix is not yet released (or we can't switch to a new release), we use forks. Fix should be committed to the fork, tagged and referenced from package.json.
|
||||
|
||||
Example: [`react-native-hole-view`](https://github.com/status-im/react-native-hole-view#refs/tags/v2.1.1-status)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ QA process was focused on account creation and login with keycard, and this bug
|
||||
|
||||
## Resolution process
|
||||
|
||||
- the commit was reverted locally by the developer which fixed the issue localy and confirmed the faulty commit
|
||||
- the commit was reverted locally by the developer which fixed the issue locally and confirmed the faulty commit
|
||||
- the bug was only reproducible on iOS which strongly hinted at a potential issue with native code
|
||||
- the missing method was not the origin of the problem but was found at this point and further analysis lead to the shadowing error
|
||||
|
||||
|
||||
@@ -765,7 +765,7 @@ SPEC CHECKSUMS:
|
||||
RNLanguages: 962e562af0d34ab1958d89bcfdb64fafc37c513e
|
||||
RNPermissions: ad71dd4f767ec254f2cd57592fbee02afee75467
|
||||
RNReactNativeHapticFeedback: 2566b468cc8d0e7bb2f84b23adc0f4614594d071
|
||||
RNReanimated: b2dc21e263319892469d656dc58fc26fb03eadab
|
||||
RNReanimated: 62e43ee6baafb9ba3d3af1857d7fd23a1d41bff0
|
||||
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
|
||||
RNStaticSafeAreaInsets: 055ddbf5e476321720457cdaeec0ff2ba40ec1b8
|
||||
RNSVG: 80584470ff1ffc7994923ea135a3e5ad825546b9
|
||||
@@ -777,6 +777,6 @@ SPEC CHECKSUMS:
|
||||
TouchID: ba4c656d849cceabc2e4eef722dea5e55959ecf4
|
||||
Yoga: d24d6184b6b85f742536bd93bd07d69d7b9bb4c1
|
||||
|
||||
PODFILE CHECKSUM: 6a5f0e6776ce4ef6193032d077947117ace77a87
|
||||
PODFILE CHECKSUM: 84b749023e1c988c133d7a6ec09d125536e4a4fd
|
||||
|
||||
COCOAPODS: 1.12.0
|
||||
|
||||
|
After Width: | Height: | Size: 615 B |
|
After Width: | Height: | Size: 930 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 967 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if pgrep -f 'shadow-cljs watch mobile' > /dev/null; then
|
||||
echo "Error: make run-clojure is already running in another terminal" >&2
|
||||
echo "Please close that terminal before running this command." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if pgrep -f 'react-native start' > /dev/null; then
|
||||
echo "Error: make run-metro is already running in another terminal" >&2
|
||||
echo "Please close that terminal before running this command." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -106,11 +106,9 @@
|
||||
:target :node-test
|
||||
;; Uncomment line below to `make test-watch` a specific file
|
||||
;; :ns-regexp "status-im2.subs.messages-test$"
|
||||
:main
|
||||
status-im.test-runner/main
|
||||
:main status-im.test-runner/main
|
||||
;; set :ui-driven to true to let shadow-cljs inject node-repl
|
||||
:ui-driven
|
||||
true
|
||||
:ui-driven true
|
||||
:closure-defines
|
||||
{status-im2.config/POKT_TOKEN #shadow/env "POKT_TOKEN"
|
||||
status-im2.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN"
|
||||
|
||||
@@ -6,10 +6,3 @@ export function infoLayout(input, isTop) {
|
||||
return isTop ? input.value : -input.value;
|
||||
});
|
||||
}
|
||||
|
||||
export function textSheet(input, isHeight) {
|
||||
return useDerivedValue(function () {
|
||||
'worklet';
|
||||
return isHeight ? input.value : -input.value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
(ns quo.components.colors.color.constants)
|
||||
|
||||
(def ^:const IPHONE_11_PRO_VIEWPORT_WIDTH 375)
|
||||
@@ -1,24 +1,36 @@
|
||||
(ns quo.components.colors.color.style
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]))
|
||||
[quo.foundations.colors :as colors]
|
||||
[utils.responsiveness :as responsiveness]))
|
||||
|
||||
(def color-button-common
|
||||
{:width 48
|
||||
:height 48
|
||||
:border-width 4
|
||||
:border-radius 24
|
||||
:margin-horizontal 4
|
||||
:transform [{:rotate "45deg"}]
|
||||
:border-color :transparent})
|
||||
(defn color-button-common
|
||||
[window-width]
|
||||
{:width 48
|
||||
:height 48
|
||||
:border-width 4
|
||||
:border-radius 24
|
||||
:margin-right (-> window-width
|
||||
(- responsiveness/IPHONE_11_PRO_VIEWPORT_WIDTH) ;We want the design to be 100%
|
||||
;identical to Figma on iPhone 11
|
||||
;Pro, So we're using it's VW here.
|
||||
(/ 6) ;Dividing by an appropriate factor to get a reasonable value for each VW
|
||||
;on other devices (This will yield 0 on iPhone 11 Pro, Which is what we
|
||||
;want)
|
||||
(+ 8)) ;Add same margin that's on Figma for 11 Pro, This value will increase
|
||||
;based on the device VW because of the division above.
|
||||
:transform [{:rotate "45deg"}]
|
||||
:border-color :transparent})
|
||||
|
||||
(defn color-button
|
||||
[color selected?]
|
||||
(merge color-button-common
|
||||
(when selected?
|
||||
{:border-top-color (colors/alpha color 0.4)
|
||||
:border-end-color (colors/alpha color 0.4)
|
||||
:border-bottom-color (colors/alpha color 0.2)
|
||||
:border-start-color (colors/alpha color 0.2)})))
|
||||
([color selected?]
|
||||
(color-button color selected? nil nil))
|
||||
([color selected? idx window-width]
|
||||
(cond-> (color-button-common window-width)
|
||||
selected? (assoc :border-top-color (colors/alpha color 0.4)
|
||||
:border-end-color (colors/alpha color 0.4)
|
||||
:border-bottom-color (colors/alpha color 0.2)
|
||||
:border-start-color (colors/alpha color 0.2))
|
||||
(zero? idx) (assoc :margin-left -4))))
|
||||
|
||||
(defn color-circle
|
||||
[color border?]
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
selected?
|
||||
on-press
|
||||
blur?
|
||||
theme]
|
||||
theme
|
||||
idx
|
||||
window-width]
|
||||
:as props}]
|
||||
(let [border? (and (not blur?) (not selected?))
|
||||
hex-color (if (= :feng-shui color)
|
||||
@@ -29,7 +31,7 @@
|
||||
theme))]
|
||||
|
||||
[rn/pressable
|
||||
{:style (style/color-button hex-color selected?)
|
||||
{:style (style/color-button hex-color selected? idx window-width)
|
||||
:accessibility-label :color-picker-item
|
||||
:on-press #(on-press color)}
|
||||
(if (and (= :feng-shui color) (not selected?))
|
||||
|
||||
@@ -18,19 +18,22 @@
|
||||
- `on-change` Callback called when a color is selected `(fn [color-name])`.
|
||||
- `blur?` Boolean to enable blur background support.}"
|
||||
[{:keys [default-selected]}]
|
||||
(let [selected (reagent/atom default-selected)]
|
||||
(let [selected (reagent/atom default-selected)
|
||||
{window-width :width} (rn/get-window)]
|
||||
(fn [{:keys [blur? on-change feng-shui? container-style]}]
|
||||
[rn/scroll-view
|
||||
{:horizontal true
|
||||
:shows-horizontal-scroll-indicator false
|
||||
:content-container-style container-style}
|
||||
(doall (map (fn [color]
|
||||
[color/view
|
||||
{:selected? (= color @selected)
|
||||
:on-press #(on-change-handler selected % on-change)
|
||||
:blur? blur?
|
||||
:key color
|
||||
:color color}])
|
||||
;; TODO: using :feng-shui? temporarily while b & w is being developed.
|
||||
;; https://github.com/status-im/status-mobile/discussions/16676
|
||||
(if feng-shui? (conj color-list :feng-shui) color-list)))])))
|
||||
(doall (map-indexed (fn [idx color]
|
||||
[color/view
|
||||
{:selected? (= color @selected)
|
||||
:on-press #(on-change-handler selected % on-change)
|
||||
:blur? blur?
|
||||
:key color
|
||||
:color color
|
||||
:idx idx
|
||||
:window-width window-width}])
|
||||
;; TODO: using :feng-shui? temporarily while b & w is being developed.
|
||||
;; https://github.com/status-im/status-mobile/discussions/16676
|
||||
(if feng-shui? (conj color-list :feng-shui) color-list)))])))
|
||||
|
||||
@@ -18,13 +18,19 @@
|
||||
(h/render [bottom-actions/view
|
||||
{:actions :2-actions
|
||||
:button-one-label button-one
|
||||
:button-one-props {:icon-left :i/arrow-left}
|
||||
:button-two-label button-two}])
|
||||
(h/is-truthy (h/get-by-text button-one))
|
||||
(h/is-truthy (h/get-by-label-text :icon))
|
||||
(h/is-truthy (h/get-by-text button-two))))
|
||||
|
||||
(h/test "render disabled button"
|
||||
(h/render [bottom-actions/view
|
||||
{:description "Sample description"
|
||||
:disabled? true
|
||||
:button-one-props {:disabled? true}
|
||||
:button-one-label "button"}])
|
||||
(h/is-disabled (h/get-by-label-text :button-one))))
|
||||
(h/is-disabled (h/get-by-label-text :button-one)))
|
||||
|
||||
(h/test "sane defaults"
|
||||
(h/render [bottom-actions/view {}])
|
||||
(h/is-truthy (h/get-by-label-text :button-one))))
|
||||
|
||||
@@ -17,34 +17,35 @@
|
||||
:description - string (default nil) - Description to display below the title
|
||||
:button-one-label - string (default nil) - Label for the first button
|
||||
:button-two-label - string (default nil) - Label for the second button
|
||||
:button-one-press - fn (default nil) - Function to call when the first button is pressed
|
||||
:button-two-press - fn (default nil) - Function to call when the second button is pressed
|
||||
:button-one-props - map with props for button one
|
||||
:button-two-props - map with props for button two
|
||||
:theme - :light/:dark
|
||||
:scroll - bool (default false) - Whether the iOS Home Indicator should be rendered
|
||||
:button-one-type - same as button/button :type
|
||||
:button-two-type - same as button/button :type"
|
||||
:scroll? - bool (default false) - Whether the iOS Home Indicator should be rendered"
|
||||
[props]
|
||||
(let [{:keys [actions description button-one-label button-two-label
|
||||
button-one-press button-two-press theme
|
||||
scroll? button-one-type button-two-type disabled?]}
|
||||
button-one-props button-two-props theme scroll?]}
|
||||
(merge default-props props)]
|
||||
[:<>
|
||||
[rn/view {:style style/buttons-container}
|
||||
(when (= actions :2-actions)
|
||||
[button/button
|
||||
{:size 40
|
||||
:background (when scroll? :blur)
|
||||
:container-style style/button-container-2-actions
|
||||
:type button-two-type
|
||||
:on-press button-two-press} button-two-label])
|
||||
(merge
|
||||
{:size 40
|
||||
:background (when scroll? :blur)
|
||||
:container-style style/button-container-2-actions
|
||||
:theme theme
|
||||
:accessibility-label :button-two}
|
||||
button-two-props)
|
||||
button-two-label])
|
||||
[button/button
|
||||
{:size 40
|
||||
:container-style style/button-container
|
||||
:type button-one-type
|
||||
:disabled? disabled?
|
||||
:background (when scroll? :blur)
|
||||
:on-press button-one-press
|
||||
:accessibility-label :button-one} button-one-label]]
|
||||
(merge
|
||||
{:size 40
|
||||
:container-style style/button-container
|
||||
:background (when scroll? :blur)
|
||||
:theme theme
|
||||
:accessibility-label :button-one}
|
||||
button-one-props)
|
||||
button-one-label]]
|
||||
(when description
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{:title "Title"
|
||||
:type :account
|
||||
:account-avatar-emoji "🍿"
|
||||
:networks [{:name :ethereum :short :eth}]
|
||||
:networks [{:name :ethereum :short-name "eth"}]
|
||||
:description "0x62b...0a5"
|
||||
:customization-color :purple}])
|
||||
(h/is-truthy (h/get-by-text "Title"))
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[quo.components.icon :as icons]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.components.tags.context-tag.view :as context-tag]
|
||||
[quo.components.wallet.address-text.view :as address-text]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
@@ -32,14 +33,6 @@
|
||||
:profile-picture profile-picture}]
|
||||
nil))
|
||||
|
||||
(defn- network-view
|
||||
[network]
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :regular
|
||||
:style (style/network-text-color (:name network))}
|
||||
(str (name (:short network)) ":")])
|
||||
|
||||
(defn- keypair-subtitle
|
||||
[{:keys [theme blur? keycard?]}]
|
||||
[rn/view {:style style/row}
|
||||
@@ -59,15 +52,16 @@
|
||||
|
||||
(defn- account-subtitle
|
||||
[{:keys [networks theme blur? description]}]
|
||||
[rn/view {:style style/row}
|
||||
(for [network networks]
|
||||
^{:key (str network)}
|
||||
[network-view network])
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :regular
|
||||
:style (style/description theme blur?)}
|
||||
description]])
|
||||
(if networks
|
||||
[address-text/view
|
||||
{:networks networks
|
||||
:address description
|
||||
:format :short}]
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :regular
|
||||
:style (style/description theme blur?)}
|
||||
description]))
|
||||
|
||||
(defn- default-keypair-subtitle
|
||||
[{:keys [description theme blur?]}]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
(ns quo.components.dropdowns.dropdown-input.view
|
||||
(:require
|
||||
[quo.components.dropdowns.dropdown.properties :as properties]
|
||||
[quo.components.dropdowns.dropdown.style :as style]
|
||||
[quo.components.icon :as icon]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.foundations.customization-colors :as customization-colors]
|
||||
[quo.theme :as theme]
|
||||
[react-native.blur :as blur]
|
||||
[react-native.core :as rn]))
|
||||
|
||||
(defn view-internal
|
||||
[]
|
||||
[rn/pressable
|
||||
{:on-press nil}])
|
||||
|
||||
(def view
|
||||
"Props:
|
||||
- :on-press - function to call when pressed"
|
||||
(theme/with-theme view-internal))
|
||||
@@ -150,7 +150,7 @@
|
||||
:auto-complete :password
|
||||
:secure-text-entry (not @password-shown?)
|
||||
:right-icon {:style-fn style/password-icon
|
||||
:icon-name (if @password-shown? :i/hide :i/reveal)
|
||||
:icon-name (if @password-shown? :i/hide-password :i/reveal)
|
||||
:on-press #(swap! password-shown? not)})])))
|
||||
|
||||
(defn input-internal
|
||||
|
||||
@@ -4,19 +4,12 @@
|
||||
[quo.components.icon :as icon]
|
||||
[quo.components.list-items.account.style :as style]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.components.wallet.address-text.view :as address-text]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]))
|
||||
|
||||
(defn- network-view
|
||||
[network]
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :regular
|
||||
:style {:color (colors/custom-color (:name network))}}
|
||||
(str (name (:short network)) ":")])
|
||||
|
||||
(defn- account-view
|
||||
[{:keys [account-props title-icon? blur? theme]
|
||||
:or {title-icon? false}}]
|
||||
@@ -38,15 +31,10 @@
|
||||
:color (if blur?
|
||||
colors/white-opa-40
|
||||
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}]])]
|
||||
[text/text {:size :paragraph-2}
|
||||
(for [network (:networks account-props)]
|
||||
^{:key (str network)}
|
||||
[network-view network])
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :monospace
|
||||
:style (style/account-address blur? theme)}
|
||||
(:address account-props)]]]])
|
||||
[address-text/view
|
||||
{:networks (:networks account-props)
|
||||
:address (:address account-props)
|
||||
:format :short}]]])
|
||||
|
||||
(defn- balance-view
|
||||
[{:keys [balance-props type theme]}]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[quo.components.icon :as icon]
|
||||
[quo.components.list-items.account-list-card.style :as style]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.components.wallet.address-text.view :as address-text]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
@@ -26,20 +27,10 @@
|
||||
{:weight :semi-bold
|
||||
:size :paragraph-2}
|
||||
(:name account-props)]
|
||||
[text/text {:size :paragraph-2}
|
||||
(map (fn [network]
|
||||
^{:key (str network)}
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:style {:color (colors/resolve-color network theme)}}
|
||||
(str (subs (name network) 0 3) ":")])
|
||||
networks)
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:style {:color (if blur?
|
||||
colors/white-opa-40
|
||||
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}}
|
||||
(:address account-props)]]]]
|
||||
[address-text/view
|
||||
{:networks networks
|
||||
:address (:address account-props)
|
||||
:format :short}]]]
|
||||
(when (= action :icon)
|
||||
[rn/pressable {:on-press on-options-press}
|
||||
[icon/icon :i/options
|
||||
|
||||
@@ -64,11 +64,11 @@
|
||||
(defn- view-internal
|
||||
"[preview-list opts items]
|
||||
opts
|
||||
{:type :user/:communities/:accounts/:tokens/:collectibles/:dapps/:network
|
||||
:size :size-32 | :size-24 | :size-20 | :size-16 | :size-14
|
||||
:number number of items in the list (optional)
|
||||
:blur? overflow-label blur?}
|
||||
items preview list items (only 4 items is required for preview)
|
||||
{:type :user/:communities/:accounts/:tokens/:collectibles/:dapps/:network
|
||||
:size :size-32 | :size-24 | :size-20 | :size-16 | :size-14
|
||||
:number number of items in the list (optional)
|
||||
:blur? overflow-label blur?}
|
||||
items preview list items (only 4 items is required for preview)
|
||||
"
|
||||
[{:keys [type size number blur?]} items]
|
||||
(let [size-key (if (contains? properties/sizes size) size :size-24)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
(ns quo.components.list-items.token-network.component-spec
|
||||
(:require [quo.components.list-items.token-network.view :as token-network]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.foundations.resources :as resources]
|
||||
[test-helpers.component :as h]))
|
||||
|
||||
(def props
|
||||
{:token (resources/get-token :snt)
|
||||
:state :default
|
||||
:label "Status"
|
||||
:networks [(resources/get-network :ethereum)]
|
||||
:token-value "100.00 SNT"
|
||||
:fiat-value "€0.00"})
|
||||
|
||||
(h/describe "List items: Token network"
|
||||
(h/test "default state"
|
||||
(h/render [token-network/view (dissoc props :state)])
|
||||
(h/is-truthy (h/get-by-text "Status")))
|
||||
|
||||
(h/test "default state explicit"
|
||||
(h/render [token-network/view props])
|
||||
(h/is-truthy (h/get-by-text "Status")))
|
||||
|
||||
(h/test "Pressed state"
|
||||
(h/render [token-network/view props])
|
||||
(h/fire-event :on-press-in (h/get-by-label-text :token-network))
|
||||
(h/wait-for #(h/has-style (h/query-by-label-text :token-network)
|
||||
{:backgroundColor (colors/resolve-color :blue :light 5)})))
|
||||
|
||||
(h/test "Active state"
|
||||
(h/render [token-network/view (assoc props :state :active)])
|
||||
(h/has-style (h/query-by-label-text :token-network)
|
||||
{:backgroundColor (colors/resolve-color :blue :light 10)}))
|
||||
|
||||
(h/test "Selected state"
|
||||
(h/render [token-network/view (assoc props :state :selected)])
|
||||
(h/is-truthy (h/query-by-label-text :check-icon)))
|
||||
|
||||
(h/test "Call on-press"
|
||||
(let [on-press (h/mock-fn)]
|
||||
(h/render [token-network/view (assoc props :on-press on-press)])
|
||||
(h/fire-event :on-press (h/get-by-label-text :token-network))
|
||||
(h/was-called on-press)))
|
||||
|
||||
(h/test "Empty props"
|
||||
(h/render [token-network/view {}])
|
||||
(h/is-truthy (h/get-by-label-text :token-network))))
|
||||
@@ -0,0 +1,47 @@
|
||||
(ns quo.components.list-items.token-network.style
|
||||
(:require [quo.foundations.colors :as colors]))
|
||||
|
||||
(defn- background-color
|
||||
[state customization-color theme]
|
||||
(cond
|
||||
(= state :pressed) (colors/resolve-color customization-color theme 5)
|
||||
(= state :active) (colors/resolve-color customization-color theme 10)
|
||||
(= state :selected) (colors/resolve-color customization-color theme 5)
|
||||
:else :transparent))
|
||||
|
||||
(defn container
|
||||
[state customization-color theme]
|
||||
{:flex-direction :row
|
||||
:justify-content :space-between
|
||||
:align-items :center
|
||||
:padding-horizontal 12
|
||||
:padding-vertical 8
|
||||
:border-radius 12
|
||||
:height 56
|
||||
:background-color (background-color state customization-color theme)})
|
||||
|
||||
(defn check-color
|
||||
[customization-color theme]
|
||||
(colors/resolve-color customization-color theme))
|
||||
|
||||
(def info
|
||||
{:flex-direction :row
|
||||
:align-items :center})
|
||||
|
||||
(def token-info
|
||||
{:height 40})
|
||||
|
||||
(def token-image
|
||||
{:width 32
|
||||
:height 32
|
||||
:border-width 1
|
||||
:border-radius 16
|
||||
:border-color colors/neutral-80-opa-5
|
||||
:margin-right 8})
|
||||
|
||||
(def values-container
|
||||
{:align-items :flex-end})
|
||||
|
||||
(defn fiat-value
|
||||
[theme]
|
||||
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)})
|
||||
@@ -0,0 +1,62 @@
|
||||
(ns quo.components.list-items.token-network.view
|
||||
(:require
|
||||
[quo.components.icon :as icon]
|
||||
[quo.components.list-items.preview-list.view :as preview-list]
|
||||
[quo.components.list-items.token-network.style :as style]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]))
|
||||
|
||||
(defn- info
|
||||
[{:keys [token label networks]}]
|
||||
[rn/view {:style style/info}
|
||||
[rn/image
|
||||
{:source (or (:source token) token)
|
||||
:style style/token-image}]
|
||||
[rn/view {:style style/token-info}
|
||||
[text/text {:weight :semi-bold} label]
|
||||
[preview-list/view
|
||||
{:type :network
|
||||
:size :size-14
|
||||
:number 3}
|
||||
networks]]])
|
||||
|
||||
(defn- values
|
||||
[{:keys [state token-value fiat-value customization-color theme]}]
|
||||
(if (= state :selected)
|
||||
[icon/icon :i/check
|
||||
{:color (style/check-color customization-color theme)
|
||||
:accessibility-label :check-icon}]
|
||||
[rn/view {:style style/values-container}
|
||||
[text/text
|
||||
{:weight :medium
|
||||
:size :paragraph-2}
|
||||
token-value]
|
||||
[text/text
|
||||
{:style (style/fiat-value theme)
|
||||
:size :paragraph-2}
|
||||
fiat-value]]))
|
||||
|
||||
(defn- view-internal
|
||||
[]
|
||||
(let [pressed? (reagent/atom false)
|
||||
on-press-in #(reset! pressed? true)
|
||||
on-press-out #(reset! pressed? false)]
|
||||
(fn [{:keys [on-press state customization-color
|
||||
_token _networks _token-value _fiat-value theme]
|
||||
:as props
|
||||
:or {customization-color :blue}}]
|
||||
(let [internal-state (if @pressed?
|
||||
:pressed
|
||||
state)]
|
||||
[rn/pressable
|
||||
{:style (style/container internal-state customization-color theme)
|
||||
:on-press-in on-press-in
|
||||
:on-press-out on-press-out
|
||||
:on-press on-press
|
||||
:accessibility-label :token-network}
|
||||
[info props]
|
||||
[values props]]))))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
@@ -14,7 +14,7 @@
|
||||
(defn- internal-view
|
||||
[]
|
||||
(let [state (reagent/atom :default)]
|
||||
(fn [{:keys [theme customization-color status token metrics? values on-press]}]
|
||||
(fn [{:keys [theme customization-color status token metrics? values on-press on-long-press]}]
|
||||
(let [bg-opacity (case @state
|
||||
:active 10
|
||||
:pressed 5
|
||||
@@ -28,6 +28,7 @@
|
||||
(reset! state :active)
|
||||
(js/setTimeout #(reset! state :default) 300)
|
||||
on-press)
|
||||
:on-long-press on-long-press
|
||||
:accessibility-label :container}
|
||||
[rn/view
|
||||
{:style {:flex-direction :row
|
||||
|
||||
@@ -1,34 +1,119 @@
|
||||
(ns quo.components.share.share-qr-code.component-spec
|
||||
(:require
|
||||
[quo.components.share.share-qr-code.view :as share-qr-code]
|
||||
[test-helpers.component :as h]))
|
||||
(:require [quo.components.share.share-qr-code.view :as share-qr-code]
|
||||
[test-helpers.component :as h]))
|
||||
|
||||
(defn render-share-qr-code
|
||||
[{share-qr-type :type :as props}]
|
||||
(let [component-rendered (h/render [share-qr-code/view {:type share-qr-type}])
|
||||
rerender-fn (h/get-rerender-fn component-rendered)
|
||||
share-qr-code (h/get-by-label-text :share-qr-code)]
|
||||
;; Fires on-layout since it's needed to render the content
|
||||
(h/fire-event :layout share-qr-code #js {:nativeEvent #js {:layout #js {:width 500}}})
|
||||
(rerender-fn [share-qr-code/view props])))
|
||||
|
||||
(h/describe "Share QR Code component"
|
||||
(h/test "renders share qr code component"
|
||||
(h/render [share-qr-code/view
|
||||
{:link-title " A test title"}])
|
||||
(-> (js/expect (h/get-by-text "A test title"))
|
||||
(.toBeTruthy)))
|
||||
(h/describe "Renders share-qr-code component in all types"
|
||||
(let [qr-label "Text shown below QR"]
|
||||
(h/test "Profile"
|
||||
(render-share-qr-code {:type :profile
|
||||
:qr-data qr-label})
|
||||
(h/is-truthy (h/get-by-text qr-label)))
|
||||
|
||||
(h/test "renders share qr code url"
|
||||
(h/render [share-qr-code/view
|
||||
{:qr-url " A test url"}])
|
||||
(-> (js/expect (h/get-by-text "A test url"))
|
||||
(.toBeTruthy)))
|
||||
(h/test "Wallet Legacy"
|
||||
(render-share-qr-code {:type :wallet-legacy
|
||||
:qr-data qr-label
|
||||
:emoji "👻"})
|
||||
(h/is-truthy (h/get-by-text qr-label)))
|
||||
|
||||
(h/test "on press link event fires"
|
||||
(let [event (h/mock-fn)]
|
||||
(h/render [share-qr-code/view
|
||||
{:url-on-press event
|
||||
:qr-url " A test url"}])
|
||||
(h/fire-event :press (h/get-by-text "A test url"))
|
||||
(-> (js/expect event)
|
||||
(.toHaveBeenCalledTimes 1))))
|
||||
(h/test "Wallet Multichain"
|
||||
(render-share-qr-code {:type :wallet-multichain
|
||||
:qr-data qr-label
|
||||
:emoji "👻"})
|
||||
(h/is-truthy (h/get-by-text qr-label)))))
|
||||
|
||||
(h/test "on press share event fires"
|
||||
(let [event (h/mock-fn)]
|
||||
(h/render [share-qr-code/view
|
||||
{:share-on-press event}])
|
||||
(h/fire-event :press (h/get-by-label-text :share-profile))
|
||||
(-> (js/expect event)
|
||||
(.toHaveBeenCalledTimes 1)))))
|
||||
(h/describe "Fires all events for all types"
|
||||
(letfn [(test-fire-events [props test-seq]
|
||||
(doseq [{:keys [test-name event-name
|
||||
callback-prop-key
|
||||
accessibility-label]} test-seq
|
||||
:let [event-fn (h/mock-fn)]]
|
||||
(h/test test-name
|
||||
(render-share-qr-code (assoc props callback-prop-key event-fn))
|
||||
(h/fire-event event-name (h/get-by-label-text accessibility-label))
|
||||
(h/was-called-times event-fn 1))))]
|
||||
|
||||
(h/describe "Profile"
|
||||
(test-fire-events
|
||||
{:type :profile}
|
||||
[{:test-name "Text pressed"
|
||||
:accessibility-label :share-qr-code-info-text
|
||||
:event-name :press
|
||||
:callback-prop-key :on-text-press}
|
||||
{:test-name "Text long pressed"
|
||||
:accessibility-label :share-qr-code-info-text
|
||||
:event-name :long-press
|
||||
:callback-prop-key :on-text-long-press}
|
||||
{:test-name "Share button"
|
||||
:accessibility-label :link-to-profile
|
||||
:event-name :press
|
||||
:callback-prop-key :on-share-press}]))
|
||||
|
||||
(h/describe "Wallet Legacy"
|
||||
(test-fire-events
|
||||
{:type :wallet-legacy :emoji "👽"}
|
||||
[{:test-name "Text pressed"
|
||||
:accessibility-label :share-qr-code-info-text
|
||||
:event-name :press
|
||||
:callback-prop-key :on-text-press}
|
||||
{:test-name "Text long pressed"
|
||||
:accessibility-label :share-qr-code-info-text
|
||||
:event-name :long-press
|
||||
:callback-prop-key :on-text-long-press}
|
||||
{:test-name "Share button pressed"
|
||||
:accessibility-label :link-to-profile
|
||||
:event-name :press
|
||||
:callback-prop-key :on-share-press}
|
||||
{:test-name "Info icon pressed"
|
||||
:accessibility-label :share-qr-code-info-icon
|
||||
:event-name :press
|
||||
:callback-prop-key :on-info-press}
|
||||
{:test-name "Legacy tab pressed"
|
||||
:accessibility-label :share-qr-code-legacy-tab
|
||||
:event-name :press
|
||||
:callback-prop-key :on-legacy-press}
|
||||
{:test-name "Multichain tab pressed"
|
||||
:accessibility-label :share-qr-code-multichain-tab
|
||||
:event-name :press
|
||||
:callback-prop-key :on-multichain-press}]))
|
||||
|
||||
(h/describe "Wallet Multichain"
|
||||
(test-fire-events
|
||||
{:type :wallet-multichain :emoji "👽"}
|
||||
[{:test-name "Text pressed"
|
||||
:accessibility-label :share-qr-code-info-text
|
||||
:event-name :press
|
||||
:callback-prop-key :on-text-press}
|
||||
{:test-name "Text long pressed"
|
||||
:accessibility-label :share-qr-code-info-text
|
||||
:event-name :long-press
|
||||
:callback-prop-key :on-text-long-press}
|
||||
{:test-name "Share button pressed"
|
||||
:accessibility-label :link-to-profile
|
||||
:event-name :press
|
||||
:callback-prop-key :on-share-press}
|
||||
{:test-name "Info icon pressed"
|
||||
:accessibility-label :share-qr-code-info-icon
|
||||
:event-name :press
|
||||
:callback-prop-key :on-info-press}
|
||||
{:test-name "Legacy tab pressed"
|
||||
:accessibility-label :share-qr-code-legacy-tab
|
||||
:event-name :press
|
||||
:callback-prop-key :on-legacy-press}
|
||||
{:test-name "Multichain tab pressed"
|
||||
:accessibility-label :share-qr-code-multichain-tab
|
||||
:event-name :press
|
||||
:callback-prop-key :on-multichain-press}
|
||||
{:test-name "Settings pressed"
|
||||
:accessibility-label :share-qr-code-settings
|
||||
:event-name :press
|
||||
:callback-prop-key :on-settings-press}])))))
|
||||
|
||||
@@ -1,45 +1,116 @@
|
||||
(ns quo.components.share.share-qr-code.style
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]))
|
||||
(:require [quo.foundations.colors :as colors]))
|
||||
|
||||
(def qr-code-container
|
||||
{:padding-top 12
|
||||
:padding-horizontal 12
|
||||
:padding-bottom 8
|
||||
:border-radius 16
|
||||
:background-color colors/white-opa-5
|
||||
:flex-direction :column
|
||||
:justify-content :center
|
||||
:align-items :center})
|
||||
(def outer-container
|
||||
{:border-radius 16
|
||||
:width "100%"
|
||||
:overflow :hidden})
|
||||
|
||||
(def profile-address-column
|
||||
{:margin-horizontal :auto
|
||||
:flex 4})
|
||||
(def overlay-color colors/white-opa-5)
|
||||
|
||||
(def profile-address-container
|
||||
(def ^:private padding-horizontal 12)
|
||||
|
||||
(def content-container
|
||||
{:z-index 1
|
||||
:padding-horizontal padding-horizontal
|
||||
:padding-top 12
|
||||
:padding-bottom 8})
|
||||
|
||||
;;; Header
|
||||
(def header-container
|
||||
{:flex-direction :row
|
||||
:margin-bottom 12})
|
||||
|
||||
(def header-tab-active {:background-color colors/white-opa-20})
|
||||
(def header-tab-inactive {:background-color colors/white-opa-5})
|
||||
(def space-between-tabs {:width 8})
|
||||
|
||||
(def info-icon
|
||||
{:margin-left :auto
|
||||
:align-self :center})
|
||||
|
||||
(def info-icon-color colors/white-opa-40)
|
||||
|
||||
;;; QR code
|
||||
(defn qr-code-size
|
||||
[total-width]
|
||||
(- total-width (* 2 padding-horizontal)))
|
||||
|
||||
;;; Bottom part
|
||||
(def bottom-container
|
||||
{:margin-top 8
|
||||
:flex-direction :row
|
||||
:justify-content :space-between})
|
||||
|
||||
(def title {:color colors/white-opa-40})
|
||||
|
||||
(def share-button-size 32)
|
||||
(def ^:private share-button-gap 16)
|
||||
|
||||
(defn share-button-container
|
||||
[alignment]
|
||||
{:justify-content (if (= alignment :top) :flex-start :center)
|
||||
:margin-left share-button-gap})
|
||||
|
||||
(defn data-text
|
||||
[total-width]
|
||||
{:width (- total-width (* 2 padding-horizontal) share-button-size share-button-gap)})
|
||||
|
||||
;;; Wallet variants
|
||||
(def wallet-data-and-share-container
|
||||
{:margin-top 2
|
||||
:flex-direction :row
|
||||
:justify-content :space-between})
|
||||
|
||||
(def wallet-legacy-container {:flex 1})
|
||||
|
||||
(def wallet-multichain-container {:flex 1 :margin-top 4})
|
||||
|
||||
(def wallet-multichain-networks
|
||||
{:flex-direction :row
|
||||
:justify-content :space-between
|
||||
:margin-top 6})
|
||||
:margin-bottom 8})
|
||||
|
||||
(def profile-address-content-container
|
||||
{:padding-top 2
|
||||
:align-self :flex-start})
|
||||
(def wallet-multichain-data-container {:margin-top 4})
|
||||
|
||||
(def profile-address-content
|
||||
{:color colors/white})
|
||||
;;; Dashed line
|
||||
(def divider-container
|
||||
{:height 8
|
||||
:margin-horizontal 4
|
||||
:justify-content :center
|
||||
:overflow :hidden})
|
||||
|
||||
(def profile-address-label
|
||||
{:color colors/white-opa-40})
|
||||
(def ^:private padding-for-divider (+ padding-horizontal 4))
|
||||
(def ^:private dashed-line-width 2)
|
||||
(def ^:private dashed-line-space 4)
|
||||
|
||||
(def share-button-container
|
||||
{:flex 1
|
||||
:flex-direction :column
|
||||
:justify-content :center
|
||||
:align-items :flex-end})
|
||||
(def dashed-line
|
||||
{:flex-direction :row
|
||||
:margin-left -1})
|
||||
|
||||
(def icon-container
|
||||
{:height 36
|
||||
:flex-direction :row
|
||||
:align-items :center
|
||||
:justify-content :space-between
|
||||
:padding-bottom 12})
|
||||
(def line
|
||||
{:background-color colors/white-opa-20
|
||||
:width dashed-line-width
|
||||
:height 1})
|
||||
|
||||
(def line-space
|
||||
{:width dashed-line-space
|
||||
:height 1})
|
||||
|
||||
(defn number-lines-and-spaces-to-fill
|
||||
[component-width]
|
||||
(let [line-and-space-width (+ dashed-line-width dashed-line-space)
|
||||
width-to-fill (- component-width (* 2 padding-for-divider))
|
||||
number-of-lines (* (/ width-to-fill line-and-space-width) 2)]
|
||||
(inc (int number-of-lines))))
|
||||
|
||||
(def ^:private get-network-full-name
|
||||
{"eth" :ethereum
|
||||
"opt" :optimism
|
||||
"arb1" :arbitrum})
|
||||
|
||||
(defn network-short-name-text
|
||||
[network-short-name]
|
||||
{:color (-> network-short-name
|
||||
(get-network-full-name :unknown)
|
||||
(colors/resolve-color nil))})
|
||||
|
||||
@@ -1,46 +1,249 @@
|
||||
(ns quo.components.share.share-qr-code.view
|
||||
(:require
|
||||
[quo.components.buttons.button.view :as button]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.components.share.qr-code.view :as qr-code]
|
||||
[quo.components.share.share-qr-code.style :as style]
|
||||
[quo.foundations.colors :as colors]
|
||||
[react-native.blur :as blur]
|
||||
[react-native.core :as rn]))
|
||||
(:require [clojure.set]
|
||||
[clojure.string :as string]
|
||||
[oops.core :as oops]
|
||||
[quo.components.buttons.button.view :as button]
|
||||
[quo.components.icon :as icon]
|
||||
[quo.components.list-items.preview-list.view :as preview-list]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.components.share.qr-code.view :as qr-code]
|
||||
[quo.components.share.share-qr-code.style :as style]
|
||||
[quo.components.tabs.tab.view :as tab]
|
||||
[quo.foundations.resources :as quo.resources]
|
||||
[quo.theme]
|
||||
[react-native.blur :as blur]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[reagent.core :as reagent]
|
||||
[utils.i18n :as i18n]))
|
||||
|
||||
(defn- line [] [rn/view {:style style/line}])
|
||||
(defn- space [] [rn/view {:style style/line-space}])
|
||||
|
||||
(defn- dashed-line
|
||||
[width]
|
||||
(into [rn/view {:style style/dashed-line}]
|
||||
(take (style/number-lines-and-spaces-to-fill width))
|
||||
(cycle [[line] [space]])))
|
||||
|
||||
(defn- header
|
||||
[{:keys [share-qr-type on-info-press on-legacy-press on-multichain-press]}]
|
||||
[rn/view {:style style/header-container}
|
||||
[tab/view
|
||||
{:accessibility-label :share-qr-code-legacy-tab
|
||||
:id :wallet-legacy-tab
|
||||
:active-item-container-style style/header-tab-active
|
||||
:item-container-style style/header-tab-inactive
|
||||
:size 24
|
||||
:active (= :wallet-legacy share-qr-type)
|
||||
:on-press on-legacy-press}
|
||||
(i18n/label :t/legacy)]
|
||||
[rn/view {:style style/space-between-tabs}]
|
||||
[tab/view
|
||||
{:accessibility-label :share-qr-code-multichain-tab
|
||||
:id :wallet-multichain-tab
|
||||
:active-item-container-style style/header-tab-active
|
||||
:item-container-style style/header-tab-inactive
|
||||
:size 24
|
||||
:active (= :wallet-multichain share-qr-type)
|
||||
:on-press on-multichain-press}
|
||||
(i18n/label :t/multichain)]
|
||||
[rn/pressable
|
||||
{:accessibility-label :share-qr-code-info-icon
|
||||
:style style/info-icon
|
||||
:on-press on-info-press
|
||||
:hit-slop 6}
|
||||
[icon/icon :i/info
|
||||
{:size 20
|
||||
:color style/info-icon-color}]]])
|
||||
|
||||
(defn- info-label
|
||||
[share-qr-code-type]
|
||||
[text/text {:size :paragraph-2 :weight :medium :style style/title}
|
||||
(if (= share-qr-code-type :profile)
|
||||
(i18n/label :t/link-to-profile)
|
||||
(i18n/label :t/wallet-address))])
|
||||
|
||||
(defn- info-text
|
||||
[{:keys [width on-press on-long-press ellipsize?]} qr-data-text]
|
||||
[rn/pressable
|
||||
{:accessibility-label :share-qr-code-info-text
|
||||
:style (style/data-text width)
|
||||
:on-press on-press
|
||||
:on-long-press on-long-press}
|
||||
[text/text
|
||||
(cond-> {:size :paragraph-1
|
||||
:weight :monospace}
|
||||
ellipsize? (assoc :number-of-lines 1
|
||||
:ellipsize-mode :middle))
|
||||
qr-data-text]])
|
||||
|
||||
(defn- share-button
|
||||
[{:keys [alignment on-press]}]
|
||||
[rn/view {:style (style/share-button-container alignment)}
|
||||
[button/button
|
||||
{:icon-only? true
|
||||
:type :grey
|
||||
:background :blur
|
||||
:size style/share-button-size
|
||||
:accessibility-label :link-to-profile
|
||||
:on-press on-press}
|
||||
:i/share]])
|
||||
|
||||
(defn- network-colored-text
|
||||
[network-short-name]
|
||||
[text/text {:style (style/network-short-name-text network-short-name)}
|
||||
(str network-short-name ":")])
|
||||
|
||||
(defn- wallet-multichain-colored-address
|
||||
[full-address]
|
||||
(let [[networks address] (as-> full-address $
|
||||
(string/split $ ":")
|
||||
[(butlast $) (last $)])
|
||||
->network-hiccup-xf (map #(vector network-colored-text %))]
|
||||
(as-> networks $
|
||||
(into [:<>] ->network-hiccup-xf $)
|
||||
(conj $ address))))
|
||||
|
||||
(defn- profile-bottom
|
||||
[{:keys [component-width qr-data on-text-press on-text-long-press on-share-press share-qr-type]}]
|
||||
[:<>
|
||||
[rn/view
|
||||
[info-label share-qr-type]
|
||||
[info-text
|
||||
{:width component-width
|
||||
:ellipsize? true
|
||||
:on-press on-text-press
|
||||
:on-long-press on-text-long-press}
|
||||
qr-data]]
|
||||
[share-button
|
||||
{:alignment :center
|
||||
:on-press on-share-press}]])
|
||||
|
||||
(defn- wallet-legacy-bottom
|
||||
[{:keys [share-qr-type component-width qr-data on-text-press on-text-long-press on-share-press]}]
|
||||
[rn/view {:style style/wallet-legacy-container}
|
||||
[info-label share-qr-type]
|
||||
[rn/view {:style style/wallet-data-and-share-container}
|
||||
[info-text
|
||||
{:width component-width
|
||||
:on-press on-text-press
|
||||
:on-long-press on-text-long-press}
|
||||
qr-data]
|
||||
[share-button
|
||||
{:alignment :top
|
||||
:on-press on-share-press}]]])
|
||||
|
||||
(def ^:private known-networks #{:ethereum :optimism :arbitrum})
|
||||
|
||||
(defn- get-network-image-source
|
||||
[network]
|
||||
{:source (quo.resources/get-network (get known-networks network :unknown))})
|
||||
|
||||
(defn wallet-multichain-bottom
|
||||
[{:keys [share-qr-type component-width qr-data on-text-press on-text-long-press
|
||||
on-share-press networks on-settings-press]}]
|
||||
[rn/view {:style style/wallet-multichain-container}
|
||||
[rn/view {:style style/wallet-multichain-networks}
|
||||
[preview-list/view {:type :network :size :size-32}
|
||||
(map get-network-image-source networks)]
|
||||
[button/button
|
||||
{:icon-only? true
|
||||
:type :grey
|
||||
:background :blur
|
||||
:size 32
|
||||
:accessibility-label :share-qr-code-settings
|
||||
:on-press on-settings-press}
|
||||
:i/advanced]]
|
||||
[rn/view {:style style/divider-container}
|
||||
[dashed-line component-width]]
|
||||
[rn/view {:style style/wallet-multichain-data-container}
|
||||
[info-label share-qr-type]
|
||||
[rn/view {:style style/wallet-data-and-share-container}
|
||||
[info-text
|
||||
{:width component-width
|
||||
:on-press on-text-press
|
||||
:on-long-press on-text-long-press}
|
||||
[wallet-multichain-colored-address qr-data]]
|
||||
[share-button
|
||||
{:alignment :top
|
||||
:on-press on-share-press}]]]])
|
||||
|
||||
(defn- share-qr-code
|
||||
[{:keys [share-qr-type qr-image-uri component-width customization-color full-name
|
||||
profile-picture emoji]
|
||||
:as props}]
|
||||
[rn/view {:style style/content-container}
|
||||
(when (#{:wallet-legacy :wallet-multichain} share-qr-type)
|
||||
[header props])
|
||||
[quo.theme/provider {:theme :light}
|
||||
[qr-code/view
|
||||
{:qr-image-uri qr-image-uri
|
||||
:size (style/qr-code-size component-width)
|
||||
:avatar (if (= share-qr-type :profile)
|
||||
:profile
|
||||
:wallet-account)
|
||||
:customization-color customization-color
|
||||
:full-name full-name
|
||||
:profile-picture profile-picture
|
||||
:emoji emoji}]]
|
||||
[rn/view {:style style/bottom-container}
|
||||
(case share-qr-type
|
||||
:profile [profile-bottom props]
|
||||
:wallet-legacy [wallet-legacy-bottom props]
|
||||
:wallet-multichain [wallet-multichain-bottom props]
|
||||
nil)]])
|
||||
|
||||
(defn view
|
||||
[{:keys [qr-image-uri link-title url-on-press url-on-long-press qr-url share-on-press]}]
|
||||
[blur/ios-view
|
||||
{:style style/qr-code-container
|
||||
:blur-type :light}
|
||||
[qr-code/view {:qr-image-uri qr-image-uri}]
|
||||
[rn/view {:style style/profile-address-container}
|
||||
[rn/view {:style style/profile-address-column}
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :medium
|
||||
:style style/profile-address-label}
|
||||
link-title]
|
||||
[rn/touchable-highlight
|
||||
{:active-opacity 1
|
||||
:underlay-color colors/neutral-80-opa-1-blur
|
||||
:background-color :transparent
|
||||
:on-press url-on-press
|
||||
:on-long-press url-on-long-press
|
||||
:style style/profile-address-content-container}
|
||||
[text/text
|
||||
{:style style/profile-address-content
|
||||
:size :paragraph-1
|
||||
:weight :medium
|
||||
:ellipsize-mode :middle
|
||||
:number-of-lines 1}
|
||||
qr-url]]]
|
||||
[rn/view {:style style/share-button-container}
|
||||
[button/button
|
||||
{:icon-only? true
|
||||
:type :grey
|
||||
:background :blur
|
||||
:size 32
|
||||
:accessibility-label :share-profile
|
||||
:on-press share-on-press}
|
||||
:i/share]]]])
|
||||
"Receives the following properties:
|
||||
- type: :profile | :wallet-legacy | :wallet-multichain
|
||||
- qr-image-uri: Image source value.
|
||||
- qr-data: Text to show below the QR code.
|
||||
- on-text-press: Callback for the `qr-data` text.
|
||||
- on-text-long-press: Callback for the `qr-data` text.
|
||||
- on-share-press: Callback for the share button.
|
||||
- customization-color: Custom color for the QR code component.
|
||||
- unblur-on-android?: [Android only] disables blur for this component.
|
||||
|
||||
Depending on the `type`, different properties are accepted:
|
||||
`:profile`
|
||||
- full-name: User full name.
|
||||
- profile-picture: map ({:source image-source}) or any image source.
|
||||
`:wallet-legacy`
|
||||
- emoji: Emoji in a string to show in the QR code.
|
||||
- on-info-press: Callback for the info icon.
|
||||
- on-legacy-press: Callback for the legacy tab.
|
||||
- on-multichain-press: Callback for the multichain tab.
|
||||
`:wallet-multichain`
|
||||
- networks: A vector of network names as keywords (`[:ethereum, :my-net, ...]`).
|
||||
- on-settings-press: Callback for the settings button.
|
||||
- emoji: Emoji in a string to show in the QR code.
|
||||
- on-info-press: Callback for the info icon.
|
||||
- on-legacy-press: Callback for the legacy tab.
|
||||
- on-multichain-press: Callback for the multichain tab.
|
||||
|
||||
WARNING on Android:
|
||||
Sometimes while using a blur layer on top of another on Android, this component looks
|
||||
bad because of the `blur/view`, so we can set `unblur-on-android? true` to fix it.
|
||||
"
|
||||
[{:keys [unblur-on-android?] :as props}]
|
||||
(reagent/with-let [component-width (reagent/atom nil)
|
||||
container-component (if (and platform/android? unblur-on-android?)
|
||||
[rn/view {:background-color style/overlay-color}]
|
||||
[blur/view
|
||||
{:blur-radius 20
|
||||
:blur-amount 20
|
||||
:blur-type :transparent
|
||||
:overlay-color style/overlay-color
|
||||
:background-color style/overlay-color}])]
|
||||
[quo.theme/provider {:theme :dark}
|
||||
[rn/view
|
||||
{:accessibility-label :share-qr-code
|
||||
:style style/outer-container
|
||||
:on-layout #(reset! component-width (oops/oget % "nativeEvent.layout.width"))}
|
||||
(conj container-component
|
||||
(when @component-width
|
||||
[share-qr-code
|
||||
(-> props
|
||||
(assoc :component-width @component-width)
|
||||
(clojure.set/rename-keys {:type :share-qr-type}))]))]]))
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(def empty-username "Account 1")
|
||||
|
||||
(defn get-test-data
|
||||
[type watch-only? empty-type? loading? metrics?]
|
||||
[{:keys [type watch-only? empty-type? loading? no-metrics?]}]
|
||||
{:name (if empty-type? empty-username username)
|
||||
:balance "€1,000.00"
|
||||
:percentage-value "50%"
|
||||
@@ -15,17 +15,18 @@
|
||||
:customization-color :blue
|
||||
:watch-only? watch-only?
|
||||
:loading? loading?
|
||||
:metrics? metrics?
|
||||
:metrics? (not no-metrics?)
|
||||
:type type})
|
||||
|
||||
(h/describe "Account_card tests"
|
||||
(h/test "Renders Default"
|
||||
(let [data (get-test-data :default false false false true)]
|
||||
(let [data (get-test-data {:type :default})]
|
||||
(h/render [account-card/view data])
|
||||
(h/is-truthy (h/get-by-text username))))
|
||||
|
||||
(h/test "Renders Watch-Only"
|
||||
(let [data (get-test-data :watch-only true false false true)]
|
||||
(let [data (get-test-data {:type :watch-only
|
||||
:watch-only? true})]
|
||||
(h/render [account-card/view data])
|
||||
(h/is-truthy (h/get-by-text username))))
|
||||
|
||||
@@ -35,10 +36,16 @@
|
||||
(h/is-truthy (h/get-by-label-text :add-account))))
|
||||
|
||||
(h/test "Renders Empty"
|
||||
(let [data (get-test-data :empty false true false true)]
|
||||
(let [data (get-test-data {:type :empty
|
||||
:empty-type? true})]
|
||||
(h/render [account-card/view data])
|
||||
(h/is-truthy (h/get-by-text empty-username))))
|
||||
|
||||
(h/test "Renders Missing Keypair"
|
||||
(let [data (get-test-data {:type :missing-keypair})]
|
||||
(h/render [account-card/view data])
|
||||
(h/is-truthy (h/get-by-text username))))
|
||||
|
||||
(h/test "Add account on press fires correctly"
|
||||
(let [on-press (h/mock-fn)
|
||||
data {:type :add-account
|
||||
@@ -48,16 +55,20 @@
|
||||
(h/was-called on-press)))
|
||||
|
||||
(h/test "Renders component without metrics"
|
||||
(let [data (get-test-data :default false false false false)]
|
||||
(let [data (get-test-data {:type :default
|
||||
:no-metrics? true})]
|
||||
(h/render [account-card/view data])
|
||||
(h/is-falsy (h/query-by-label-text :metrics))))
|
||||
|
||||
(h/test "Renders loading state"
|
||||
(let [data (get-test-data :default false false true true)]
|
||||
(let [data (get-test-data {:type :default
|
||||
:loading? true})]
|
||||
(h/render [account-card/view data])
|
||||
(h/is-truthy (h/get-by-label-text :loading))))
|
||||
|
||||
(h/test "Renders loading state without metrics"
|
||||
(let [data (get-test-data :default false false true false)]
|
||||
(let [data (get-test-data {:type :default
|
||||
:no-metrics? true
|
||||
:loading? true})]
|
||||
(h/render [account-card/view data])
|
||||
(h/is-falsy (h/query-by-label-text :metrics)))))
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
(ns quo.components.wallet.account-card.properties
|
||||
(:require [quo.foundations.colors :as colors]))
|
||||
|
||||
(defn gradient-start-color
|
||||
[theme customization-color]
|
||||
(colors/theme-colors
|
||||
(colors/resolve-color customization-color theme 0)
|
||||
colors/neutral-95
|
||||
theme))
|
||||
|
||||
(defn gradient-end-color
|
||||
[theme customization-color]
|
||||
(colors/theme-colors
|
||||
(colors/resolve-color customization-color theme 6)
|
||||
colors/neutral-95
|
||||
theme))
|
||||
|
||||
(defn alert-icon-color
|
||||
[theme]
|
||||
(colors/theme-colors colors/neutral-50 colors/white theme))
|
||||
@@ -3,24 +3,29 @@
|
||||
[quo.foundations.colors :as colors]))
|
||||
|
||||
(defn text-color
|
||||
[watch-only? theme]
|
||||
(if (and watch-only? (= :light theme))
|
||||
[type theme]
|
||||
(if (and (or (= :missing-keypair type)
|
||||
(= :watch-only type))
|
||||
(= :light theme))
|
||||
colors/neutral-100
|
||||
colors/white))
|
||||
|
||||
(defn card
|
||||
[{:keys [customization-color watch-only? metrics? theme pressed?]}]
|
||||
[{:keys [customization-color type theme pressed?]}]
|
||||
{:width 162
|
||||
:height (if metrics? 88 68)
|
||||
:background-color (if watch-only?
|
||||
(colors/theme-colors colors/neutral-80-opa-5 colors/neutral-95 theme)
|
||||
:background-color (when (not= :watch-only type)
|
||||
(colors/theme-colors
|
||||
(colors/custom-color customization-color 50)
|
||||
(colors/custom-color customization-color 60)
|
||||
(colors/resolve-color customization-color
|
||||
theme
|
||||
(when (= :missing-keypair type) (if pressed? 20 10)))
|
||||
(colors/resolve-color customization-color
|
||||
theme
|
||||
(when (= :missing-keypair type) (if pressed? 30 20)))
|
||||
theme))
|
||||
:border-radius 16
|
||||
:border-width 1
|
||||
:border-color (if watch-only?
|
||||
:border-color (if (or (= :missing-keypair type)
|
||||
(= :watch-only type))
|
||||
(colors/theme-colors
|
||||
(if pressed? colors/neutral-80-opa-10 colors/neutral-80-opa-5)
|
||||
(if pressed? colors/white-opa-10 colors/white-opa-5)
|
||||
@@ -28,10 +33,10 @@
|
||||
colors/neutral-80-opa-10)
|
||||
:padding-horizontal 12
|
||||
:padding-top 6
|
||||
:padding-bottom 10})
|
||||
:padding-bottom 9})
|
||||
|
||||
(def profile-container
|
||||
{:margin-bottom 6
|
||||
{:margin-bottom 8
|
||||
:flex-direction :row})
|
||||
|
||||
(def metrics-container
|
||||
@@ -39,8 +44,8 @@
|
||||
:align-items :center})
|
||||
|
||||
(defn account-name
|
||||
[watch-only? theme]
|
||||
{:color (text-color watch-only? theme)
|
||||
[type theme]
|
||||
{:color (text-color type theme)
|
||||
:margin-left 2})
|
||||
|
||||
(def watch-only-container
|
||||
@@ -50,21 +55,25 @@
|
||||
:flex 1})
|
||||
|
||||
(defn account-value
|
||||
[watch-only? theme]
|
||||
{:color (text-color watch-only? theme)})
|
||||
[type theme]
|
||||
{:color (text-color type theme)})
|
||||
|
||||
(defn metrics
|
||||
[watch-only? theme]
|
||||
{:color (if (and watch-only? (= :light theme))
|
||||
[type theme]
|
||||
{:color (if (and (or (= :missing-keypair type)
|
||||
(= :watch-only type))
|
||||
(= :light theme))
|
||||
colors/neutral-80-opa-60
|
||||
colors/white-opa-70)})
|
||||
|
||||
(defn separator
|
||||
[watch-only? theme]
|
||||
[type theme]
|
||||
{:width 2
|
||||
:height 2
|
||||
:border-radius 20
|
||||
:background-color (if (and watch-only? (= :light theme))
|
||||
:background-color (if (and (or (= :missing-keypair type)
|
||||
(= :watch-only type))
|
||||
(= :light theme))
|
||||
colors/neutral-80-opa-20
|
||||
colors/white-opa-40)
|
||||
:margin-horizontal 4})
|
||||
@@ -102,3 +111,12 @@
|
||||
|
||||
(def metrics-icon-container
|
||||
{:margin-left 4})
|
||||
|
||||
(def gradient-view
|
||||
{:position :absolute
|
||||
:bottom 0
|
||||
:top 0
|
||||
:left 0
|
||||
:right 0
|
||||
:border-radius 16
|
||||
:z-index -1})
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
[quo.components.buttons.button.view :as button]
|
||||
[quo.components.icon :as icon]
|
||||
[quo.components.markdown.text :as text]
|
||||
[quo.components.wallet.account-card.properties :as properties]
|
||||
[quo.components.wallet.account-card.style :as style]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.foundations.customization-colors :as customization-colors]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.linear-gradient :as linear-gradient]
|
||||
[reagent.core :as reagent]))
|
||||
|
||||
(defn- loading-view
|
||||
@@ -17,8 +19,7 @@
|
||||
[rn/view
|
||||
{:accessibility-label :loading
|
||||
:style (style/card {:customization-color customization-color
|
||||
:watch-only? watch-only?
|
||||
:metrics? metrics?
|
||||
:type type
|
||||
:theme theme
|
||||
:pressed? false})}
|
||||
[rn/view {:style style/loader-container}
|
||||
@@ -54,31 +55,43 @@
|
||||
10)}])]))
|
||||
|
||||
(defn- metrics-percentage
|
||||
[watch-only? theme account-percentage]
|
||||
[type theme account-percentage]
|
||||
[text/text
|
||||
{:weight :semi-bold
|
||||
{:weight :medium
|
||||
:size :paragraph-2
|
||||
:accessibility-label :metrics
|
||||
:style (style/metrics watch-only? theme)}
|
||||
:style (style/metrics type theme)}
|
||||
account-percentage])
|
||||
|
||||
(defn- metrics-info
|
||||
[watch-only? theme account-amount]
|
||||
[type theme account-amount]
|
||||
[:<>
|
||||
[rn/view (style/separator watch-only? theme)]
|
||||
[rn/view (style/separator type theme)]
|
||||
[text/text
|
||||
{:weight :semi-bold
|
||||
{:weight :medium
|
||||
:size :paragraph-2
|
||||
:style (style/metrics watch-only? theme)}
|
||||
:style (style/metrics type theme)}
|
||||
account-amount]
|
||||
[rn/view {:style style/metrics-icon-container}
|
||||
[icon/icon
|
||||
:i/positive
|
||||
{:color (colors/theme-colors (if watch-only? colors/neutral-50 colors/white-opa-70)
|
||||
{:color (colors/theme-colors (if (or (= :missing-keypair type)
|
||||
(= :watch-only type))
|
||||
colors/neutral-50
|
||||
colors/white-opa-70)
|
||||
colors/white-opa-70
|
||||
theme)
|
||||
:size 16}]]])
|
||||
|
||||
(defn- gradient-overview
|
||||
[theme customization-color]
|
||||
[linear-gradient/linear-gradient
|
||||
{:colors [(properties/gradient-start-color theme customization-color)
|
||||
(properties/gradient-end-color theme customization-color)]
|
||||
:style style/gradient-view
|
||||
:start {:x 0 :y 0}
|
||||
:end {:x 1 :y 0}}])
|
||||
|
||||
(defn- user-account
|
||||
[]
|
||||
(let [pressed? (reagent/atom false)
|
||||
@@ -86,7 +99,8 @@
|
||||
on-press-out #(reset! pressed? false)]
|
||||
(fn [{:keys [name balance percentage-value loading? amount customization-color type emoji metrics?
|
||||
theme on-press]}]
|
||||
(let [watch-only? (= :watch-only type)]
|
||||
(let [watch-only? (= :watch-only type)
|
||||
missing-keypair? (= :missing-keypair type)]
|
||||
(if loading?
|
||||
[loading-view
|
||||
{:customization-color customization-color
|
||||
@@ -97,12 +111,11 @@
|
||||
{:on-press-in on-press-in
|
||||
:on-press-out on-press-out
|
||||
:style (style/card {:customization-color customization-color
|
||||
:watch-only? watch-only?
|
||||
:metrics? metrics?
|
||||
:type type
|
||||
:theme theme
|
||||
:pressed? @pressed?})
|
||||
:on-press on-press}
|
||||
(when (and customization-color (not watch-only?))
|
||||
(when (and customization-color (and (not watch-only?) (not missing-keypair?)))
|
||||
[customization-colors/overlay
|
||||
{:customization-color customization-color
|
||||
:border-radius 16
|
||||
@@ -115,19 +128,23 @@
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :medium
|
||||
:style (style/account-name watch-only? theme)}
|
||||
:style (style/account-name type theme)}
|
||||
name]
|
||||
(when watch-only? [icon/icon :i/reveal {:color colors/neutral-50 :size 12}])]]
|
||||
(when watch-only? [icon/icon :i/reveal {:color colors/neutral-50 :size 12}])
|
||||
(when missing-keypair?
|
||||
[icon/icon :i/alert {:color (properties/alert-icon-color theme) :size 12}])]]
|
||||
[text/text
|
||||
{:size :heading-2
|
||||
:weight :semi-bold
|
||||
:style (style/account-value watch-only? theme)}
|
||||
:style (style/account-value type theme)}
|
||||
balance]
|
||||
(when metrics?
|
||||
[rn/view {:style style/metrics-container}
|
||||
[metrics-percentage watch-only? theme percentage-value]
|
||||
[metrics-percentage type theme percentage-value]
|
||||
(when (not= :empty type)
|
||||
[metrics-info watch-only? theme amount])])])))))
|
||||
[metrics-info type theme amount])])
|
||||
(when watch-only?
|
||||
[gradient-overview theme customization-color])])))))
|
||||
|
||||
(defn- add-account-view
|
||||
[]
|
||||
@@ -156,10 +173,8 @@
|
||||
(defn- view-internal
|
||||
[{:keys [type] :as props}]
|
||||
(case type
|
||||
:watch-only [user-account props]
|
||||
:add-account [add-account-view props]
|
||||
:default [user-account props]
|
||||
:empty [user-account props]
|
||||
(:watch-only :default :empty :missing-keypair) [user-account props]
|
||||
:add-account [add-account-view props]
|
||||
nil))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
(ns quo.components.wallet.address-text.style
|
||||
(:require [quo.foundations.colors :as colors]))
|
||||
|
||||
(defn address-text
|
||||
[format blur? theme]
|
||||
(when (= format :short)
|
||||
{:color (if blur?
|
||||
colors/white-opa-40
|
||||
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme))}))
|
||||
@@ -0,0 +1,31 @@
|
||||
(ns quo.components.wallet.address-text.view
|
||||
(:require [quo.components.markdown.text :as text]
|
||||
[quo.components.wallet.address-text.style :as style]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme]
|
||||
[utils.address :as utils]))
|
||||
|
||||
(defn- colored-network-text
|
||||
[theme network]
|
||||
(let [{:keys [name short-name]} network]
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:style {:color (colors/resolve-color name theme)}}
|
||||
(str short-name ":")]))
|
||||
|
||||
(defn- view-internal
|
||||
[{:keys [networks address blur? theme format]}]
|
||||
(let [network-text-xf (map #(colored-network-text theme %))
|
||||
address-text [text/text
|
||||
{:size :paragraph-2
|
||||
;; TODO: monospace font https://github.com/status-im/status-mobile/issues/17009
|
||||
:weight :monospace
|
||||
:style (style/address-text format blur? theme)}
|
||||
(if (= format :short)
|
||||
(utils/get-short-wallet-address address)
|
||||
address)]]
|
||||
(as-> networks $
|
||||
(into [text/text] network-text-xf $)
|
||||
(conj $ address-text))))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
@@ -10,7 +10,8 @@
|
||||
:type :default
|
||||
:name "Trip to Vegas"
|
||||
:address "0x0ah...71a"}
|
||||
:networks [:ethereum :optimism]
|
||||
:networks [{:name :ethereum :short-name "eth"}
|
||||
{:name :optimism :short-name "opt"}]
|
||||
:state :default
|
||||
:action :none}])
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
(ns quo.components.wallet.network-routing.animation
|
||||
(:require [react-native.reanimated :as reanimated]))
|
||||
|
||||
(def ^:private slider-timing 300)
|
||||
|
||||
(defn show-slider
|
||||
[opacity-shared-value]
|
||||
(reanimated/animate opacity-shared-value 1 slider-timing))
|
||||
|
||||
(defn hide-slider
|
||||
[opacity-shared-value]
|
||||
(reanimated/animate opacity-shared-value 0 slider-timing))
|
||||
|
||||
(defn increase-slider
|
||||
[width-shared-value height-shared-value]
|
||||
(reanimated/animate width-shared-value 8 slider-timing)
|
||||
(reanimated/animate height-shared-value 40 slider-timing))
|
||||
|
||||
(defn decrease-slider
|
||||
[width-shared-value height-shared-value]
|
||||
(reanimated/animate width-shared-value 4 slider-timing)
|
||||
(reanimated/animate height-shared-value 32 slider-timing))
|
||||
|
||||
(def ^:private pressed-bar-timing 600)
|
||||
|
||||
(defn move-previous-bars
|
||||
[{:keys [bars bars-widths-negative]}]
|
||||
(doseq [[bar-idx bar] (map-indexed vector bars)
|
||||
:let [new-translation-x (->> (take (inc bar-idx) bars-widths-negative)
|
||||
(reduce +)
|
||||
(dec))]]
|
||||
(reanimated/animate (:translate-x-shared-value bar)
|
||||
new-translation-x
|
||||
pressed-bar-timing)))
|
||||
|
||||
(defn move-pressed-bar
|
||||
[{:keys [bars-widths-negative number-previous-bars]
|
||||
{:keys [translate-x-shared-value]} :bar}]
|
||||
(let [new-translation-x (reduce + (take number-previous-bars bars-widths-negative))]
|
||||
(reanimated/animate translate-x-shared-value
|
||||
new-translation-x
|
||||
(- pressed-bar-timing 20))))
|
||||
|
||||
(defn move-next-bars
|
||||
[{:keys [bars bars-widths-negative number-previous-bars extra-offset add-new-timeout]}]
|
||||
(doseq [[bar-idx bar] (map-indexed vector bars)
|
||||
:let [number-bars-before (+ number-previous-bars
|
||||
(inc bar-idx))
|
||||
new-translation-x (->> (take number-bars-before bars-widths-negative)
|
||||
(reduce +)
|
||||
(* 1.05))]]
|
||||
(reanimated/animate (:translate-x-shared-value bar) new-translation-x pressed-bar-timing)
|
||||
(add-new-timeout
|
||||
(keyword (str "fix-next-bars-position-" bar-idx))
|
||||
(fn []
|
||||
(let [translate-x-value (reanimated/get-shared-value (:translate-x-shared-value bar))
|
||||
hidden-position (- translate-x-value extra-offset)]
|
||||
(reanimated/set-shared-value (:translate-x-shared-value bar) hidden-position)))
|
||||
pressed-bar-timing)))
|
||||
|
||||
(def ^:private max-limit-bar-timing 300)
|
||||
|
||||
(defn show-max-limit-bar
|
||||
[max-limit-bar-opacity]
|
||||
(reanimated/animate max-limit-bar-opacity 1 max-limit-bar-timing))
|
||||
|
||||
(defn hide-max-limit-bar
|
||||
[max-limit-bar-opacity]
|
||||
(reanimated/animate max-limit-bar-opacity 0 max-limit-bar-timing))
|
||||
|
||||
(defn reset-bars-positions
|
||||
[bars unlock-press-fn add-new-timeout]
|
||||
(let [bars-reset-timing 500]
|
||||
(doseq [{:keys [translate-x-shared-value]} bars]
|
||||
(reanimated/animate translate-x-shared-value 0 bars-reset-timing))
|
||||
(add-new-timeout :unlock-press unlock-press-fn bars-reset-timing)))
|
||||
|
||||
(defn align-bars-off-screen
|
||||
[{:keys [new-network-values network-bars amount->width add-new-timeout]}]
|
||||
(let [width-to-off-screen (->> new-network-values
|
||||
(reduce #(- %1 (:amount %2)) 0)
|
||||
(amount->width))]
|
||||
(doseq [[bar-idx {new-amount :amount} bar] (map vector (range) new-network-values network-bars)]
|
||||
(reanimated/set-shared-value (:amount-shared-value bar) new-amount)
|
||||
(reanimated/set-shared-value (:translate-x-shared-value bar) (* 2 width-to-off-screen))
|
||||
(add-new-timeout
|
||||
(keyword (str "align-bar-" bar-idx))
|
||||
#(reanimated/set-shared-value (:translate-x-shared-value bar) width-to-off-screen)
|
||||
1))))
|
||||
|
||||
(def ^:private hide-bar-timing 400)
|
||||
|
||||
(defn hide-pressed-bar
|
||||
[{:keys [translate-x-shared-value amount-shared-value]} amount->width]
|
||||
(let [bar-width (amount->width (reanimated/get-shared-value amount-shared-value))
|
||||
new-translation-x (- (reanimated/get-shared-value translate-x-shared-value)
|
||||
bar-width)]
|
||||
(reanimated/animate translate-x-shared-value new-translation-x hide-bar-timing)))
|
||||
|
||||
(defn update-bar-values-and-reset-animations
|
||||
[{:keys [new-network-values network-bars amount->width reset-values-fn add-new-timeout
|
||||
lock-press-fn unlock-press-fn]}]
|
||||
(lock-press-fn)
|
||||
(add-new-timeout
|
||||
:update-bars-values
|
||||
(fn []
|
||||
(align-bars-off-screen {:new-network-values new-network-values
|
||||
:network-bars network-bars
|
||||
:amount->width amount->width
|
||||
:add-new-timeout add-new-timeout})
|
||||
(reset-values-fn)
|
||||
(add-new-timeout :reset-bars
|
||||
#(reset-bars-positions network-bars unlock-press-fn add-new-timeout)
|
||||
100))
|
||||
hide-bar-timing))
|
||||
@@ -0,0 +1,28 @@
|
||||
(ns quo.components.wallet.network-routing.component-spec
|
||||
(:require [oops.core :as oops]
|
||||
[quo.components.wallet.network-routing.view :as network-routing]
|
||||
[reagent.core :as reagent]
|
||||
[test-helpers.component :as h]))
|
||||
|
||||
(h/describe "Network-routing tests"
|
||||
(let [network {:amount 250 :max-amount 300 :network-name :unknown}
|
||||
default-props {:networks [network network network]
|
||||
:total-amount 500
|
||||
:requesting-data? false
|
||||
:on-amount-selected (fn [_new-amount _network-idx] nil)}]
|
||||
(h/test "Renders Default"
|
||||
(h/render [network-routing/view default-props])
|
||||
(h/is-truthy (h/get-by-label-text :network-routing)))
|
||||
|
||||
(h/test "Renders bars inside"
|
||||
(let [component (h/render [network-routing/view default-props])
|
||||
rerender-fn #((oops/oget component "rerender") (reagent/as-element %))
|
||||
component (h/get-by-label-text :network-routing)]
|
||||
;; Fires on-layout callback since the total width is required
|
||||
(h/fire-event :layout component #js {:nativeEvent #js {:layout #js {:width 1000}}})
|
||||
;; Update props to trigger rerender, otherwise it won't be updated
|
||||
(rerender-fn [network-routing/view (assoc default-props :requesting-data? true)])
|
||||
;; Check number of networks rendered
|
||||
(->> (js->clj (h/query-all-by-label-text :network-routing-bar))
|
||||
(count)
|
||||
(h/is-equal 3))))))
|
||||
@@ -0,0 +1,79 @@
|
||||
(ns quo.components.wallet.network-routing.style
|
||||
(:require [quo.foundations.colors :as colors]
|
||||
[react-native.reanimated :as reanimated]))
|
||||
|
||||
(defn container
|
||||
[container-style theme]
|
||||
(assoc container-style
|
||||
:flex-direction :row
|
||||
:height 64
|
||||
:background-color (colors/theme-colors colors/neutral-100-opa-5 colors/neutral-90 theme)
|
||||
:border-radius 20
|
||||
:overflow :hidden))
|
||||
|
||||
(defn max-limit-bar
|
||||
[{:keys [opacity-shared-value width]}]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity-shared-value}
|
||||
{:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:left 0
|
||||
:width width
|
||||
:z-index -1
|
||||
:flex-direction :row}))
|
||||
|
||||
(defn max-limit-bar-background
|
||||
[network-name]
|
||||
{:flex 1
|
||||
:background-color (colors/resolve-color network-name nil 10)})
|
||||
|
||||
(defn network-bar
|
||||
[{:keys [max-width on-top? bar-division? theme]
|
||||
{:keys [network-name translate-x-shared-value]} :bar}
|
||||
width-shared-value]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:width width-shared-value
|
||||
:transform [{:translate-x translate-x-shared-value}]}
|
||||
{:max-width max-width
|
||||
:flex-direction :row
|
||||
:justify-content :flex-end
|
||||
:background-color (colors/resolve-color network-name nil)
|
||||
:z-index (if on-top? 1 0)
|
||||
:border-right-width (if bar-division? 0 1)
|
||||
:border-color (colors/theme-colors colors/white colors/neutral-95 theme)}))
|
||||
|
||||
(def slider-container
|
||||
{:width 40
|
||||
:background-color :transparent
|
||||
:justify-content :center
|
||||
:align-items :center
|
||||
:right -20})
|
||||
|
||||
(def ^:private slider-fixed-styles
|
||||
{:background-color colors/white
|
||||
:height 32
|
||||
:width 4
|
||||
:border-radius 4})
|
||||
|
||||
(defn slider
|
||||
[{:keys [width-shared-value height-shared-value opacity-shared-value]}]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:width width-shared-value
|
||||
:height height-shared-value
|
||||
:opacity opacity-shared-value}
|
||||
slider-fixed-styles))
|
||||
|
||||
(def dashed-line
|
||||
{:width 1
|
||||
:height "100%"
|
||||
:margin-left -1
|
||||
:margin-top -1.5})
|
||||
|
||||
(defn dashed-line-line
|
||||
[network-name]
|
||||
{:background-color (colors/resolve-color network-name nil)
|
||||
:height 3
|
||||
:width 1})
|
||||
|
||||
(def dashed-line-space {:height 4 :width 1})
|
||||
@@ -0,0 +1,189 @@
|
||||
(ns quo.components.wallet.network-routing.view
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[quo.components.wallet.network-routing.animation :as animation]
|
||||
[quo.components.wallet.network-routing.style :as style]
|
||||
[quo.theme :as quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[utils.number]))
|
||||
|
||||
(def ^:private timeouts (atom {}))
|
||||
|
||||
(defn- add-new-timeout
|
||||
[k f ms]
|
||||
(letfn [(exec-fn-and-remove-timeout []
|
||||
(f)
|
||||
(swap! timeouts dissoc k))]
|
||||
(js/clearTimeout (k @timeouts))
|
||||
(swap! timeouts assoc k (js/setTimeout exec-fn-and-remove-timeout ms))))
|
||||
|
||||
(defn- f-slider
|
||||
[slider-shared-values]
|
||||
[rn/view {:style style/slider-container}
|
||||
[reanimated/view {:style (style/slider slider-shared-values)}]])
|
||||
|
||||
(defn f-network-bar
|
||||
[_]
|
||||
(let [detecting-gesture? (reagent/atom false)
|
||||
amount-on-gesture-start (atom 0)]
|
||||
(fn [{:keys [total-width total-amount on-press on-new-amount allow-press?]
|
||||
{:keys [amount-shared-value
|
||||
max-amount]} :bar
|
||||
:as props}]
|
||||
(let [slider-width-shared-value (reanimated/use-shared-value 4)
|
||||
slider-height-shared-value (reanimated/use-shared-value 32)
|
||||
slider-opacity-shared-value (reanimated/use-shared-value 0)
|
||||
network-bar-shared-value (reanimated/interpolate amount-shared-value
|
||||
[0 total-amount]
|
||||
[0 total-width])
|
||||
width->amount #(/ (* % total-amount) total-width)]
|
||||
[rn/touchable-without-feedback
|
||||
{:on-press (fn []
|
||||
(when (and (not @detecting-gesture?) allow-press?)
|
||||
(on-press)
|
||||
(reset! detecting-gesture? true)
|
||||
(animation/show-slider slider-opacity-shared-value)))}
|
||||
[reanimated/view
|
||||
{:style (style/network-bar props network-bar-shared-value)
|
||||
:accessibility-label :network-routing-bar}
|
||||
[gesture/gesture-detector
|
||||
{:gesture
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/enabled @detecting-gesture?)
|
||||
(gesture/on-begin
|
||||
(fn [_]
|
||||
(animation/increase-slider slider-width-shared-value slider-height-shared-value)
|
||||
(reset! amount-on-gesture-start (reanimated/get-shared-value amount-shared-value))))
|
||||
(gesture/on-update
|
||||
(fn [event]
|
||||
(let [new-amount (-> (oops/oget event "translationX")
|
||||
(width->amount)
|
||||
(+ @amount-on-gesture-start)
|
||||
(utils.number/value-in-range 1 max-amount))]
|
||||
(reanimated/set-shared-value amount-shared-value new-amount))))
|
||||
(gesture/on-finalize
|
||||
(fn [_]
|
||||
(animation/decrease-slider slider-width-shared-value slider-height-shared-value)
|
||||
(animation/hide-slider slider-opacity-shared-value)
|
||||
(on-new-amount (reanimated/get-shared-value amount-shared-value))
|
||||
(add-new-timeout :turn-off-gesture #(reset! detecting-gesture? false) 20))))}
|
||||
[:f> f-slider
|
||||
{:width-shared-value slider-width-shared-value
|
||||
:height-shared-value slider-height-shared-value
|
||||
:opacity-shared-value slider-opacity-shared-value}]]]]))))
|
||||
|
||||
(defn- add-bar-shared-values
|
||||
[{:keys [amount] :as network}]
|
||||
(assoc network
|
||||
:amount-shared-value (reanimated/use-shared-value amount)
|
||||
:translate-x-shared-value (reanimated/use-shared-value 0)))
|
||||
|
||||
(def ^:private get-negative-amount
|
||||
(comp - reanimated/get-shared-value :amount-shared-value))
|
||||
|
||||
(defn- dashed-line
|
||||
[network-name]
|
||||
[rn/view {:style style/dashed-line}
|
||||
(take 19
|
||||
(interleave (repeat [rn/view {:style (style/dashed-line-line network-name)}])
|
||||
(repeat [rn/view {:style style/dashed-line-space}])))])
|
||||
|
||||
(defn f-network-routing-bars
|
||||
[_]
|
||||
(let [selected-network-idx (reagent/atom nil)
|
||||
press-locked? (reagent/atom false)
|
||||
lock-press #(reset! press-locked? true)
|
||||
unlock-press #(reset! press-locked? false)
|
||||
reset-state-values #(reset! selected-network-idx nil)]
|
||||
(fn [{:keys [networks total-width total-amount requesting-data? on-amount-selected]}]
|
||||
(let [bar-opacity-shared-value (reanimated/use-shared-value 0)
|
||||
network-bars (map add-bar-shared-values networks)
|
||||
amount->width #(* % (/ total-width total-amount))
|
||||
bars-widths-negative (map #(-> % get-negative-amount amount->width)
|
||||
network-bars)
|
||||
last-bar-idx (dec (count network-bars))]
|
||||
(rn/use-effect
|
||||
#(when (and (not requesting-data?) @selected-network-idx)
|
||||
(let [bar (nth network-bars @selected-network-idx)]
|
||||
(animation/hide-pressed-bar bar amount->width))
|
||||
(animation/update-bar-values-and-reset-animations
|
||||
{:new-network-values networks
|
||||
:network-bars network-bars
|
||||
:amount->width amount->width
|
||||
:reset-values-fn reset-state-values
|
||||
:lock-press-fn lock-press
|
||||
:unlock-press-fn unlock-press
|
||||
:add-new-timeout add-new-timeout}))
|
||||
[requesting-data?])
|
||||
[:<>
|
||||
(doall
|
||||
(for [[bar-idx bar] (map-indexed vector network-bars)
|
||||
:let [bar-max-width (amount->width (:max-amount bar))
|
||||
bar-width (-> (:amount-shared-value bar)
|
||||
(reanimated/get-shared-value)
|
||||
(amount->width))
|
||||
hide-division? (or (= last-bar-idx bar-idx) @selected-network-idx)
|
||||
this-bar-selected? (= @selected-network-idx bar-idx)]]
|
||||
^{:key (str "network-bar-" bar-idx)}
|
||||
[:f> f-network-bar
|
||||
{:bar bar
|
||||
:max-width bar-max-width
|
||||
:total-width total-width
|
||||
:total-amount total-amount
|
||||
:bar-division? hide-division?
|
||||
:on-top? this-bar-selected?
|
||||
:allow-press? (and (or (not @selected-network-idx) this-bar-selected?)
|
||||
(not requesting-data?)
|
||||
(not @press-locked?))
|
||||
:on-press (fn []
|
||||
(when-not @selected-network-idx
|
||||
(let [[previous-bars [_ & next-bars]] (split-at bar-idx network-bars)
|
||||
number-previous-bars bar-idx]
|
||||
(animation/move-previous-bars
|
||||
{:bars previous-bars
|
||||
:bars-widths-negative bars-widths-negative})
|
||||
(animation/move-pressed-bar
|
||||
{:bar bar
|
||||
:bars-widths-negative bars-widths-negative
|
||||
:number-previous-bars number-previous-bars})
|
||||
(animation/move-next-bars
|
||||
{:bars next-bars
|
||||
:bars-widths-negative bars-widths-negative
|
||||
:number-previous-bars (inc number-previous-bars)
|
||||
:extra-offset (max 0 (- bar-max-width bar-width))
|
||||
:add-new-timeout add-new-timeout}))
|
||||
(animation/show-max-limit-bar bar-opacity-shared-value)
|
||||
(reset! selected-network-idx bar-idx)))
|
||||
:on-new-amount (fn [new-amount]
|
||||
(animation/hide-max-limit-bar bar-opacity-shared-value)
|
||||
(when on-amount-selected
|
||||
(on-amount-selected new-amount @selected-network-idx)))}]))
|
||||
|
||||
(let [{:keys [max-amount network-name]} (some->> @selected-network-idx
|
||||
(nth network-bars))
|
||||
limit-bar-width (amount->width max-amount)]
|
||||
[reanimated/view
|
||||
{:style (style/max-limit-bar
|
||||
{:opacity-shared-value bar-opacity-shared-value
|
||||
:width limit-bar-width})}
|
||||
[rn/view {:style (style/max-limit-bar-background network-name)}]
|
||||
[dashed-line network-name]])]))))
|
||||
|
||||
(defn view-internal
|
||||
[{:keys [networks container-style theme] :as params}]
|
||||
(reagent/with-let [total-width (reagent/atom nil)]
|
||||
[rn/view
|
||||
{:accessibility-label :network-routing
|
||||
:style (style/container container-style theme)
|
||||
:on-layout #(reset! total-width (oops/oget % "nativeEvent.layout.width"))}
|
||||
(when @total-width
|
||||
^{:key (str "network-routing-" (count networks))}
|
||||
[:f> f-network-routing-bars (assoc params :total-width @total-width)])]
|
||||
(finally
|
||||
(doseq [[_ living-timeout] @timeouts]
|
||||
(js/clearTimeout living-timeout)))))
|
||||
|
||||
(def view (quo.theme/with-theme view-internal))
|
||||
@@ -48,6 +48,7 @@
|
||||
quo.components.drawers.permission-context.view
|
||||
quo.components.dropdowns.dropdown.view
|
||||
quo.components.dropdowns.network-dropdown.view
|
||||
quo.components.dropdowns.dropdown-input.view
|
||||
quo.components.empty-state.empty-state.view
|
||||
quo.components.gradient.gradient-cover.view
|
||||
[quo.components.graph.interactive-graph.view :as interactive-graph]
|
||||
@@ -77,6 +78,7 @@
|
||||
quo.components.list-items.preview-list.view
|
||||
quo.components.list-items.saved-address.view
|
||||
quo.components.list-items.saved-contact-address.view
|
||||
quo.components.list-items.token-network.view
|
||||
quo.components.list-items.token-value.view
|
||||
quo.components.list-items.user-list
|
||||
quo.components.loaders.skeleton-list.view
|
||||
@@ -112,7 +114,6 @@
|
||||
quo.components.selectors.selectors.view
|
||||
quo.components.settings.accounts.view
|
||||
quo.components.settings.category.view
|
||||
quo.components.settings.category.view
|
||||
quo.components.settings.data-item.view
|
||||
quo.components.settings.privacy-option.view
|
||||
quo.components.settings.reorder-item.view
|
||||
@@ -137,10 +138,12 @@
|
||||
quo.components.wallet.account-card.view
|
||||
quo.components.wallet.account-origin.view
|
||||
quo.components.wallet.account-overview.view
|
||||
quo.components.wallet.address-text.view
|
||||
quo.components.wallet.keypair.view
|
||||
quo.components.wallet.network-amount.view
|
||||
quo.components.wallet.network-bridge.view
|
||||
quo.components.wallet.network-link.view
|
||||
quo.components.wallet.network-routing.view
|
||||
quo.components.wallet.progress-bar.view
|
||||
quo.components.wallet.summary-info.view
|
||||
quo.components.wallet.token-input.view
|
||||
@@ -227,6 +230,7 @@
|
||||
;;;; Dropdowns
|
||||
(def dropdown quo.components.dropdowns.dropdown.view/view)
|
||||
(def network-dropdown quo.components.dropdowns.network-dropdown.view/view)
|
||||
(def dropdown-input quo.components.dropdowns.dropdown-input.view/view)
|
||||
|
||||
;;;; Empty State
|
||||
(def empty-state quo.components.empty-state.empty-state.view/empty-state)
|
||||
@@ -278,6 +282,7 @@
|
||||
(def community-list-item quo.components.list-items.community.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)
|
||||
|
||||
;;;; Loaders
|
||||
@@ -369,9 +374,11 @@
|
||||
(def account-card quo.components.wallet.account-card.view/view)
|
||||
(def account-origin quo.components.wallet.account-origin.view/view)
|
||||
(def account-overview quo.components.wallet.account-overview.view/view)
|
||||
(def address-text quo.components.wallet.address-text.view/view)
|
||||
(def keypair quo.components.wallet.keypair.view/view)
|
||||
(def network-amount quo.components.wallet.network-amount.view/view)
|
||||
(def network-bridge quo.components.wallet.network-bridge.view/view)
|
||||
(def network-routing quo.components.wallet.network-routing.view/view)
|
||||
(def progress-bar quo.components.wallet.progress-bar.view/view)
|
||||
(def summary-info quo.components.wallet.summary-info.view/view)
|
||||
(def network-link quo.components.wallet.network-link.view/view)
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
[quo.components.inputs.address-input.component-spec]
|
||||
[quo.components.inputs.input.component-spec]
|
||||
[quo.components.inputs.locked-input.component-spec]
|
||||
[quo.components.inputs.locked-input.component-spec]
|
||||
[quo.components.inputs.profile-input.component-spec]
|
||||
[quo.components.inputs.recovery-phrase.component-spec]
|
||||
[quo.components.inputs.title-input.component-spec]
|
||||
@@ -46,6 +45,7 @@
|
||||
[quo.components.list-items.dapp.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]
|
||||
[quo.components.list-items.token-value.component-spec]
|
||||
[quo.components.loaders.skeleton-list.component-spec]
|
||||
[quo.components.markdown.list.component-spec]
|
||||
@@ -72,12 +72,14 @@
|
||||
[quo.components.switchers.group-messaging-card.component-spec]
|
||||
[quo.components.tags.network-tags.component-spec]
|
||||
[quo.components.tags.status-tags-component-spec]
|
||||
[quo.components.tags.tiny-tag.component-spec]
|
||||
[quo.components.wallet.account-card.component-spec]
|
||||
[quo.components.wallet.account-origin.component-spec]
|
||||
[quo.components.wallet.account-overview.component-spec]
|
||||
[quo.components.wallet.keypair.component-spec]
|
||||
[quo.components.wallet.network-amount.component-spec]
|
||||
[quo.components.wallet.network-bridge.component-spec]
|
||||
[quo.components.wallet.network-routing.component-spec]
|
||||
[quo.components.wallet.progress-bar.component-spec]
|
||||
[quo.components.wallet.summary-info.component-spec]
|
||||
[quo.components.wallet.token-input.component-spec]
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
[k]
|
||||
(get tokens k))
|
||||
|
||||
(def networks
|
||||
(def ^:private networks
|
||||
{:arbitrum (js/require "../resources/images/networks/Arbitrum.png")
|
||||
:ethereum (js/require "../resources/images/networks/Ethereum.png")
|
||||
:gnosis (js/require "../resources/images/networks/Gnosis.png")
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
(defn universal-link
|
||||
[community-id]
|
||||
(str (:external universal-links/domains)
|
||||
"/c/"
|
||||
"/c#"
|
||||
community-id))
|
||||
|
||||
(defn <-request-to-join-community-rpc
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
|
||||
(def handled-schemes (set (into uri-schemes web-urls)))
|
||||
|
||||
(def group-chat-extractor
|
||||
{[#"(.*)" :params] {"" :group-chat
|
||||
"/" :group-chat}})
|
||||
|
||||
(def eip-extractor
|
||||
{#{[:prefix "-" :address]
|
||||
[:address]}
|
||||
@@ -39,9 +43,15 @@
|
||||
|
||||
(def routes
|
||||
[""
|
||||
{handled-schemes {["c/" :community-data] :community
|
||||
["cc/" :chat-data] :community-chat
|
||||
["u/" :user-data] :user}
|
||||
{handled-schemes {["c/" :community-data] :community
|
||||
["cc/" :community-data] :community-chat
|
||||
["p/" :chat-id] :private-chat
|
||||
["cr/" :community-id] :community-requests
|
||||
"g/" group-chat-extractor
|
||||
["wallet/" :account] :wallet-account
|
||||
["u/" :user-data] :user
|
||||
"c" :community
|
||||
"u" :user}
|
||||
ethereum-scheme eip-extractor}])
|
||||
|
||||
(defn parse-query-params
|
||||
@@ -58,7 +68,6 @@
|
||||
|
||||
(defn match-uri
|
||||
[uri]
|
||||
;;
|
||||
(let [;; bidi has trouble parse path with `=` in it extract `=` here and add back to parsed
|
||||
;; base64url regex based on https://datatracker.ietf.org/doc/html/rfc4648#section-5 may
|
||||
;; include invalid base64 (invalid length, length of any base64 encoded string must be a
|
||||
@@ -70,6 +79,7 @@
|
||||
uri-without-equal-in-path
|
||||
(if equal-end-of-base64url (string/replace-first uri equal-end-of-base64url "") uri)
|
||||
|
||||
;; fragment is the one after `#`, usually user-id, ens-name, community-id
|
||||
fragment (parse-fragment uri)
|
||||
ens? (ens/is-valid-eth-name? fragment)
|
||||
|
||||
@@ -87,8 +97,18 @@
|
||||
(and equal-end-of-base64url (= handler :community) (:community-data route-params))
|
||||
(update-in [:route-params :community-data] #(str % equal-end-of-base64url))
|
||||
|
||||
(and equal-end-of-base64url (= handler :community-chat) (:chat-data route-params))
|
||||
(update-in [:route-params :chat-data] #(str % equal-end-of-base64url))
|
||||
(and equal-end-of-base64url (= handler :community-chat) (:community-data route-params))
|
||||
(update-in [:route-params :community-data] #(str % equal-end-of-base64url))
|
||||
|
||||
(and fragment (= handler :community-chat) (:community-data route-params))
|
||||
(assoc-in [:route-params :community-id] fragment)
|
||||
|
||||
(and fragment
|
||||
(= handler :community-chat)
|
||||
(:community-data route-params)
|
||||
(string? (:community-data route-params))
|
||||
(re-find constants/regx-starts-with-uuid (:community-data route-params)))
|
||||
(assoc-in [:route-params :community-channel-id] (:community-data route-params))
|
||||
|
||||
(and equal-end-of-base64url (= handler :user) (:user-data route-params))
|
||||
(update-in [:route-params :user-data] #(str % equal-end-of-base64url))
|
||||
@@ -174,6 +194,15 @@
|
||||
(cb {:type :private-chat
|
||||
:error :invalid-chat-id})))))
|
||||
|
||||
(defn match-community-channel-async
|
||||
[{:keys [community-channel-id community-id]} cb]
|
||||
(if (validators/valid-compressed-key? community-id)
|
||||
(native-module/deserialize-and-compress-key
|
||||
community-id
|
||||
#(cb {:type :community-chat :chat-id (str % community-channel-id)}))
|
||||
(cb {:type :community-chat
|
||||
:error :not-found})))
|
||||
|
||||
(defn match-browser
|
||||
[uri {:keys [domain]}]
|
||||
;; NOTE: We rebuild domain from original URI and matched domain
|
||||
@@ -238,8 +267,8 @@
|
||||
:community))
|
||||
|
||||
(defn handle-uri
|
||||
[chain _chats uri cb]
|
||||
(let [{:keys [handler route-params]} (match-uri uri)]
|
||||
[chain chats uri cb]
|
||||
(let [{:keys [handler route-params query-params]} (match-uri uri)]
|
||||
(log/info "[router] uri " uri " matched " handler " with " route-params)
|
||||
(cond
|
||||
|
||||
@@ -253,36 +282,35 @@
|
||||
(and (= handler :user) (:user-id route-params))
|
||||
(match-contact-async chain route-params cb)
|
||||
|
||||
;; ;; NOTE: removed in `match-uri`, might need this in the future
|
||||
;; (= handler :private-chat)
|
||||
;; (match-private-chat-async chain route-params cb)
|
||||
;; NOTE: removed in `match-uri`, might need this in the future
|
||||
(= handler :private-chat)
|
||||
(match-private-chat-async chain route-params cb)
|
||||
|
||||
;; ;; NOTE: removed in `match-uri`, might need this in the future
|
||||
;; (= handler :group-chat)
|
||||
;; (cb (match-group-chat chats query-params))
|
||||
;; NOTE: removed in `match-uri`, might need this in the future
|
||||
(= handler :group-chat)
|
||||
(cb (match-group-chat chats query-params))
|
||||
|
||||
(validators/valid-public-key? uri)
|
||||
(match-contact-async chain {:user-id uri} cb)
|
||||
|
||||
;; ;; NOTE: removed in `match-uri`, might need this in the future
|
||||
;; (= handler :community-requests)
|
||||
;; (cb {:type handler :community-id (:community-id route-params)})
|
||||
;; NOTE: removed in `match-uri`, might need this in the future
|
||||
(= handler :community-requests)
|
||||
(cb {:type handler :community-id (:community-id route-params)})
|
||||
|
||||
(and (= handler :community) (:community-id route-params))
|
||||
(cb {:type (community-route-type route-params)
|
||||
:community-id (:community-id route-params)})
|
||||
|
||||
;; ;; TODO: jump to community overview for now, should jump to community channel
|
||||
;; (and (= handler :community-chat) (:chat-id route-params))
|
||||
;; (cb {:type handler :chat-id (:chat-id route-params)})
|
||||
(and (= handler :community-chat) (:community-channel-id route-params) (:community-id route-params))
|
||||
(match-community-channel-async route-params cb)
|
||||
|
||||
(and (= handler :community-chat) (:community-id route-params))
|
||||
(cb {:type (community-route-type route-params)
|
||||
:community-id (:community-id route-params)})
|
||||
|
||||
;; ;; NOTE: removed in `match-uri`, might need this in the future
|
||||
;; (= handler :wallet-account)
|
||||
;; (cb (match-wallet-account route-params))
|
||||
;; NOTE: removed in `match-uri`, might need this in the future
|
||||
(= handler :wallet-account)
|
||||
(cb (match-wallet-account route-params))
|
||||
|
||||
(address/address? uri)
|
||||
(cb (address->eip681 uri))
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
:query-params (when (= 3 (count expected)) (last expected))
|
||||
:uri uri})
|
||||
|
||||
"https://status.app/u#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"
|
||||
[:user
|
||||
{:user-id "zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"}]
|
||||
"https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"
|
||||
[:user
|
||||
{:user-data
|
||||
@@ -31,23 +34,46 @@
|
||||
"G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y"
|
||||
:user-id "zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"}]
|
||||
|
||||
"status-app://u#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"
|
||||
[:user
|
||||
{:user-id "zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"}]
|
||||
|
||||
"https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
|
||||
[:community-chat
|
||||
{:chat-data
|
||||
{:community-data
|
||||
"G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM="
|
||||
:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}]
|
||||
|
||||
"status-app://cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
|
||||
[:community-chat
|
||||
{:chat-data
|
||||
{:community-data
|
||||
"G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM="
|
||||
:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}]
|
||||
|
||||
"https://status.app/cc/c432709e-fc73-440d-bb67-cb3a0929dfda#zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo"
|
||||
[:community-chat
|
||||
{:community-data
|
||||
"c432709e-fc73-440d-bb67-cb3a0929dfda"
|
||||
:community-channel-id
|
||||
"c432709e-fc73-440d-bb67-cb3a0929dfda"
|
||||
:community-id "zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo"}]
|
||||
|
||||
"status-app://cc/c432709e-fc73-440d-bb67-cb3a0929dfda#zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo"
|
||||
[:community-chat
|
||||
{:community-data
|
||||
"c432709e-fc73-440d-bb67-cb3a0929dfda"
|
||||
:community-channel-id
|
||||
"c432709e-fc73-440d-bb67-cb3a0929dfda"
|
||||
:community-id "zQ3shZL6dXiFCbDyxnXxwQa9v8QFC2q19subFtyxd7kVszMVo"}]
|
||||
|
||||
"https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
|
||||
[:community
|
||||
{:community-data
|
||||
"iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM="
|
||||
:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}]
|
||||
"https://status.app/c#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
|
||||
[:community
|
||||
{:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}]
|
||||
|
||||
"status-app://c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
|
||||
[:community
|
||||
@@ -55,6 +81,10 @@
|
||||
"iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM="
|
||||
:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}]
|
||||
|
||||
"status-app://c#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
|
||||
[:community
|
||||
{:community-id "zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"}]
|
||||
|
||||
"ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"
|
||||
[:ethereum {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"}]
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
(ns status-im.ui.screens.qr-scanner.views
|
||||
(:require-macros [status-im.utils.views :refer [defview letsubs]])
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[re-frame.core :as re-frame]
|
||||
[react-native.camera-kit :as camera-kit]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.ui.components.colors :as colors]
|
||||
[status-im.ui.components.core :as quo]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im.ui.components.topbar :as topbar]
|
||||
[status-im.ui.screens.qr-scanner.styles :as styles]
|
||||
[status-im2.config :as config]
|
||||
[utils.i18n :as i18n]))
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn get-qr-code-data
|
||||
[^js event]
|
||||
@@ -85,13 +87,18 @@
|
||||
[opts data]
|
||||
(re-frame/dispatch [:qr-scanner.callback/scan-qr-code-success opts (get-qr-code-data data)]))
|
||||
|
||||
(defview qr-scanner
|
||||
(defn- navigate-back-handler
|
||||
[]
|
||||
(letsubs [read-once? (atom false)
|
||||
{:keys [height width]} [:dimensions/window]
|
||||
camera-flashlight [:wallet-legacy.send/camera-flashlight]
|
||||
opts [:get-screen-params]
|
||||
camera-ref (atom nil)]
|
||||
(re-frame/dispatch [:navigate-back])
|
||||
true)
|
||||
|
||||
(defn f-qr-scanner
|
||||
[]
|
||||
(let [read-once? (reagent/atom false)
|
||||
{:keys [height width]} (rf/sub [:dimensions/window])
|
||||
camera-flashlight (rf/sub [:wallet-legacy.send/camera-flashlight])
|
||||
opts (rf/sub [:get-screen-params])
|
||||
camera-ref (reagent/atom nil)]
|
||||
[react/view
|
||||
{:flex 1
|
||||
:background-color colors/black-persist}
|
||||
@@ -111,3 +118,11 @@
|
||||
(reset! read-once? true)
|
||||
(on-barcode-read opts %))}]]
|
||||
[viewfinder (int (* 2 (/ (min height width) 3)))]])]))
|
||||
|
||||
(defn qr-scanner
|
||||
[]
|
||||
(reagent/create-class
|
||||
{:display-name "qr-scanner"
|
||||
:component-did-mount #(rn/hw-back-add-listener navigate-back-handler)
|
||||
:component-will-unmount #(rn/hw-back-remove-listener navigate-back-handler)
|
||||
:reagent-render f-qr-scanner}))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
(ns status-im.utils.universal-links.core
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[goog.string :as gstring]
|
||||
[native-module.core :as native-module]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.group-chats.core :as group-chats]
|
||||
@@ -9,7 +8,6 @@
|
||||
[status-im.router.core :as router]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im.wallet.choose-recipient.core :as choose-recipient]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.navigation.events :as navigation]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.ethereum.chain :as chain]
|
||||
@@ -27,27 +25,11 @@
|
||||
(def links
|
||||
{:private-chat "%s/p/%s"
|
||||
:community-requests "%s/cr/%s"
|
||||
:community "%s/c/%s"
|
||||
:community "%s/c#%s"
|
||||
:group-chat "%s/g/%s"
|
||||
:user "%s/u/%s"
|
||||
:user "%s/u#%s"
|
||||
:browse "%s/b/%s"})
|
||||
|
||||
(defn generate-link
|
||||
[link-type domain-type param]
|
||||
(gstring/format (get links link-type)
|
||||
(get domains domain-type)
|
||||
param))
|
||||
|
||||
(defn universal-link?
|
||||
[url]
|
||||
(boolean
|
||||
(re-matches constants/regx-universal-link url)))
|
||||
|
||||
(defn deep-link?
|
||||
[url]
|
||||
(boolean
|
||||
(re-matches constants/regx-deep-link url)))
|
||||
|
||||
(rf/defn handle-browse
|
||||
[cofx {:keys [url]}]
|
||||
(log/info "universal-links: handling browse" url)
|
||||
@@ -220,7 +202,6 @@
|
||||
#_(native-module/start-searching-for-local-pairing-peers
|
||||
#(log/info "[local-pairing] errors from local-pairing-preflight-outbound-check ->" %)))
|
||||
|
||||
|
||||
(defn finalize
|
||||
"Remove event listener for url"
|
||||
[]
|
||||
|
||||
@@ -20,9 +20,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")
|
||||
(is (= (get-in (links/handle-url {:db db} "https://status.app/u#statuse2e")
|
||||
[::router/handle-uri :uri])
|
||||
"https://status.app/u/statuse2e")))))))
|
||||
"https://status.app/u#statuse2e")))))))
|
||||
|
||||
(deftest url-event-listener
|
||||
(testing "the url is not nil"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
(def links
|
||||
{:private-chat "%s/p/%s"
|
||||
:user "%s/u/%s"
|
||||
:user "%s/u#%s"
|
||||
:browse "%s/b/%s"})
|
||||
|
||||
(defn universal-link?
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
(ns status-im2.common.data-store.wallet
|
||||
(:require
|
||||
clojure.set))
|
||||
|
||||
(defn <-rpc
|
||||
[network]
|
||||
(-> network
|
||||
(clojure.set/rename-keys
|
||||
{:Prod :prod
|
||||
:Test :test
|
||||
:isTest :test?
|
||||
:tokenOverrides :token-overrides
|
||||
:rpcUrl :rpc-url
|
||||
:chainColor :chain-color
|
||||
:chainName :chain-name
|
||||
:nativeCurrencyDecimals :native-currency-decimals
|
||||
:relatedChainId :related-chain-id
|
||||
:shortName :short-name
|
||||
:chainId :chain-id
|
||||
:originalFallbackURL :original-fallback-url
|
||||
:originalRpcUrl :original-rpc-url
|
||||
:fallbackURL :fallback-url
|
||||
:blockExplorerUrl :block-explorer-url
|
||||
:nativeCurrencySymbol :native-currency-symbol
|
||||
:nativeCurrencyName :native-currency-symbol})))
|
||||
@@ -36,3 +36,64 @@
|
||||
:qr-size (or 400 (int size))
|
||||
:error-level :highest})]
|
||||
[quo/qr-code (assoc props :qr-image-uri qr-media-server-uri)]))
|
||||
|
||||
(defn- get-network-short-name-url
|
||||
[network]
|
||||
(case network
|
||||
:ethereum "eth:"
|
||||
:optimism "opt:"
|
||||
:arbitrum "arb1:"
|
||||
(str (name network) ":")))
|
||||
|
||||
(defn- get-qr-data-for-wallet-multichain
|
||||
[qr-data networks]
|
||||
(as-> networks $
|
||||
(map get-network-short-name-url $)
|
||||
(apply str $)
|
||||
(str $ qr-data)))
|
||||
|
||||
(defn share-qr-code
|
||||
"Receives the following properties:
|
||||
- type: :profile | :wallet-legacy | :wallet-multichain
|
||||
- qr-data: Text to encode in the QR code
|
||||
- qr-data-label-shown: Text to show below the QR code
|
||||
- on-text-press: Callback for the `qr-data-label-shown` text.
|
||||
- on-text-long-press: Callback for the `qr-data-label-shown` text.
|
||||
- on-share-press: Callback for the share button.
|
||||
- customization-color: Custom color for the QR code component.
|
||||
- unblur-on-android?: [Android only] disables blur for this component.
|
||||
|
||||
Depending on the `type`, different properties are accepted:
|
||||
`:profile`
|
||||
- full-name: User full name.
|
||||
- profile-picture: map ({:source image-source}) or any image source.
|
||||
`:wallet-legacy`
|
||||
- emoji: Emoji in a string to show in the QR code.
|
||||
- on-info-press: Callback for the info icon.
|
||||
- on-legacy-press: Callback for the legacy tab.
|
||||
- on-multichain-press: Callback for the multichain tab.
|
||||
`:wallet-multichain`
|
||||
- networks: A vector of network names (`[:ethereum, :optimism, :my-net, ,,,]`)
|
||||
they will add network prefixes to the address in
|
||||
`qr-data-label-shown` and the QR code generated.
|
||||
- on-settings-press: Callback for the settings button.
|
||||
- emoji: Emoji in a string to show in the QR code.
|
||||
- on-info-press: Callback for the info icon.
|
||||
- on-legacy-press: Callback for the legacy tab.
|
||||
- on-multichain-press: Callback for the multichain tab."
|
||||
[{:keys [qr-data qr-data-label-shown networks]
|
||||
share-qr-type :type
|
||||
:as props}]
|
||||
(let [label (or qr-data-label-shown qr-data)
|
||||
string-to-encode (if (= share-qr-type :wallet-multichain)
|
||||
(get-qr-data-for-wallet-multichain qr-data networks)
|
||||
qr-data)
|
||||
qr-media-server-uri (image-server/get-qr-image-uri-for-any-url
|
||||
{:url string-to-encode
|
||||
:port (rf/sub [:mediaserver/port])
|
||||
:qr-size 600
|
||||
:error-level :highest})]
|
||||
[quo/share-qr-code
|
||||
(assoc props
|
||||
:qr-data label
|
||||
:qr-image-uri qr-media-server-uri)]))
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
:no-contacts-to-invite-light (js/require "../resources/images/ui2/no-contacts-to-invite-light.png")
|
||||
:no-contacts-to-invite-dark (js/require "../resources/images/ui2/no-contacts-to-invite-dark.png")
|
||||
:no-notifications-light (js/require "../resources/images/ui2/no-notifications-light.png")
|
||||
:no-notifications-dark (js/require "../resources/images/ui2/no-notifications-dark.png")})
|
||||
:no-notifications-dark (js/require "../resources/images/ui2/no-notifications-dark.png")
|
||||
:welcome-illustration (js/require "../resources/images/ui2/welcome_illustration.png")})
|
||||
|
||||
(def ui-themed
|
||||
{:no-messages
|
||||
|
||||
@@ -9,13 +9,11 @@
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn view
|
||||
[{:keys [on-enter-password button-label]}]
|
||||
(let [{:keys [key-uid display-name
|
||||
customization-color]
|
||||
:as account} (rf/sub [:profile/multiaccount])
|
||||
{:keys [error processing password]} (rf/sub [:profile/login])
|
||||
sign-in-enabled? (rf/sub [:sign-in-enabled?])
|
||||
profile-picture (multiaccounts/displayed-photo account)]
|
||||
[{:keys [on-enter-password button-label button-icon-left customization-color]}]
|
||||
(let [{:keys [key-uid display-name] :as account} (rf/sub [:profile/multiaccount])
|
||||
{:keys [error processing password]} (rf/sub [:profile/login])
|
||||
sign-in-enabled? (rf/sub [:sign-in-enabled?])
|
||||
profile-picture (multiaccounts/displayed-photo account)]
|
||||
[:<>
|
||||
[rn/view {:style style/enter-password-container}
|
||||
[rn/view
|
||||
@@ -45,7 +43,7 @@
|
||||
:type :primary
|
||||
:customization-color (or customization-color :primary)
|
||||
:accessibility-label :login-button
|
||||
:icon-left :i/reveal
|
||||
:icon-left button-icon-left
|
||||
:disabled? (or (not sign-in-enabled?) processing)
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:set-in [:profile/login :key-uid] key-uid])
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
(defn authorize
|
||||
[{:keys [on-enter-password biometric-auth? on-auth-success on-auth-fail on-close
|
||||
fallback-button-label theme blur?]}]
|
||||
auth-button-label theme blur? customization-color auth-button-icon-left]}]
|
||||
(biometric/get-supported-type
|
||||
(fn [biometric-type]
|
||||
(if (and biometric-auth? biometric-type)
|
||||
@@ -43,8 +43,10 @@
|
||||
:shell? blur?
|
||||
:content (fn []
|
||||
[enter-password/view
|
||||
{:on-enter-password on-enter-password
|
||||
:button-label fallback-button-label}])}]))))))
|
||||
{:customization-color customization-color
|
||||
:on-enter-password on-enter-password
|
||||
:button-icon-left auth-button-icon-left
|
||||
:button-label auth-button-label}])}]))))))
|
||||
|
||||
(defn- view-internal
|
||||
[_]
|
||||
@@ -53,26 +55,31 @@
|
||||
(fn [{:keys [biometric-auth?
|
||||
track-text
|
||||
customization-color
|
||||
fallback-button-label
|
||||
auth-button-label
|
||||
on-enter-password
|
||||
on-auth-success
|
||||
on-auth-fail
|
||||
auth-button-icon-left
|
||||
size
|
||||
theme
|
||||
blur?]}]
|
||||
blur?
|
||||
container-style]}]
|
||||
[rn/view {:style {:flex 1}}
|
||||
[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 {:on-close on-close
|
||||
:auth-button-icon-left auth-button-icon-left
|
||||
:theme theme
|
||||
:blur? blur?
|
||||
:customization-color customization-color
|
||||
:on-enter-password on-enter-password
|
||||
:biometric-auth? biometric-auth?
|
||||
:on-auth-success on-auth-success
|
||||
:on-auth-fail on-auth-fail
|
||||
:fallback-button-label fallback-button-label})
|
||||
:auth-button-label auth-button-label})
|
||||
:track-icon (if biometric-auth? :i/face-id :password)
|
||||
:track-text track-text}]])))
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
(def regx-ens #"^(?=.{5,255}$)([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$")
|
||||
(def regx-address #"^0x[a-fA-F0-9]{40}$")
|
||||
(def regx-address-contains #"(?i)0x[a-fA-F0-9]{40}")
|
||||
(def regx-starts-with-uuid #"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
|
||||
|
||||
(def ^:const dapp-permission-contact-code "contact-code")
|
||||
(def ^:const dapp-permission-web3 "web3")
|
||||
@@ -358,3 +359,17 @@
|
||||
|
||||
(def ^:const ens-action-type-register 1)
|
||||
(def ^:const ens-action-type-set-pub-key 2)
|
||||
|
||||
;; wallet
|
||||
(def ^:const mainnet-chain-id 1)
|
||||
(def ^:const optimism-chain-id 10)
|
||||
(def ^:const arbitrum-chain-id 42161)
|
||||
(def ^:const goerli-chain-id 5)
|
||||
|
||||
(def ^:const mainnet-short-name "eth")
|
||||
(def ^:const optimism-short-name "opt")
|
||||
(def ^:const arbitrum-short-name "arb1")
|
||||
|
||||
(def ^:const mainnet-network-name :ethereum)
|
||||
(def ^:const optimism-network-name :optimism)
|
||||
(def ^:const arbitrum-network-name :arbitrum)
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
(zipmap (repeat nil))))
|
||||
([kv] (-> (init-contact) (merge kv))))
|
||||
|
||||
(def url-regex #"^https?://status.app/u/(.+)")
|
||||
(def url-regex #"^https?://status.app/u(/([a-zA-Z0-9_-]+)(={0,2}))?#(.+)")
|
||||
|
||||
(defn ->id
|
||||
[{:keys [input] :as contact}]
|
||||
(let [trimmed-input (utils.string/safe-trim input)]
|
||||
(->> {:id (if (empty? trimmed-input)
|
||||
nil
|
||||
(if-some [[_ id] (re-matches url-regex trimmed-input)]
|
||||
(if-some [id (last (re-matches url-regex trimmed-input))]
|
||||
id
|
||||
trimmed-input))}
|
||||
(merge contact))))
|
||||
@@ -175,8 +175,11 @@
|
||||
(let [contact (get-in db [:contacts/new-identity])]
|
||||
(when (= (:input contact) input)
|
||||
(let [state (cond
|
||||
(or (string/includes? (:message err) "fallback failed")
|
||||
(string/includes? (:message err) "no such host"))
|
||||
(and (string? err) (string/includes? err "invalid public key"))
|
||||
{:state :invalid :msg :t/not-a-chatkey}
|
||||
(and (string? (:message err))
|
||||
(or (string/includes? (:message err) "fallback failed")
|
||||
(string/includes? (:message err) "no such host")))
|
||||
{:state :invalid :msg :t/lost-connection}
|
||||
:else {:state :invalid})]
|
||||
{:db (assoc db :contacts/new-identity (merge contact state))}))))
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
(def ckey "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA")
|
||||
(def ens "esep")
|
||||
(def ens-stateofus-eth (str ens ".stateofus.eth"))
|
||||
(def link-ckey (str "https://status.app/u/" ckey))
|
||||
(def link-ens (str "https://status.app/u/" ens))
|
||||
(def link-ckey (str "https://status.app/u#" ckey))
|
||||
(def link-ckey-with-encoded-data (str "https://status.app/u/CwSACgcKBVBhdmxvAw==#" ckey))
|
||||
(def link-ens (str "https://status.app/u#" ens))
|
||||
|
||||
;;; unit tests (no app-db involved)
|
||||
|
||||
@@ -22,56 +23,61 @@
|
||||
:input i}))
|
||||
(events/init-contact e))
|
||||
|
||||
"" {:user-public-key user-ukey
|
||||
:input ""
|
||||
:type :empty
|
||||
:state :empty}
|
||||
"" {:user-public-key user-ukey
|
||||
:input ""
|
||||
:type :empty
|
||||
:state :empty}
|
||||
|
||||
" " {:user-public-key user-ukey
|
||||
:input " "
|
||||
:type :empty
|
||||
:state :empty}
|
||||
" " {:user-public-key user-ukey
|
||||
:input " "
|
||||
:type :empty
|
||||
:state :empty}
|
||||
|
||||
ukey {:user-public-key user-ukey
|
||||
:input ukey
|
||||
:id ukey
|
||||
:type :public-key
|
||||
:public-key ukey
|
||||
:state :invalid
|
||||
:msg :t/not-a-chatkey}
|
||||
ukey {:user-public-key user-ukey
|
||||
:input ukey
|
||||
:id ukey
|
||||
:type :public-key
|
||||
:public-key ukey
|
||||
:state :invalid
|
||||
:msg :t/not-a-chatkey}
|
||||
|
||||
ens {:user-public-key user-ukey
|
||||
:input ens
|
||||
:id ens
|
||||
:type :ens
|
||||
:ens ens-stateofus-eth
|
||||
:state :resolve-ens}
|
||||
ens {:user-public-key user-ukey
|
||||
:input ens
|
||||
:id ens
|
||||
:type :ens
|
||||
:ens ens-stateofus-eth
|
||||
:state :resolve-ens}
|
||||
|
||||
(str " " ens) {:user-public-key user-ukey
|
||||
:input (str " " ens)
|
||||
:id ens
|
||||
:type :ens
|
||||
:ens ens-stateofus-eth
|
||||
:state :resolve-ens}
|
||||
(str " " ens) {:user-public-key user-ukey
|
||||
:input (str " " ens)
|
||||
:id ens
|
||||
:type :ens
|
||||
:ens ens-stateofus-eth
|
||||
:state :resolve-ens}
|
||||
|
||||
ckey {:user-public-key user-ukey
|
||||
:input ckey
|
||||
:id ckey
|
||||
:type :compressed-key
|
||||
:state :decompress-key}
|
||||
ckey {:user-public-key user-ukey
|
||||
:input ckey
|
||||
:id ckey
|
||||
:type :compressed-key
|
||||
:state :decompress-key}
|
||||
|
||||
link-ckey {:user-public-key user-ukey
|
||||
:input link-ckey
|
||||
:id ckey
|
||||
:type :compressed-key
|
||||
:state :decompress-key}
|
||||
link-ckey {:user-public-key user-ukey
|
||||
:input link-ckey
|
||||
:id ckey
|
||||
:type :compressed-key
|
||||
:state :decompress-key}
|
||||
link-ckey-with-encoded-data {:user-public-key user-ukey
|
||||
:input link-ckey-with-encoded-data
|
||||
:id ckey
|
||||
:type :compressed-key
|
||||
:state :decompress-key}
|
||||
|
||||
link-ens {:user-public-key user-ukey
|
||||
:input link-ens
|
||||
:id ens
|
||||
:type :ens
|
||||
:ens ens-stateofus-eth
|
||||
:state :resolve-ens}))
|
||||
link-ens {:user-public-key user-ukey
|
||||
:input link-ens
|
||||
:id ens
|
||||
:type :ens
|
||||
:ens ens-stateofus-eth
|
||||
:state :resolve-ens}))
|
||||
|
||||
;;; event handler tests (no callbacks)
|
||||
|
||||
|
||||
@@ -80,11 +80,10 @@
|
||||
:button (when empty-input?
|
||||
{:on-press paste-on-input
|
||||
:text (i18n/label :t/paste)})
|
||||
;; NOTE: `scanned` has priority over `@input-value`, we clean it when the input
|
||||
;; is updated so that it's `nil` and `@input-value` is shown.
|
||||
;; To fastly clean it, we use `dispatch-sync`.
|
||||
;; This call could be avoided if `::qr-scanner/scan-code` were able to receive a
|
||||
;; callback function, not only a re-frame event as callback.
|
||||
;; NOTE: `scanned` has priority over `@input-value`, we clean it when the input is updated
|
||||
;; so that it's `nil` and `@input-value` is shown. To fastly clean it, we use
|
||||
;; `dispatch-sync`. This call could be avoided if `::qr-scanner/scan-code` were able to
|
||||
;; receive a callback function, not only a re-frame event as callback.
|
||||
:value (or scanned @input-value)
|
||||
:on-change-text (fn [new-text]
|
||||
(reset! input-value new-text)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn send-message
|
||||
"Minimize composer, animate-out background overlay, clear input and flush state"
|
||||
[{:keys [sending-images? sending-links?]}
|
||||
{:keys [text-value focused? maximized?]}
|
||||
{:keys [height saved-height last-height opacity background-y container-opacity]}
|
||||
@@ -47,6 +48,7 @@
|
||||
(let [{:keys [text-value]} state
|
||||
customization-color (rf/sub [:profile/customization-color])]
|
||||
(rn/use-effect (fn []
|
||||
;; Handle send button opacity animation and z-index when input content changes
|
||||
(if (or (seq @text-value) images?)
|
||||
(when (or (not= @z-index 1) (not= (reanimated/get-shared-value btn-opacity) 1))
|
||||
(reset! z-index 1)
|
||||
@@ -172,11 +174,12 @@
|
||||
[edit]
|
||||
(let [images-count (count (vals (rf/sub [:chats/sending-image])))]
|
||||
[quo/composer-button
|
||||
{:on-press (if edit
|
||||
#(js/alert "This feature is temporarily unavailable in edit mode.")
|
||||
#(go-to-camera images-count))
|
||||
:icon :i/camera
|
||||
:container-style {:margin-right 12}}]))
|
||||
{:on-press (if edit
|
||||
#(js/alert "This feature is temporarily unavailable in edit mode.")
|
||||
#(go-to-camera images-count))
|
||||
:accessibility-label :camera-button
|
||||
:icon :i/camera
|
||||
:container-style {:margin-right 12}}]))
|
||||
|
||||
|
||||
(defn open-photo-selector
|
||||
|
||||
@@ -17,9 +17,15 @@
|
||||
|
||||
(def ^:const empty-opacity 0.7)
|
||||
|
||||
(def ^:const images-container-height 76)
|
||||
(def ^:const images-padding-top 12)
|
||||
(def ^:const images-padding-bottom 8)
|
||||
(def ^:const images-container-height
|
||||
(+ actions-container-height images-padding-top images-padding-bottom))
|
||||
|
||||
(def ^:const links-container-height 76)
|
||||
(def ^:const links-padding-top 12)
|
||||
(def ^:const links-padding-bottom 8)
|
||||
(def ^:const links-container-height
|
||||
(+ actions-container-height links-padding-top links-padding-bottom))
|
||||
|
||||
(def ^:const reply-container-height 32)
|
||||
|
||||
|
||||
@@ -112,7 +112,10 @@
|
||||
(let [edit-text (get-in edit [:content :text])
|
||||
text-value-count (count @text-value)]
|
||||
(when (and edit @input-ref)
|
||||
(.focus ^js @input-ref)
|
||||
;; A small setTimeout is necessary to ensure the statement is enqueued and will get executed
|
||||
;; ASAP.
|
||||
;; https://github.com/software-mansion/react-native-screens/issues/472
|
||||
(js/setTimeout #(.focus ^js @input-ref) 250)
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text edit-text}))
|
||||
(reset! text-value edit-text)
|
||||
(reset! saved-cursor-position (if (zero? text-value-count)
|
||||
@@ -129,7 +132,7 @@
|
||||
(when reply
|
||||
(reanimated/animate container-opacity 1))
|
||||
(when (and reply @input-ref)
|
||||
(.focus ^js @input-ref)))
|
||||
(js/setTimeout #(.focus ^js @input-ref) 250)))
|
||||
[(:message-id reply)]))
|
||||
|
||||
(defn edit-mentions
|
||||
@@ -165,30 +168,23 @@
|
||||
(defn link-previews
|
||||
[{:keys [sending-links?]}
|
||||
{:keys [text-value maximized?]}
|
||||
{:keys [height saved-height last-height]}
|
||||
{:keys [height saved-height]}
|
||||
{:keys [link-previews?]}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(if-not @maximized?
|
||||
(let [value (if link-previews?
|
||||
constants/links-container-height
|
||||
(- constants/links-container-height))]
|
||||
(when (not= @sending-links? link-previews?)
|
||||
(reanimated/animate height (+ (reanimated/get-shared-value saved-height) value))
|
||||
(reanimated/set-shared-value saved-height
|
||||
(+ (reanimated/get-shared-value saved-height) value))
|
||||
(reanimated/set-shared-value last-height
|
||||
(+ (reanimated/get-shared-value last-height) value))))
|
||||
(when (not= @sending-links? link-previews?)
|
||||
(reanimated/animate height (reanimated/get-shared-value saved-height)))
|
||||
(let [curr-text @text-value]
|
||||
(reset! text-value (str @text-value " "))
|
||||
(js/setTimeout #(reset! text-value curr-text) 10)))
|
||||
(js/setTimeout #(reset! text-value curr-text) 100)))
|
||||
(reset! sending-links? link-previews?))
|
||||
[link-previews?]))
|
||||
|
||||
(defn use-images
|
||||
[{:keys [sending-images? input-ref]}
|
||||
{:keys [text-value maximized?]}
|
||||
{:keys [container-opacity height saved-height last-height]}
|
||||
{:keys [container-opacity height saved-height]}
|
||||
{:keys [images]}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
@@ -197,18 +193,11 @@
|
||||
(when (and (not @sending-images?) (seq images) @input-ref)
|
||||
(.focus ^js @input-ref))
|
||||
(if-not @maximized?
|
||||
(let [value (if (seq images)
|
||||
constants/images-container-height
|
||||
(- constants/images-container-height))]
|
||||
(when (not= @sending-images? (boolean (seq images)))
|
||||
(reanimated/animate height (+ (reanimated/get-shared-value saved-height) value))
|
||||
(reanimated/set-shared-value saved-height
|
||||
(+ (reanimated/get-shared-value saved-height) value))
|
||||
(reanimated/set-shared-value last-height
|
||||
(+ (reanimated/get-shared-value last-height) value))))
|
||||
(when (not= @sending-images? (boolean (seq images)))
|
||||
(reanimated/animate height (reanimated/get-shared-value saved-height)))
|
||||
(let [curr-text @text-value]
|
||||
(reset! text-value (str @text-value " "))
|
||||
(js/setTimeout #(reset! text-value curr-text) 10)))
|
||||
(js/setTimeout #(reset! text-value curr-text) 100)))
|
||||
(reset! sending-images? (boolean (seq images))))
|
||||
[(boolean (seq images))]))
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
[{:keys [input-ref] :as props}
|
||||
{:keys [gesture-enabled?] :as state}
|
||||
{:keys [height saved-height last-height opacity background-y container-opacity] :as animations}
|
||||
{:keys [images link-previews?]}
|
||||
{:keys [max-height lines] :as dimensions}
|
||||
keyboard-shown]
|
||||
(let [expanding? (atom true)
|
||||
@@ -82,7 +81,7 @@
|
||||
(gesture/on-update
|
||||
(fn [event]
|
||||
(let [translation (oops/oget event "translationY")
|
||||
min-height (utils/get-min-height lines images link-previews?)
|
||||
min-height (utils/get-min-height lines)
|
||||
new-height (- (reanimated/get-shared-value saved-height) translation)
|
||||
bounded-height (utils.number/value-in-range new-height min-height max-height)]
|
||||
(when keyboard-shown
|
||||
|
||||
@@ -4,24 +4,27 @@
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]))
|
||||
|
||||
(defn top-gradient-style
|
||||
[opacity z-index]
|
||||
(defn- top-gradient-style
|
||||
[opacity z-index showing-extra-space?]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity}
|
||||
{:height 80
|
||||
{:height (when (pos-int? z-index) 80)
|
||||
:position :absolute
|
||||
:z-index z-index
|
||||
:top 0
|
||||
:top (+ constants/bar-container-height
|
||||
(if showing-extra-space?
|
||||
constants/edit-container-height
|
||||
0))
|
||||
:left 0
|
||||
:right 0}))
|
||||
|
||||
(defn top-gradient
|
||||
[opacity z-index]
|
||||
[opacity z-index showing-extra-space?]
|
||||
{:colors [(colors/theme-colors colors/white-opa-0 colors/neutral-95-opa-0)
|
||||
(colors/theme-colors colors/white colors/neutral-95)]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (top-gradient-style opacity z-index)})
|
||||
:style (top-gradient-style opacity z-index showing-extra-space?)})
|
||||
|
||||
(def bottom-gradient-style
|
||||
{:height constants/line-height
|
||||
|
||||
@@ -3,22 +3,25 @@
|
||||
[react-native.core :as rn]
|
||||
[react-native.linear-gradient :as linear-gradient]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.composer.gradients.style :as style]))
|
||||
|
||||
[status-im2.contexts.chat.composer.gradients.style :as style]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn f-view
|
||||
[{:keys [input-ref]}
|
||||
{:keys [gradient-z-index]}
|
||||
{:keys [gradient-opacity]}
|
||||
show-bottom-gradient?]
|
||||
[:<>
|
||||
[reanimated/linear-gradient (style/top-gradient gradient-opacity @gradient-z-index)]
|
||||
(when show-bottom-gradient?
|
||||
[rn/touchable-without-feedback
|
||||
{:on-press #(when @input-ref (.focus ^js @input-ref))
|
||||
:accessibility-label :bottom-gradient}
|
||||
[linear-gradient/linear-gradient (style/bottom-gradient)]])])
|
||||
(let [showing-extra-space? (boolean (or (rf/sub [:chats/edit-message])
|
||||
(rf/sub [:chats/reply-message])))]
|
||||
[:<>
|
||||
[reanimated/linear-gradient
|
||||
(style/top-gradient gradient-opacity @gradient-z-index showing-extra-space?)]
|
||||
(when show-bottom-gradient?
|
||||
[rn/pressable
|
||||
{:on-press #(when @input-ref (.focus ^js @input-ref))
|
||||
:accessibility-label :bottom-gradient}
|
||||
[linear-gradient/linear-gradient (style/bottom-gradient)]])]))
|
||||
|
||||
(defn view
|
||||
[props state animations show-bottom-gradient?]
|
||||
[:f> f-view props state animations show-bottom-gradient?])
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn focus
|
||||
"Animate to the `saved-height`, display background-overlay if needed, and set cursor position"
|
||||
[{:keys [input-ref] :as props}
|
||||
{:keys [text-value focused? lock-selection? saved-cursor-position]}
|
||||
{:keys [height saved-height last-height opacity background-y container-opacity]
|
||||
@@ -36,6 +37,7 @@
|
||||
(reset! show-floating-scroll-down-button? false))
|
||||
|
||||
(defn blur
|
||||
"Save the current height, minimize the composer, animate-out the background, and save cursor position"
|
||||
[{:keys [text-value focused? lock-selection? cursor-position saved-cursor-position gradient-z-index
|
||||
maximized? recording?]}
|
||||
{:keys [height saved-height last-height gradient-opacity container-opacity opacity background-y]}
|
||||
@@ -43,14 +45,12 @@
|
||||
{:keys [images link-previews? reply]}]
|
||||
(when-not @recording?
|
||||
(let [lines (utils/calc-lines (- @content-height constants/extra-content-offset))
|
||||
min-height (utils/get-min-height lines images link-previews?)
|
||||
min-height (utils/get-min-height lines)
|
||||
reopen-height (utils/calc-reopen-height text-value
|
||||
min-height
|
||||
max-height
|
||||
content-height
|
||||
saved-height
|
||||
images
|
||||
link-previews?)]
|
||||
saved-height)]
|
||||
(reset! focused? false)
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(reanimated/set-shared-value last-height reopen-height)
|
||||
@@ -69,24 +69,23 @@
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])))))
|
||||
|
||||
(defn content-size-change
|
||||
"Save new text height, expand composer if possible, show background overlay if needed"
|
||||
[event
|
||||
{:keys [maximized? lock-layout? text-value]}
|
||||
{:keys [height saved-height opacity background-y]}
|
||||
{:keys [images link-previews?]}
|
||||
{:keys [content-height window-height max-height]}
|
||||
keyboard-shown]
|
||||
(when keyboard-shown
|
||||
(let [event-size (oops/oget event "nativeEvent.contentSize.height")
|
||||
content-size (+ event-size constants/extra-content-offset)
|
||||
lines (utils/calc-lines event-size)
|
||||
content-size (if (or (= lines 1) (empty? @text-value))
|
||||
constants/input-height
|
||||
(if (= lines 2) constants/multiline-minimized-height content-size))
|
||||
new-height (utils.number/value-in-range content-size
|
||||
constants/input-height
|
||||
max-height)
|
||||
bottom-content-height (utils/calc-bottom-content-height images link-previews?)
|
||||
new-height (min (+ new-height bottom-content-height) max-height)]
|
||||
(let [event-size (oops/oget event "nativeEvent.contentSize.height")
|
||||
content-size (+ event-size constants/extra-content-offset)
|
||||
lines (utils/calc-lines event-size)
|
||||
content-size (if (or (= lines 1) (empty? @text-value))
|
||||
constants/input-height
|
||||
(if (= lines 2) constants/multiline-minimized-height content-size))
|
||||
new-height (utils.number/value-in-range content-size
|
||||
constants/input-height
|
||||
max-height)
|
||||
new-height (min new-height max-height)]
|
||||
(reset! content-height content-size)
|
||||
(when (utils/update-height? content-size height max-height maximized?)
|
||||
(reanimated/animate height new-height)
|
||||
@@ -105,6 +104,7 @@
|
||||
(reset! lock-layout? (> lines 2)))))
|
||||
|
||||
(defn scroll
|
||||
"Hide or show top gradient while scroll"
|
||||
[event
|
||||
{:keys [scroll-y]}
|
||||
{:keys [gradient-z-index focused?]}
|
||||
@@ -120,12 +120,10 @@
|
||||
(js/setTimeout #(reset! gradient-z-index 0) 300))))
|
||||
|
||||
(defn change-text
|
||||
"Update `text-value`, update cursor selection, find links, find mentions"
|
||||
[text
|
||||
{:keys [input-ref record-reset-fn]}
|
||||
{:keys [text-value cursor-position recording?]}]
|
||||
(debounce/debounce-and-dispatch [:link-preview/unfurl-urls text]
|
||||
constants/unfurl-debounce-ms)
|
||||
|
||||
(reset! text-value text)
|
||||
(reagent/next-tick #(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
@@ -135,11 +133,14 @@
|
||||
(@record-reset-fn)
|
||||
(reset! recording? false))
|
||||
(rf/dispatch [:chat.ui/set-chat-input-text text])
|
||||
(debounce/debounce-and-dispatch [:link-preview/unfurl-urls text]
|
||||
constants/unfurl-debounce-ms)
|
||||
(if (string/ends-with? text "@")
|
||||
(rf/dispatch [:mention/on-change-text text])
|
||||
(debounce/debounce-and-dispatch [:mention/on-change-text text] 300)))
|
||||
|
||||
(defn selection-change
|
||||
"A method that handles our custom selector for `B I U`"
|
||||
[event
|
||||
{:keys [input-ref selection-event selection-manager]}
|
||||
{:keys [lock-selection? cursor-position first-level? menu-items]}]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
(ns status-im2.contexts.chat.composer.images.style
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]))
|
||||
[quo.foundations.colors :as colors]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]))
|
||||
|
||||
(def image-container
|
||||
{:padding-top 12
|
||||
:padding-bottom 8
|
||||
{:padding-top constants/images-padding-top
|
||||
:padding-bottom constants/images-padding-bottom
|
||||
:padding-right 12})
|
||||
|
||||
(defn remove-photo-container
|
||||
@@ -32,4 +33,3 @@
|
||||
{:width 56
|
||||
:height 56
|
||||
:border-radius 12})
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
(async-storage/set-item! :kb-default-height (str height)))))
|
||||
|
||||
(defn handle-emoji-kb-ios
|
||||
"Opening emoji KB on iOS while maximized will cause a flicker up and down. This method handles that."
|
||||
[event
|
||||
{:keys [emoji-kb-extra-height]}
|
||||
{:keys [text-value]}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
(ns status-im2.contexts.chat.composer.link-preview.style)
|
||||
(ns status-im2.contexts.chat.composer.link-preview.style
|
||||
(:require [status-im2.contexts.chat.composer.constants :as constants]))
|
||||
|
||||
(def padding-horizontal 20)
|
||||
(def preview-list-padding-top 12)
|
||||
(def preview-list-padding-bottom 8)
|
||||
(def preview-height 56)
|
||||
|
||||
(def preview-list
|
||||
{:padding-top preview-list-padding-top
|
||||
:padding-bottom preview-list-padding-bottom
|
||||
{:padding-top constants/links-padding-top
|
||||
:padding-bottom constants/links-padding-bottom
|
||||
:margin-horizontal (- padding-horizontal)
|
||||
;; Keep a high index, otherwise the parent gesture detector used by the
|
||||
;; composer grabs the initiating gesture event.
|
||||
|
||||
@@ -31,18 +31,19 @@
|
||||
|
||||
(defn- f-view
|
||||
[suggestions-atom props state animations max-height cursor-pos images link-previews? reply edit]
|
||||
(let [suggestions (rf/sub [:chat/mention-suggestions])
|
||||
opacity (reanimated/use-shared-value (if (seq suggestions) 1 0))
|
||||
size (count suggestions)
|
||||
data {:keyboard-height @(:kb-height state)
|
||||
:insets (safe-area/get-insets)
|
||||
:curr-height (reanimated/get-shared-value (:height animations))
|
||||
:window-height (:height (rn/get-window))
|
||||
:images images
|
||||
:link-previews? link-previews?
|
||||
:reply reply
|
||||
:edit edit}
|
||||
mentions-pos (utils/calc-suggestions-position cursor-pos max-height size state data)]
|
||||
(let [suggestions (rf/sub [:chat/mention-suggestions])
|
||||
opacity (reanimated/use-shared-value (if (seq suggestions) 1 0))
|
||||
size (count suggestions)
|
||||
data {:keyboard-height @(:kb-height state)
|
||||
:insets (safe-area/get-insets)
|
||||
:curr-height (reanimated/get-shared-value (:height animations))
|
||||
:window-height (:height (rn/get-window))
|
||||
:images images
|
||||
:link-previews? link-previews?
|
||||
:reply reply
|
||||
:edit edit}
|
||||
mentions-pos
|
||||
(utils/calc-suggestions-position cursor-pos max-height size state data images link-previews?)]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(if (seq suggestions)
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]))
|
||||
|
||||
(def border-top-radius 20)
|
||||
|
||||
(defn shadow
|
||||
[focused? theme]
|
||||
(if platform/ios?
|
||||
@@ -27,8 +29,8 @@
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity container-opacity}
|
||||
(merge
|
||||
{:border-top-left-radius 20
|
||||
:border-top-right-radius 20
|
||||
{:border-top-left-radius border-top-radius
|
||||
:border-top-right-radius border-top-radius
|
||||
:padding-horizontal 20
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-95 theme)
|
||||
:z-index 3
|
||||
@@ -55,7 +57,8 @@
|
||||
[height max-height]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:height height}
|
||||
{:max-height max-height}))
|
||||
{:max-height max-height
|
||||
:z-index 1}))
|
||||
|
||||
(defn input-view
|
||||
[{:keys [recording?]}]
|
||||
@@ -68,19 +71,17 @@
|
||||
(defn input-text
|
||||
[{:keys [saved-emoji-kb-extra-height]}
|
||||
{:keys [focused? maximized?]}
|
||||
{:keys [link-previews? images]}
|
||||
{:keys [max-height theme]}]
|
||||
(merge typography/paragraph-1
|
||||
{:color (colors/theme-colors :black :white theme)
|
||||
:text-align-vertical :top
|
||||
:position (if @saved-emoji-kb-extra-height :relative :absolute)
|
||||
:top 0
|
||||
:left 0
|
||||
:right (when (or focused? platform/ios?) 0)
|
||||
:max-height (- max-height
|
||||
(if link-previews? constants/links-container-height 0)
|
||||
(if (seq images) constants/images-container-height 0))
|
||||
:padding-bottom (when @maximized? 0)}))
|
||||
(assoc typography/paragraph-1
|
||||
:color (colors/theme-colors :black :white theme)
|
||||
:text-align-vertical :top
|
||||
:position (if @saved-emoji-kb-extra-height :relative :absolute)
|
||||
:top 0
|
||||
:left 0
|
||||
:right (when (or focused? platform/ios?) 0)
|
||||
:max-height max-height
|
||||
:padding-bottom (when @maximized? 0)))
|
||||
|
||||
(defn background
|
||||
[opacity background-y window-height]
|
||||
(reanimated/apply-animations-to-style
|
||||
@@ -102,8 +103,8 @@
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0
|
||||
:border-top-right-radius 20
|
||||
:border-top-left-radius 20
|
||||
:border-top-right-radius border-top-radius
|
||||
:border-top-left-radius border-top-radius
|
||||
:overflow :hidden}))
|
||||
|
||||
(defn blur-view
|
||||
|
||||
@@ -57,43 +57,40 @@
|
||||
|
||||
(defn calc-top-content-height
|
||||
[reply? edit?]
|
||||
(let [height (if reply? constants/reply-container-height 0)
|
||||
height (if edit? (+ height constants/edit-container-height) height)]
|
||||
height))
|
||||
(cond-> 0
|
||||
reply? (+ constants/reply-container-height)
|
||||
edit? (+ constants/edit-container-height)))
|
||||
|
||||
(defn calc-bottom-content-height
|
||||
[images link-previews?]
|
||||
(let [height (if (seq images) constants/images-container-height 0)
|
||||
height (if link-previews? (+ height constants/links-container-height) height)]
|
||||
height))
|
||||
(cond-> 0
|
||||
(seq images) (+ constants/images-container-height)
|
||||
link-previews? (+ constants/links-container-height)))
|
||||
|
||||
(defn calc-reopen-height
|
||||
[text-value min-height max-height content-height saved-height images link-previews?]
|
||||
[text-value min-height max-height content-height saved-height]
|
||||
(if (empty? @text-value)
|
||||
min-height
|
||||
(let [bottom-content-height (calc-bottom-content-height images link-previews?)
|
||||
input-height (min (+ @content-height bottom-content-height)
|
||||
(reanimated/get-shared-value saved-height))]
|
||||
(let [input-height (min @content-height
|
||||
(reanimated/get-shared-value saved-height))]
|
||||
(min max-height input-height))))
|
||||
|
||||
(defn get-min-height
|
||||
[lines images link-previews?]
|
||||
(let [input-height (if (> lines 1)
|
||||
constants/multiline-minimized-height
|
||||
constants/input-height)
|
||||
bottom-content-height (calc-bottom-content-height images link-previews?)]
|
||||
(+ input-height bottom-content-height)))
|
||||
[lines]
|
||||
(if (> lines 1)
|
||||
constants/multiline-minimized-height
|
||||
constants/input-height))
|
||||
|
||||
(defn calc-max-height
|
||||
[{:keys [reply edit]} window-height kb-height insets]
|
||||
(let [margin-top (if platform/ios? (:top insets) (+ 10 (:top insets)))
|
||||
max-height (- window-height
|
||||
margin-top
|
||||
kb-height
|
||||
constants/bar-container-height
|
||||
constants/actions-container-height)
|
||||
max-height (- max-height (calc-top-content-height reply edit))]
|
||||
max-height))
|
||||
[{:keys [reply edit images link-previews?]} window-height kb-height insets]
|
||||
(let [margin-top (if platform/ios? (:top insets) (+ 10 (:top insets)))]
|
||||
(- window-height
|
||||
margin-top
|
||||
kb-height
|
||||
constants/bar-container-height
|
||||
constants/actions-container-height
|
||||
(calc-top-content-height reply edit)
|
||||
(calc-bottom-content-height images link-previews?))))
|
||||
|
||||
(defn empty-input?
|
||||
[text images link-previews? reply? audio?]
|
||||
@@ -124,21 +121,12 @@
|
||||
sub-text-lines-in-view (- sub-text-lines scrolled-lines)]
|
||||
(* sub-text-lines-in-view constants/line-height)))
|
||||
|
||||
(defn calc-shell-neg-y
|
||||
[insets maximized? extra-height]
|
||||
(let [padding 12
|
||||
neg-y (if @maximized? -50 0)]
|
||||
(- (+ constants/bar-container-height
|
||||
constants/actions-container-height
|
||||
(:bottom insets)
|
||||
padding
|
||||
extra-height
|
||||
neg-y))))
|
||||
|
||||
(defn calc-suggestions-position
|
||||
[cursor-pos max-height size
|
||||
{:keys [maximized?]}
|
||||
{:keys [insets curr-height window-height keyboard-height reply edit]}]
|
||||
{:keys [insets curr-height window-height keyboard-height reply edit]}
|
||||
images
|
||||
link-previews?]
|
||||
(let [base (+ constants/composer-default-height (:bottom insets) 8)
|
||||
base (+ base (- curr-height constants/input-height))
|
||||
base (+ base (calc-top-content-height reply edit))
|
||||
@@ -152,10 +140,11 @@
|
||||
(+ constants/actions-container-height (:bottom insets))
|
||||
(+ constants/actions-container-height (:bottom insets) (- max-height cursor-pos) 18))
|
||||
(if (< (+ base container-height) view-height)
|
||||
base
|
||||
(let [bottom-content-height (calc-bottom-content-height images link-previews?)]
|
||||
(+ base bottom-content-height))
|
||||
(+ constants/actions-container-height (:bottom insets) (- curr-height cursor-pos) 18)))))
|
||||
|
||||
(defn init-props
|
||||
(defn init-non-reactive-state
|
||||
[]
|
||||
{:input-ref (atom nil)
|
||||
:selectable-input-ref (atom nil)
|
||||
@@ -171,7 +160,7 @@
|
||||
:selection-event (atom nil)
|
||||
:selection-manager (rn/selectable-text-input-manager)})
|
||||
|
||||
(defn init-state
|
||||
(defn init-reactive-state
|
||||
[]
|
||||
{:text-value (reagent/atom "")
|
||||
:cursor-position (reagent/atom 0)
|
||||
|
||||
@@ -36,15 +36,19 @@
|
||||
theme]} props state]
|
||||
(let [{:keys [chat-screen-loaded?]
|
||||
:as subscriptions} (utils/init-subs)
|
||||
content-height (reagent/atom (or (:input-content-height
|
||||
content-height (reagent/atom (or (:input-content-height ; Actual text height
|
||||
subscriptions)
|
||||
constants/input-height))
|
||||
{:keys [keyboard-shown]} (hooks/use-keyboard)
|
||||
max-height (utils/calc-max-height subscriptions
|
||||
max-height (utils/calc-max-height subscriptions ; Max allowed height for the
|
||||
; composer view
|
||||
window-height
|
||||
@(:kb-height state)
|
||||
insets)
|
||||
lines (utils/calc-lines (- @content-height constants/extra-content-offset))
|
||||
lines (utils/calc-lines (- @content-height constants/extra-content-offset)) ; Current
|
||||
; lines
|
||||
; count
|
||||
;; Maximum number of lines that can be displayed when composer in maximized
|
||||
max-lines (utils/calc-lines max-height)
|
||||
animations (utils/init-animations
|
||||
subscriptions
|
||||
@@ -59,6 +63,7 @@
|
||||
:lines lines
|
||||
:max-lines max-lines}
|
||||
show-bottom-gradient? (utils/show-bottom-gradient? state dimensions)
|
||||
;; Cursor position, needed to determine where to display the mentions view
|
||||
cursor-pos (utils/cursor-y-position-relative-to-container
|
||||
props
|
||||
state)]
|
||||
@@ -86,7 +91,7 @@
|
||||
[sub-view/shell-button state scroll-to-bottom-fn show-floating-scroll-down-button?]
|
||||
[gesture/gesture-detector
|
||||
{:gesture
|
||||
(drag-gesture/drag-gesture props state animations subscriptions dimensions keyboard-shown)}
|
||||
(drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
|
||||
[reanimated/view
|
||||
{:style (style/sheet-container insets state animations theme)
|
||||
:on-layout #(handler/layout % state blur-height)}
|
||||
@@ -97,7 +102,9 @@
|
||||
[edit/view state]])
|
||||
[reanimated/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:on-press (when @(:input-ref props) #(.focus ^js @(:input-ref props)))
|
||||
:on-press (fn []
|
||||
(when-let [ref @(:input-ref props)]
|
||||
(.focus ^js ref)))
|
||||
:style (style/input-container (:height animations) max-height)
|
||||
:accessibility-label :message-input-container}
|
||||
[rn/selectable-text-input
|
||||
@@ -113,7 +120,6 @@
|
||||
:on-content-size-change #(handler/content-size-change %
|
||||
state
|
||||
animations
|
||||
subscriptions
|
||||
dimensions
|
||||
(or keyboard-shown
|
||||
(:edit subscriptions)))
|
||||
@@ -122,23 +128,21 @@
|
||||
:on-selection-change #(handler/selection-change % props state)
|
||||
:on-selection #(selection/on-selection % props state)
|
||||
:keyboard-appearance (quo.theme/theme-value :light :dark)
|
||||
:max-height max-height
|
||||
:max-font-size-multiplier 1
|
||||
:multiline true
|
||||
:placeholder (i18n/label :t/type-something)
|
||||
:placeholder-text-color (colors/theme-colors colors/neutral-30 colors/neutral-50)
|
||||
:style (style/input-text props
|
||||
state
|
||||
subscriptions
|
||||
{:max-height max-height
|
||||
:theme theme})
|
||||
:max-length constants/max-text-size
|
||||
:accessibility-label :chat-message-input}]]
|
||||
(when chat-screen-loaded?
|
||||
[:<>
|
||||
[gradients/view props state animations show-bottom-gradient?]
|
||||
[link-preview/view]
|
||||
[images/images-list]])]
|
||||
:accessibility-label :chat-message-input}]]]
|
||||
(when chat-screen-loaded?
|
||||
[:<>
|
||||
[gradients/view props state animations show-bottom-gradient?]
|
||||
[link-preview/view]
|
||||
[images/images-list]])
|
||||
[:f> actions/view props state animations window-height insets scroll-to-bottom-fn
|
||||
subscriptions]]]]]))
|
||||
|
||||
@@ -147,7 +151,7 @@
|
||||
(let [window-height (:height (rn/get-window))
|
||||
theme (quo.theme/use-theme-value)
|
||||
opacity (reanimated/use-shared-value 0)
|
||||
background-y (reanimated/use-shared-value (- window-height))
|
||||
background-y (reanimated/use-shared-value (- window-height)) ; Y position of background overlay
|
||||
blur-height (reanimated/use-shared-value (+ constants/composer-default-height
|
||||
(:bottom insets)))
|
||||
extra-params {:insets insets
|
||||
@@ -158,10 +162,11 @@
|
||||
:opacity opacity
|
||||
:background-y background-y
|
||||
:theme theme}
|
||||
props (utils/init-props)
|
||||
state (utils/init-state)]
|
||||
props (utils/init-non-reactive-state)
|
||||
state (utils/init-reactive-state)]
|
||||
[rn/view (when platform/ios? {:style {:z-index 1}})
|
||||
[reanimated/view {:style (style/background opacity background-y window-height)}]
|
||||
[reanimated/view {:style (style/background opacity background-y window-height)}] ; background
|
||||
; overlay
|
||||
[sub-view/blur-view
|
||||
{:layout-height blur-height
|
||||
:focused? (:focused? state)
|
||||
|
||||
@@ -49,15 +49,15 @@
|
||||
[item index _ render-data]
|
||||
[:f> f-small-image item index _ render-data])
|
||||
|
||||
|
||||
(defn bottom-view
|
||||
[messages index scroll-index insets animations derived item-width props state]
|
||||
[messages index scroll-index insets animations derived item-width props state transparent?]
|
||||
(let [padding-horizontal (- (/ item-width 2) (/ c/focused-image-size 2))]
|
||||
[reanimated/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-100 colors/neutral-100-opa-50]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (style/gradient-container insets animations derived)}
|
||||
{:colors [colors/neutral-100-opa-100 colors/neutral-100-opa-80 colors/neutral-100-opa-0]
|
||||
:location [0.2 0.9]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (style/gradient-container insets animations derived transparent?)}
|
||||
[text-sheet/view messages animations state props]
|
||||
[rn/flat-list
|
||||
{:ref #(reset! (:small-list-ref props) %)
|
||||
|
||||
@@ -62,14 +62,16 @@
|
||||
|
||||
;;;; BOTTOM-VIEW
|
||||
(defn gradient-container
|
||||
[insets {:keys [opacity]} {:keys [bottom-layout]}]
|
||||
[insets {:keys [opacity]} {:keys [bottom-layout]} transparent?]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:transform [{:translateY bottom-layout}]
|
||||
:opacity opacity}
|
||||
{:position :absolute
|
||||
:overflow :visible
|
||||
:display (if @transparent? :none :flex)
|
||||
:bottom 0
|
||||
:padding-bottom (:bottom insets)
|
||||
:padding-top c/text-min-height
|
||||
:z-index 3}))
|
||||
|
||||
(defn content-container
|
||||
@@ -80,17 +82,27 @@
|
||||
:justify-content :center})
|
||||
|
||||
|
||||
(defn background
|
||||
(defn background-bottom-gradient
|
||||
[{:keys [overlay-opacity]} z-index]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity overlay-opacity}
|
||||
{:background-color colors/neutral-100-opa-70
|
||||
:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:z-index z-index
|
||||
:left 0
|
||||
:right 0}))
|
||||
{:opacity (reanimated/interpolate overlay-opacity [0 0.1 0.4 1] [0 0.1 1 1])}
|
||||
{:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:z-index z-index
|
||||
:left 0
|
||||
:right 0}))
|
||||
|
||||
(defn background-top-gradient
|
||||
[{:keys [overlay-opacity]} z-index]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity (reanimated/interpolate overlay-opacity [0.3 1] [0 1])}
|
||||
{:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:z-index z-index
|
||||
:left 0
|
||||
:right 0}))
|
||||
|
||||
(defn bottom-inset-cover-up
|
||||
[insets]
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
:left 0
|
||||
:right 0}))
|
||||
|
||||
(def text-style
|
||||
(defn text-style
|
||||
[expanding-message?]
|
||||
{:color colors/white
|
||||
:margin-horizontal 20
|
||||
:margin-bottom constants/text-margin
|
||||
:align-items (when-not expanding-message? :center)
|
||||
:flex-grow 1})
|
||||
|
||||
(def bar-container
|
||||
@@ -31,30 +32,37 @@
|
||||
{:width 32
|
||||
:height 4
|
||||
:border-radius 100
|
||||
:background-color colors/white-opa-40
|
||||
:border-width 0.5
|
||||
:border-color colors/neutral-100})
|
||||
:background-color colors/white-opa-10})
|
||||
|
||||
(defn top-gradient
|
||||
[{:keys [gradient-opacity]} insets]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity gradient-opacity}
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:top (- (+ (:top insets)
|
||||
constants/top-view-height))
|
||||
:height (+ (:top insets)
|
||||
constants/top-view-height
|
||||
constants/bar-container-height
|
||||
constants/text-margin
|
||||
(* constants/line-height 2))
|
||||
:z-index 1}))
|
||||
[{:keys [derived-value]} {:keys [top]} insets max-height]
|
||||
(let [initial-top (- (+ (:top insets)
|
||||
constants/top-view-height))
|
||||
height (+ (:top insets)
|
||||
constants/top-view-height
|
||||
(* constants/line-height 2))]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity (reanimated/interpolate derived-value
|
||||
[max-height (+ max-height constants/line-height)]
|
||||
[0 1])
|
||||
:top (reanimated/interpolate top
|
||||
[0 (- max-height)]
|
||||
[initial-top (- initial-top max-height)]
|
||||
{:extrapolateLeft "clamp"
|
||||
:extrapolateRight "clamp"})}
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:height height
|
||||
:right 0
|
||||
:z-index 1})))
|
||||
|
||||
(def bottom-gradient
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:height 28
|
||||
:bottom 0
|
||||
:z-index 1})
|
||||
(defn bottom-gradient
|
||||
[bottom-inset]
|
||||
(let [gradient-distance (+ constants/small-list-height bottom-inset)]
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:height (+ gradient-distance (* 2 constants/line-height))
|
||||
:bottom (- gradient-distance)
|
||||
:opacity 0.8
|
||||
:z-index 1}))
|
||||
|
||||
@@ -3,41 +3,69 @@
|
||||
[oops.core :as oops]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.lightbox.constants :as constants]
|
||||
[utils.worklets.lightbox :as worklet]))
|
||||
[reagent.core :as r]
|
||||
[status-im2.contexts.chat.lightbox.constants :as constants]))
|
||||
|
||||
(defn- collapse-sheet
|
||||
[{:keys [derived-value overlay-opacity saved-top expanded? overlay-z-index]}]
|
||||
(reanimated/animate derived-value constants/text-min-height)
|
||||
(reanimated/animate overlay-opacity 0)
|
||||
(reanimated/set-shared-value saved-top (- constants/text-min-height))
|
||||
(reset! expanded? false)
|
||||
(js/setTimeout #(reset! overlay-z-index 0) 300))
|
||||
|
||||
(defn sheet-gesture
|
||||
[{:keys [derived-value saved-top overlay-opacity gradient-opacity]}
|
||||
expanded-height max-height overlay-z-index expanded? dragging?]
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/on-start (fn []
|
||||
(reset! overlay-z-index 1)
|
||||
(reset! dragging? true)
|
||||
(reanimated/animate gradient-opacity 0)))
|
||||
(gesture/on-update
|
||||
(fn [e]
|
||||
(let [new-value (+ (reanimated/get-shared-value saved-top) (oops/oget e "translationY"))
|
||||
bounded-value (max (min (- new-value) expanded-height) constants/text-min-height)
|
||||
progress (/ (- new-value) max-height)]
|
||||
(reanimated/set-shared-value overlay-opacity progress)
|
||||
(reanimated/set-shared-value derived-value bounded-value))))
|
||||
(gesture/on-end
|
||||
(fn []
|
||||
(if (or (> (- (reanimated/get-shared-value derived-value))
|
||||
(reanimated/get-shared-value saved-top))
|
||||
(= (reanimated/get-shared-value derived-value)
|
||||
constants/text-min-height))
|
||||
(do ; minimize
|
||||
(reanimated/animate derived-value constants/text-min-height)
|
||||
(reanimated/animate overlay-opacity 0)
|
||||
(reanimated/set-shared-value saved-top (- constants/text-min-height))
|
||||
(reset! expanded? false)
|
||||
(js/setTimeout #(reset! overlay-z-index 0) 300))
|
||||
(reanimated/set-shared-value saved-top
|
||||
(- (reanimated/get-shared-value derived-value))))
|
||||
(when (= (reanimated/get-shared-value derived-value) expanded-height)
|
||||
(reset! expanded? true))
|
||||
(reset! dragging? false)))))
|
||||
expanded-height max-height full-height overlay-z-index expanded? dragging? expanding-message?]
|
||||
(let [disable-gesture-update (r/atom false)]
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/enabled expanding-message?)
|
||||
(gesture/on-start (fn []
|
||||
(reset! overlay-z-index 1)
|
||||
(reset! dragging? true)
|
||||
(reset! disable-gesture-update false)
|
||||
(when (not expanded?)
|
||||
(reanimated/animate gradient-opacity 0))))
|
||||
(gesture/on-update
|
||||
(fn [e]
|
||||
(when-not @disable-gesture-update
|
||||
(let [event-value (oops/oget e :translationY)
|
||||
old-value (reanimated/get-shared-value saved-top)
|
||||
new-value (+ old-value event-value)
|
||||
progress (/ (- new-value) max-height)
|
||||
reached-expanded? (< new-value (- max-height))
|
||||
upper-boundary? (< new-value (- full-height))
|
||||
lower-boundary? (and (> new-value (- constants/text-min-height))
|
||||
(pos? event-value))]
|
||||
(when (and (not upper-boundary?) (not lower-boundary?))
|
||||
(reset! expanded? false)
|
||||
(reanimated/set-shared-value overlay-opacity progress)
|
||||
(reanimated/set-shared-value derived-value (- new-value)))
|
||||
(when reached-expanded? (reset! expanded? true))
|
||||
(when lower-boundary?
|
||||
(reset! disable-gesture-update true)
|
||||
(collapse-sheet {:derived-value derived-value
|
||||
:overlay-opacity overlay-opacity
|
||||
:saved-top saved-top
|
||||
:expanded? expanded?
|
||||
:overlay-z-index overlay-z-index}))))))
|
||||
(gesture/on-end
|
||||
(fn []
|
||||
(let [shared-derived-value (reanimated/get-shared-value derived-value)
|
||||
below-max-height? (< shared-derived-value max-height)
|
||||
below-saved-top? (> (- shared-derived-value)
|
||||
(reanimated/get-shared-value saved-top))]
|
||||
(if (and below-max-height? below-saved-top?)
|
||||
(collapse-sheet {:derived-value derived-value
|
||||
:overlay-opacity overlay-opacity
|
||||
:saved-top saved-top
|
||||
:expanded? expanded?
|
||||
:overlay-z-index overlay-z-index})
|
||||
(reanimated/set-shared-value saved-top
|
||||
(- shared-derived-value)))
|
||||
(when (= shared-derived-value expanded-height)
|
||||
(reset! expanded? true))
|
||||
(reset! dragging? false)))))))
|
||||
|
||||
(defn expand-sheet
|
||||
[{:keys [derived-value overlay-opacity saved-top]}
|
||||
@@ -49,12 +77,6 @@
|
||||
(reset! overlay-z-index 1)
|
||||
(reset! expanded? true)))
|
||||
|
||||
(defn on-scroll
|
||||
[e expanded? dragging? {:keys [gradient-opacity]}]
|
||||
(if (and (> (oops/oget e "nativeEvent.contentOffset.y") 0) expanded? (not dragging?))
|
||||
(reanimated/animate gradient-opacity 1)
|
||||
(reanimated/animate gradient-opacity 0)))
|
||||
|
||||
(defn on-layout
|
||||
[e text-height]
|
||||
(reset! text-height (oops/oget e "nativeEvent.layout.height")))
|
||||
@@ -68,5 +90,5 @@
|
||||
|
||||
(defn init-derived-animations
|
||||
[{:keys [derived-value]}]
|
||||
{:height (worklet/text-sheet derived-value true)
|
||||
:top (worklet/text-sheet derived-value false)})
|
||||
{:height derived-value
|
||||
:top (reanimated/interpolate derived-value [0 1] [0 -1])})
|
||||
|
||||
@@ -13,13 +13,7 @@
|
||||
[status-im2.contexts.chat.lightbox.text-sheet.utils :as utils]
|
||||
[status-im2.contexts.chat.messages.content.text.view :as message-view]))
|
||||
|
||||
(defn bar
|
||||
[text-height]
|
||||
(when (> text-height (* constants/line-height 2))
|
||||
[rn/view {:style style/bar-container}
|
||||
[rn/view {:style style/bar}]]))
|
||||
|
||||
(defn text-sheet
|
||||
(defn- text-sheet
|
||||
[messages overlay-opacity overlay-z-index text-sheet-lock?]
|
||||
(let [text-height (reagent/atom 0)
|
||||
expanded? (reagent/atom false)
|
||||
@@ -33,50 +27,63 @@
|
||||
constants/top-view-height
|
||||
(:bottom insets)
|
||||
(when platform/ios? (:top insets)))
|
||||
expanded-height (min max-height
|
||||
(+ constants/bar-container-height
|
||||
@text-height
|
||||
constants/text-margin))
|
||||
full-height (+ constants/bar-container-height
|
||||
constants/text-margin
|
||||
constants/line-height
|
||||
@text-height)
|
||||
expanded-height (min max-height full-height)
|
||||
animations (utils/init-animations overlay-opacity)
|
||||
derived (utils/init-derived-animations animations)]
|
||||
[gesture/gesture-detector
|
||||
{:gesture (utils/sheet-gesture animations
|
||||
expanded-height
|
||||
max-height
|
||||
overlay-z-index
|
||||
expanded?
|
||||
dragging?)}
|
||||
[reanimated/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:on-press
|
||||
#(utils/expand-sheet animations
|
||||
expanded-height
|
||||
max-height
|
||||
overlay-z-index
|
||||
expanded?
|
||||
text-sheet-lock?)
|
||||
:style (style/sheet-container derived)}
|
||||
[bar @text-height]
|
||||
[reanimated/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-0 colors/neutral-100]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (style/top-gradient animations insets)}]
|
||||
[linear-gradient/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-50 colors/neutral-100-opa-0]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style style/bottom-gradient}]
|
||||
[gesture/scroll-view
|
||||
{:scroll-enabled @expanded?
|
||||
:scroll-event-throttle 16
|
||||
:on-scroll #(utils/on-scroll % @expanded? @dragging? animations)
|
||||
:style {:height (- max-height constants/bar-container-height)}}
|
||||
[message-view/render-parsed-text
|
||||
{:content content
|
||||
:chat-id chat-id
|
||||
:style-override style/text-style
|
||||
:on-layout #(utils/on-layout % text-height)}]]]]))))
|
||||
derived (utils/init-derived-animations animations)
|
||||
expanding-message? (> @text-height (* constants/line-height 2))]
|
||||
[rn/view
|
||||
[reanimated/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-0 colors/neutral-100]
|
||||
:pointer-events :none
|
||||
:locations [0 0.3]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (style/top-gradient animations derived insets max-height)}]
|
||||
[gesture/gesture-detector
|
||||
{:gesture (utils/sheet-gesture animations
|
||||
expanded-height
|
||||
max-height
|
||||
full-height
|
||||
overlay-z-index
|
||||
expanded?
|
||||
dragging?
|
||||
expanding-message?)}
|
||||
[gesture/gesture-detector
|
||||
{:gesture (-> (gesture/gesture-tap)
|
||||
(gesture/enabled (and expanding-message? (not @expanded?)))
|
||||
(gesture/on-start (fn []
|
||||
(utils/expand-sheet animations
|
||||
expanded-height
|
||||
max-height
|
||||
overlay-z-index
|
||||
expanded?
|
||||
text-sheet-lock?))))}
|
||||
[reanimated/view {:style (style/sheet-container derived)}
|
||||
(when expanding-message?
|
||||
[rn/view {:style style/bar-container}
|
||||
[rn/view {:style style/bar}]])
|
||||
[linear-gradient/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-100 colors/neutral-100-opa-70 colors/neutral-100-opa-0]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:locations [0.7 0.8 1]
|
||||
:style (style/bottom-gradient (:bottom insets))}]
|
||||
[gesture/scroll-view
|
||||
{:scroll-enabled false
|
||||
:scroll-event-throttle 16
|
||||
:bounces false
|
||||
:style {:height (- max-height constants/bar-container-height)}
|
||||
:content-container-style {:padding-top (when (not expanding-message?)
|
||||
constants/bar-container-height)}}
|
||||
[message-view/render-parsed-text
|
||||
{:content content
|
||||
:chat-id chat-id
|
||||
:style-override (style/text-style expanding-message?)
|
||||
:on-layout #(utils/on-layout % text-height)}]]]]]]))))
|
||||
|
||||
(defn view
|
||||
[messages {:keys [overlay-opacity]} {:keys [overlay-z-index]} {:keys [text-sheet-lock?]}]
|
||||
|
||||
@@ -75,7 +75,16 @@
|
||||
{:transform [{:translateY (:pan-y animations)}
|
||||
{:translateX (:pan-x animations)}]}
|
||||
{})}
|
||||
[reanimated/view {:style (style/background animations @(:overlay-z-index state))}]
|
||||
[reanimated/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-0 colors/neutral-100-opa-0 colors/neutral-100-opa-100
|
||||
colors/neutral-100]
|
||||
:locations [0.3 0.4 0.6 1]
|
||||
:pointer-events :none
|
||||
:style (style/background-bottom-gradient animations @(:overlay-z-index state))}]
|
||||
[reanimated/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-50 colors/neutral-100]
|
||||
:pointer-events :none
|
||||
:style (style/background-top-gradient animations @(:overlay-z-index state))}]
|
||||
[gesture/flat-list
|
||||
{:ref #(reset! (:flat-list-ref props) %)
|
||||
:key-fn :message-id
|
||||
@@ -85,6 +94,7 @@
|
||||
:data @data
|
||||
:render-fn image
|
||||
:render-data {:opacity-value (:opacity animations)
|
||||
:overlay-opacity (:overlay-opacity animations)
|
||||
:border-value (:border animations)
|
||||
:full-screen-scale (:full-screen-scale animations)
|
||||
:images-opacity (:images-opacity animations)
|
||||
@@ -105,9 +115,12 @@
|
||||
:shows-vertical-scroll-indicator false
|
||||
:shows-horizontal-scroll-indicator false
|
||||
:on-viewable-items-changed handle-items-changed}]]]
|
||||
(when (and (not @transparent?) (not landscape?))
|
||||
;; NOTE: not un-mounting bottom-view based on `transparent?` (like we do with the top-view
|
||||
;; above), since we need to save the state of the text-sheet position. Instead, we use
|
||||
;; the `:display` style property to hide the bottom-sheet.
|
||||
(when (not landscape?)
|
||||
[:f> bottom-view/bottom-view messages index scroll-index insets animations derived
|
||||
item-width props state])]))
|
||||
item-width props state transparent?])]))
|
||||
|
||||
(defn- f-lightbox
|
||||
[]
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
[react-native.navigation :as navigation]
|
||||
[react-native.orientation :as orientation]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.chat.lightbox.animations :as anim]
|
||||
[status-im2.contexts.chat.lightbox.zoomable-image.constants :as constants]
|
||||
@@ -107,12 +106,36 @@
|
||||
(rescale constants/min-scale true)))
|
||||
|
||||
(defn toggle-opacity
|
||||
[index {:keys [opacity-value border-value full-screen-scale transparent? props]} portrait?]
|
||||
[index
|
||||
{:keys [opacity-value overlay-opacity border-value full-screen-scale transparent?
|
||||
props]}
|
||||
portrait? last-overlay-opacity]
|
||||
(let [{:keys [small-list-ref timers text-sheet-lock?]} props
|
||||
opacity (reanimated/get-shared-value opacity-value)
|
||||
scale-value (inc (/ constants/margin
|
||||
(:width (rn/get-window))))]
|
||||
(if (= opacity 1)
|
||||
(if @transparent?
|
||||
(do
|
||||
(js/clearTimeout (:hide-0 @timers))
|
||||
(js/clearTimeout (:hide-1 @timers))
|
||||
(reset! transparent? (not @transparent?))
|
||||
(swap! timers assoc
|
||||
:show-0
|
||||
(js/setTimeout #((anim/animate opacity-value 1)
|
||||
(anim/animate overlay-opacity @last-overlay-opacity)
|
||||
(reset! last-overlay-opacity 0))
|
||||
50))
|
||||
(swap! timers assoc
|
||||
:show-1
|
||||
(js/setTimeout #(when @small-list-ref
|
||||
(.scrollToIndex ^js @small-list-ref #js {:animated false :index index}))
|
||||
100))
|
||||
(anim/animate border-value 16)
|
||||
(anim/animate full-screen-scale 1)
|
||||
(when portrait?
|
||||
(swap! timers assoc
|
||||
:show-2
|
||||
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible true}})
|
||||
(if platform/ios? 150 50)))))
|
||||
(do
|
||||
(js/clearTimeout (:show-0 @timers))
|
||||
(js/clearTimeout (:show-1 @timers))
|
||||
@@ -122,26 +145,13 @@
|
||||
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible false}})
|
||||
(if platform/ios? 75 0)))
|
||||
(anim/animate opacity-value 0)
|
||||
(reset! last-overlay-opacity (anim/get-val overlay-opacity))
|
||||
(anim/animate overlay-opacity 0)
|
||||
(anim/animate border-value 0)
|
||||
(anim/animate full-screen-scale scale-value)
|
||||
(swap! timers assoc :hide-1 (js/setTimeout #(reset! transparent? (not @transparent?)) 400))
|
||||
(reset! text-sheet-lock? true)
|
||||
(js/setTimeout #(reset! text-sheet-lock? false) 300))
|
||||
(do
|
||||
(js/clearTimeout (:hide-0 @timers))
|
||||
(js/clearTimeout (:hide-1 @timers))
|
||||
(reset! transparent? (not @transparent?))
|
||||
(swap! timers assoc :show-0 (js/setTimeout #(anim/animate opacity-value 1) 50))
|
||||
(swap! timers assoc
|
||||
:show-1
|
||||
(js/setTimeout #(when @small-list-ref
|
||||
(.scrollToIndex ^js @small-list-ref #js {:animated false :index index}))
|
||||
100))
|
||||
(when portrait?
|
||||
(swap! timers assoc
|
||||
:show-2
|
||||
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible true}})
|
||||
(if platform/ios? 150 50))))))
|
||||
(anim/animate border-value (if (= opacity 1) 0 16))
|
||||
(anim/animate full-screen-scale (if (= opacity 1) scale-value 1))))
|
||||
(js/setTimeout #(reset! text-sheet-lock? false) 300)))))
|
||||
|
||||
;;; Dimensions
|
||||
(defn get-dimensions
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[react-native.orientation :as orientation]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as r]
|
||||
[status-im2.contexts.chat.lightbox.animations :as anim]
|
||||
[status-im2.contexts.chat.lightbox.zoomable-image.constants :as c]
|
||||
[status-im2.contexts.chat.lightbox.zoomable-image.style :as style]
|
||||
@@ -209,7 +210,8 @@
|
||||
image-dimensions-nil?]
|
||||
(let [{:keys [transparent? set-full-height?]} render-data
|
||||
portrait? (= curr-orientation orientation/portrait)
|
||||
on-tap #(utils/toggle-opacity index render-data portrait?)
|
||||
last-overlay-opacity (r/atom 0)
|
||||
on-tap #(utils/toggle-opacity index render-data portrait? last-overlay-opacity)
|
||||
tap (tap-gesture on-tap)
|
||||
double-tap (double-tap-gesture dimensions animations rescale transparent? on-tap)
|
||||
pinch (pinch-gesture dimensions animations state rescale transparent? on-tap)
|
||||
@@ -242,8 +244,8 @@
|
||||
zoom-out-signal (rf/sub [:lightbox/zoom-out-signal])
|
||||
{:keys [set-full-height? curr-orientation]} render-data
|
||||
focused? (= shared-element-id message-id)
|
||||
;; TODO - remove `image-dimensions` check,
|
||||
;; once https://github.com/status-im/status-desktop/issues/10944 is fixed
|
||||
;; TODO - remove `image-dimensions` check, once
|
||||
;; https://github.com/status-im/status-desktop/issues/10944 is fixed
|
||||
image-dimensions-nil? (not (and image-width image-height))
|
||||
dimensions (utils/get-dimensions
|
||||
(or image-width (:screen-width render-data))
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
(:require
|
||||
[quo.components.selectors.reaction-resource :as reactions.resource]
|
||||
[quo.core :as quo]
|
||||
[react-native.clipboard :as clipboard]
|
||||
[react-native.core :as rn]
|
||||
[react-native.gesture :as gesture]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im2.common.contact-list-item.view :as contact-list-item]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.reply.view :as reply]
|
||||
@@ -119,7 +119,7 @@
|
||||
(when (and (not (or deleted? deleted-for-me?))
|
||||
(not= content-type constants/content-type-audio))
|
||||
[{:type :main
|
||||
:on-press #(react/copy-to-clipboard
|
||||
:on-press #(clipboard/set-string
|
||||
(reply/get-quoted-text-with-mentions
|
||||
(get content :parsed-text)))
|
||||
:label (i18n/label :t/copy-text)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
(defn community-link
|
||||
[id]
|
||||
(str "https://status.app/c/" id))
|
||||
(str "https://status.app/c#" id))
|
||||
|
||||
(rf/defn cache-link-preview-data
|
||||
{:events [:chat.ui/cache-link-preview-data]}
|
||||
|
||||