mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
260 Commits
refactor/m
...
feat/file-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8472154abb | ||
|
|
d90dd5b48f | ||
|
|
3ce5b7929e | ||
|
|
25bf4fee82 | ||
|
|
c2ab5ded47 | ||
|
|
ad6a0ebfab | ||
|
|
27b3575fcf | ||
|
|
aef6a1b540 | ||
|
|
e78ac3c925 | ||
|
|
f30c066305 | ||
|
|
ac39ca8a11 | ||
|
|
cb9e6578c1 | ||
|
|
17a1475ded | ||
|
|
2cee5a4d89 | ||
|
|
337f88c373 | ||
|
|
53d5073690 | ||
|
|
9cc9660b48 | ||
|
|
e6162b2580 | ||
|
|
abc14d2d1f | ||
|
|
fa11b5fabb | ||
|
|
d904916863 | ||
|
|
0cfc2e1609 | ||
|
|
3742e923c9 | ||
|
|
792a254513 | ||
|
|
17928ba182 | ||
|
|
2da0f164c6 | ||
|
|
4eabe79e7e | ||
|
|
89dd8f5b6c | ||
|
|
b4d3662897 | ||
|
|
675e1e2c6f | ||
|
|
0f7f3bae7e | ||
|
|
c917d95a7b | ||
|
|
3ae9001504 | ||
|
|
cc97b8beed | ||
|
|
954bdebcef | ||
|
|
04b62f4e53 | ||
|
|
a2dcb67b36 | ||
|
|
cc9141c981 | ||
|
|
85484b1762 | ||
|
|
1a0c94896a | ||
|
|
068a99463d | ||
|
|
b413315777 | ||
|
|
7227513783 | ||
|
|
083e4e1150 | ||
|
|
09098410a5 | ||
|
|
2c9f5df530 | ||
|
|
3ad346362b | ||
|
|
e176b1bf76 | ||
|
|
b368232598 | ||
|
|
bcd27e10c1 | ||
|
|
af9b9b7083 | ||
|
|
ea1565048a | ||
|
|
59b081704b | ||
|
|
2761222b36 | ||
|
|
571f15b556 | ||
|
|
283efa6e41 | ||
|
|
b3b1b7470e | ||
|
|
a9e59b17dc | ||
|
|
22d946b677 | ||
|
|
6a748c3f98 | ||
|
|
520296106a | ||
|
|
3b727e6f64 | ||
|
|
b77711a53d | ||
|
|
aebb2d414b | ||
|
|
e0e505371c | ||
|
|
b9040a9358 | ||
|
|
68f10ce308 | ||
|
|
8a475f654f | ||
|
|
1043b7df55 | ||
|
|
8383e09243 | ||
|
|
df58a6cfc4 | ||
|
|
a9df577478 | ||
|
|
e021d93893 | ||
|
|
24e13f488e | ||
|
|
6f88b70937 | ||
|
|
a90f957aa4 | ||
|
|
748953b3eb | ||
|
|
874c892986 | ||
|
|
ab678fa3a9 | ||
|
|
c2351ab2d3 | ||
|
|
3fd3e02256 | ||
|
|
d2fd26abd5 | ||
|
|
dd84b9ca2f | ||
|
|
269f083dce | ||
|
|
e18867d3f1 | ||
|
|
e221882f8e | ||
|
|
153ee4bce3 | ||
|
|
a2ecdab61c | ||
|
|
eb089e6211 | ||
|
|
1981c3a2f8 | ||
|
|
68b3ebe318 | ||
|
|
8a0b6264d2 | ||
|
|
239174c3e7 | ||
|
|
db1503d828 | ||
|
|
7b1cbba9e7 | ||
|
|
2c3c193629 | ||
|
|
899e56e9cd | ||
|
|
9c9582ae12 | ||
|
|
b918667b63 | ||
|
|
878c901e06 | ||
|
|
fdd3897a60 | ||
|
|
c958657b01 | ||
|
|
e7175f48a0 | ||
|
|
4e1862d653 | ||
|
|
1d5ac37222 | ||
|
|
32d027fff6 | ||
|
|
9434993373 | ||
|
|
2bd0f1a304 | ||
|
|
7f873f240e | ||
|
|
a0229729b7 | ||
|
|
dd368a4170 | ||
|
|
6d3e985c9d | ||
|
|
15858db21b | ||
|
|
b69a94a96f | ||
|
|
73b965eb7c | ||
|
|
5738f48360 | ||
|
|
de74b8c2cd | ||
|
|
2d07d79057 | ||
|
|
5f6a38c8b1 | ||
|
|
3496e6bfa9 | ||
|
|
fc5b62ba61 | ||
|
|
9d0a206240 | ||
|
|
ee6bbca18c | ||
|
|
e6d5472514 | ||
|
|
1e5f13087b | ||
|
|
abb89ecbc3 | ||
|
|
aa168bec5e | ||
|
|
f9116b22db | ||
|
|
5b957f5f0d | ||
|
|
a578aae3dc | ||
|
|
43e5d2fa2d | ||
|
|
b58d034716 | ||
|
|
3b77d79997 | ||
|
|
ef4c9af7dc | ||
|
|
8ba3cf172e | ||
|
|
3e446ce9dd | ||
|
|
394aa98713 | ||
|
|
a2ac882a05 | ||
|
|
cae36487f9 | ||
|
|
fade5eddbf | ||
|
|
66b2ffce31 | ||
|
|
13cd2d4daa | ||
|
|
8442faeeb1 | ||
|
|
6d18383b22 | ||
|
|
f9bcfe2f7a | ||
|
|
3621f95dbc | ||
|
|
536dead44d | ||
|
|
39d74e80a9 | ||
|
|
46e03dd248 | ||
|
|
152fee2c29 | ||
|
|
492a4d16b2 | ||
|
|
3d7c1fa1e7 | ||
|
|
69bb0e3656 | ||
|
|
4875cfa108 | ||
|
|
a28beb62c6 | ||
|
|
21c8960146 | ||
|
|
2ba638b123 | ||
|
|
8548940cb9 | ||
|
|
909bdfa0cf | ||
|
|
bfb3c56abe | ||
|
|
e06836f701 | ||
|
|
bc16cb79f2 | ||
|
|
176c46e3f2 | ||
|
|
af65055ce9 | ||
|
|
ad23ff19de | ||
|
|
cf65fc4f57 | ||
|
|
9ab83e7fbb | ||
|
|
1acf2aadba | ||
|
|
06833bcac9 | ||
|
|
3bf6b5293a | ||
|
|
c40d33df37 | ||
|
|
fe609f8275 | ||
|
|
3566f47f2d | ||
|
|
4b5526dffd | ||
|
|
4977390394 | ||
|
|
87804de452 | ||
|
|
bc55d2b476 | ||
|
|
3ff34a5c82 | ||
|
|
7e01e90e22 | ||
|
|
d568be6536 | ||
|
|
734e79604f | ||
|
|
227a962042 | ||
|
|
a1640175e5 | ||
|
|
7cf61570ef | ||
|
|
83762c668e | ||
|
|
ecd4a08b1c | ||
|
|
7b66462300 | ||
|
|
41639fd325 | ||
|
|
2fe82841ef | ||
|
|
9b7af87611 | ||
|
|
d99ccbc243 | ||
|
|
0f191eba5c | ||
|
|
26657c7ee3 | ||
|
|
a12749eb82 | ||
|
|
1a56d38116 | ||
|
|
0b9025af11 | ||
|
|
6f5a44cd11 | ||
|
|
5e1cc307b1 | ||
|
|
2197f1ec5e | ||
|
|
1da5fd3661 | ||
|
|
49e79b52b2 | ||
|
|
c9a657298c | ||
|
|
cfd7caf16c | ||
|
|
9744679550 | ||
|
|
9cdc502ce5 | ||
|
|
300a104949 | ||
|
|
efad6520a2 | ||
|
|
cc58351731 | ||
|
|
cf2f4c4bbb | ||
|
|
cc161b844c | ||
|
|
45ebf0670f | ||
|
|
9cfaa90232 | ||
|
|
edf4a248ed | ||
|
|
14d7ec6b4e | ||
|
|
cd2ca94c3d | ||
|
|
f1609aaa46 | ||
|
|
851364c0bf | ||
|
|
913a1743b5 | ||
|
|
dc5572d4b9 | ||
|
|
554560903c | ||
|
|
885e4a325c | ||
|
|
156ce7e711 | ||
|
|
71ecc05bd6 | ||
|
|
98b93d42c1 | ||
|
|
2e7935cacc | ||
|
|
50d64af7f6 | ||
|
|
e452fd3d2f | ||
|
|
64dd481402 | ||
|
|
28f647fa06 | ||
|
|
9eb1061cd1 | ||
|
|
3cab264213 | ||
|
|
fb56e7c866 | ||
|
|
234e368c4d | ||
|
|
4de4693505 | ||
|
|
5563c7d5f8 | ||
|
|
93225e5a30 | ||
|
|
0ab9846732 | ||
|
|
f6f1cf0ae7 | ||
|
|
69615d54db | ||
|
|
a990d0f404 | ||
|
|
4aeff48e74 | ||
|
|
78c1a75ec9 | ||
|
|
9e6664a80e | ||
|
|
b306a09747 | ||
|
|
fab52012ad | ||
|
|
fc39c714b6 | ||
|
|
5fff4bbb0e | ||
|
|
0a29571ad8 | ||
|
|
275fa1b3ca | ||
|
|
79889665e5 | ||
|
|
9888ad327c | ||
|
|
77ab2d7740 | ||
|
|
509dc4b541 | ||
|
|
ff84ee2d42 | ||
|
|
d838c2e8ad | ||
|
|
28d718acf6 | ||
|
|
adf546f29c | ||
|
|
b18cf277f6 | ||
|
|
df94f31998 | ||
|
|
aec1d601a3 |
@@ -20,6 +20,7 @@ const SCOPES = [
|
||||
"logger",
|
||||
"theme",
|
||||
"core",
|
||||
"fs",
|
||||
"clipper",
|
||||
"config",
|
||||
"ci",
|
||||
|
||||
@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
|
||||
|
||||
Ugh! Bugs!
|
||||
|
||||
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
|
||||
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
|
||||
|
||||
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
|
||||
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
|
||||
|
||||
@@ -30,7 +30,7 @@ Before you open a new feature request, please make sure it's not a duplicate. **
|
||||
|
||||
### Helping out in the issue tracker
|
||||
|
||||
There are always new issues getting opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
New issues are always opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
|
||||
In addition to this, you can help out in the following ways:
|
||||
|
||||
@@ -62,7 +62,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
|
||||
|
||||
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
|
||||
2. Run `npm run bootstrap` in the repository root.
|
||||
3. If you’ve fixed a bug or added code that should be tested, add tests!
|
||||
3. If you've fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`npm run test`).
|
||||
5. Format your code with prettier (`npm run prettier`).
|
||||
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
|
||||
@@ -131,11 +131,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
|
||||
|
||||
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
|
||||
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb’s Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
|
||||
## Git Branch Organization
|
||||
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don’t use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
|
||||
1. All tests must be passing at all times
|
||||
2. There should be as few breaking changes as possible
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
|
||||
|
||||
## Developer guide
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
## Build instructions
|
||||
|
||||
> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
|
||||
### Setting up development environment
|
||||
### Setting up the development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
@@ -57,7 +57,7 @@ npm install
|
||||
|
||||
### Running the app on Android
|
||||
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
|
||||
|
||||
```bash
|
||||
npm run start:android
|
||||
@@ -78,11 +78,11 @@ npm run start:ios
|
||||
|
||||
## Developer guide
|
||||
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible
|
||||
We try to keep the stack as lean as possible:
|
||||
|
||||
1. React Native
|
||||
2. Typescript/Javascript
|
||||
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible
|
||||
|
||||
The app codebase is distributed over two primary directories. `native/` and `app/`.
|
||||
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
|
||||
|
||||
- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
|
||||
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
|
||||
- `hooks/`: Hooks for different app logic
|
||||
- `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `screens`: Navigator screens.
|
||||
- `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions etc.
|
||||
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions, etc.
|
||||
|
||||
There are several other folders at the root:
|
||||
|
||||
@@ -115,11 +115,11 @@ There are several other folders at the root:
|
||||
|
||||
### Running the tests
|
||||
|
||||
When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
|
||||
### Android
|
||||
|
||||
To run the tests on android, you will need to create an emulator device on your system:
|
||||
To run the tests on Android, you will need to create an emulator device on your system:
|
||||
|
||||
```
|
||||
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
|
||||
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
|
||||
|
||||
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
|
||||
|
||||
Once you have created an emulator device, build the android apks
|
||||
Once you have created an emulator device, build the Android apks:
|
||||
|
||||
```
|
||||
npm run build:android
|
||||
```
|
||||
|
||||
Finally run the tests
|
||||
Finally, run the tests:
|
||||
|
||||
```
|
||||
npm run test:android
|
||||
@@ -141,9 +141,9 @@ npm run test:android
|
||||
|
||||
### iOS
|
||||
|
||||
To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
|
||||
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
|
||||
|
||||
First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
|
||||
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
|
||||
|
||||
```
|
||||
brew tap wix/brew
|
||||
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
|
||||
npm run build:ios
|
||||
```
|
||||
|
||||
Finally run the tests:
|
||||
Finally, run the tests:
|
||||
|
||||
```
|
||||
npm run test:ios
|
||||
|
||||
@@ -31,14 +31,6 @@ import { TipManager } from "./services/tip-manager";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { View } from "react-native";
|
||||
import { useState } from "react";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
NetInfo.configure({
|
||||
reachabilityUrl: "https://notesnook.com",
|
||||
reachabilityTest: (response) => {
|
||||
if (!response) return false;
|
||||
return response?.status >= 200 && response?.status < 300;
|
||||
}
|
||||
});
|
||||
|
||||
SettingsService.init();
|
||||
SettingsService.checkOrientation();
|
||||
|
||||
@@ -22,10 +22,17 @@ import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
@@ -39,12 +46,27 @@ export async function deriveCryptoKey(name, data) {
|
||||
credentials.key,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeIOSKeychain(username, password) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
username,
|
||||
password,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
@@ -52,6 +74,9 @@ export async function getCryptoKey(_name) {
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// upgrades ios keychain to use accessGroups
|
||||
// so we have access to keychain in share extension.
|
||||
await upgradeIOSKeychain(credentials.username, credentials.password);
|
||||
return credentials.password;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -68,14 +68,3 @@ db.host(
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
export async function loadDatabase() {
|
||||
// if (!DB) {
|
||||
// let module = await import(/* webpackChunkName: "notes-core" */ 'notes-core/api/index');
|
||||
// DB = module.default;
|
||||
// }
|
||||
// db = new DB(Storage, Platform.OS === 'ios' ? EventSource : AndroidEventSource, filesystem);
|
||||
// if (DOMParser) {
|
||||
// await DOMParser.prepare();
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
deriveCryptoKey,
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Dimensions, Platform } from "react-native";
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
|
||||
@@ -17,25 +17,169 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/utils/filename";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { presentSheet, ToastEvent } from "../../services/event-manager";
|
||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir } from "./utils";
|
||||
import { getFileNameWithExtension } from "@notesnook/core/utils/filename";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
Fail: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export function downloadAllAttachments() {
|
||||
const attachments = db.attachments.all;
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||
: `${cacheDir}/notesnook-attachments.zip`;
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
let attachment = attachments[i];
|
||||
const hash = attachment.metadata.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Success
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
console.log("Error downloading attachment", e);
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (result?.size) {
|
||||
let sub;
|
||||
try {
|
||||
onProgress?.(0, `Zipping... Please wait`);
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||
);
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
onProgress(1, `Saving zip file... Please wait`);
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
|
||||
onProgress?.(1, `Done`);
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
console.log("Error zipping attachments", e);
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hash,
|
||||
global = true,
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined
|
||||
}
|
||||
) {
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
@@ -55,8 +199,13 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
try {
|
||||
await db.fs.downloadFile(
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db.fs.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
@@ -99,7 +248,8 @@ export default async function downloadAttachment(
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!attachment.metadata?.type?.startsWith("image")
|
||||
!isImage(attachment.metadata?.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||
@@ -131,5 +281,8 @@ export default async function downloadAttachment(
|
||||
.catch(console.log);
|
||||
}
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hosts from "@notesnook/core/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
|
||||
@@ -19,18 +19,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { cacheDir, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists =
|
||||
(await RNFetchBlob.fs.exists(path)) ||
|
||||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
|
||||
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
@@ -39,7 +50,8 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
hash: filename
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
},
|
||||
cipherData.outputType === "base64"
|
||||
? isPng || isJpeg
|
||||
|
||||
@@ -17,10 +17,13 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
@@ -35,9 +38,19 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||
const uploadUrl = await res.text();
|
||||
if (!uploadUrl) throw new Error("Unable to resolve upload url");
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
uploadFilePath = appGroupPath;
|
||||
}
|
||||
let request = RNFetchBlob.config({
|
||||
IOSBackgroundTask: true
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
"PUT",
|
||||
@@ -45,7 +58,7 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
{
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
|
||||
RNFetchBlob.wrap(uploadFilePath)
|
||||
)
|
||||
.uploadProgress((sent, total) => {
|
||||
useAttachmentStore
|
||||
@@ -63,7 +76,10 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
if (result) {
|
||||
let attachment = db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (!attachment.metadata.type.startsWith("image/")) {
|
||||
if (
|
||||
!isImage(attachment.metadata.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
@@ -62,3 +64,22 @@ export function cancelable(operation) {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
console.log(e, r);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (let uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
type="gray"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
right: 0
|
||||
}}
|
||||
iconSize={24}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingVertical: 0,
|
||||
|
||||
@@ -102,15 +102,21 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
ToastEvent.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
setFailed(null);
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
setAttachments([...db.attachments.all]);
|
||||
setLoading({
|
||||
name: null
|
||||
@@ -209,7 +215,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
@@ -221,7 +227,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
>
|
||||
{formatBytes(attachment.length)}
|
||||
@@ -232,7 +238,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
>
|
||||
{attachment.noteIds.length} note
|
||||
@@ -248,7 +254,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
>
|
||||
{attachment.metadata.hash}
|
||||
@@ -304,7 +310,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
}}
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
|
||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||
</PressableButton>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -22,7 +22,6 @@ import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { formatBytes } from "../../utils";
|
||||
import { SIZE } from "../../utils/size";
|
||||
@@ -37,19 +36,26 @@ function getFileExtension(filename) {
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
|
||||
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
export const AttachmentItem = ({
|
||||
attachment,
|
||||
encryption,
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
encryption
|
||||
);
|
||||
const encryptionProgress = useAttachmentStore(
|
||||
(state) => state.encryptionProgress
|
||||
);
|
||||
|
||||
const onPress = () => {
|
||||
if (!pressable) return;
|
||||
Actions.present(attachment, setAttachments, attachment.metadata.hash);
|
||||
};
|
||||
return (
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -112,22 +118,22 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
{attachment.metadata.filename}
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentProgress ||
|
||||
(encryptionProgress && encryptionProgress !== "0.00") ||
|
||||
encryption ? (
|
||||
{currentProgress ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption) return;
|
||||
if (encryption || !pressable) return;
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
@@ -140,13 +146,7 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
>
|
||||
<ProgressCircleComponent
|
||||
size={SIZE.xxl}
|
||||
progress={
|
||||
encryptionProgress
|
||||
? encryptionProgress
|
||||
: currentProgress?.value
|
||||
? currentProgress?.value / 100
|
||||
: 0
|
||||
}
|
||||
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
|
||||
showsText
|
||||
textStyle={{
|
||||
fontSize: 10
|
||||
|
||||
273
apps/mobile/app/components/attachments/download-attachments.js
Normal file
273
apps/mobile/app/components/attachments/download-attachments.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
|
||||
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
value: 0,
|
||||
statusText: "Download started... Please wait"
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef();
|
||||
|
||||
const onDownload = async () => {
|
||||
update({
|
||||
disableClosing: true
|
||||
});
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
attachments,
|
||||
(progress, statusText) => setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update({
|
||||
disableClosing: false
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update({
|
||||
disableClosing: false
|
||||
});
|
||||
canceled.current = true;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs.cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
groupId.current = null;
|
||||
};
|
||||
|
||||
const successResults = () => {
|
||||
const results = [];
|
||||
for (let [key, value] of result.entries()) {
|
||||
if (value.status === 1) results.push(db.attachments.attachment(key));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
const failedResults = () => {
|
||||
const results = [];
|
||||
for (let [key, value] of result.entries()) {
|
||||
if (value.status === 0) results.push(db.attachments.attachment(key));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>
|
||||
{downloading
|
||||
? "Downloading attachments"
|
||||
: result?.size
|
||||
? "Downloaded attachments"
|
||||
: "Download attachments"}
|
||||
</Heading>
|
||||
|
||||
{downloading ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{progress.statusText}
|
||||
</Paragraph>
|
||||
) : result?.size ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{getResultText()}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Are you sure you want to download all attachments
|
||||
{isNote ? " of this note?" : "?"}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{downloading ? (
|
||||
<View
|
||||
style={{
|
||||
width: 200,
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={progress.value ? progress.value / attachments.length : 0}
|
||||
unfilledColor={colors.nav}
|
||||
color={colors.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
width: "100%",
|
||||
minHeight: 60,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments : []}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 60
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon}>No downloads in progress.</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
attachment={item}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{result?.size ? (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={close}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
) : !downloading ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={close}
|
||||
type="grayBg"
|
||||
title="No"
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginLeft: 5
|
||||
}}
|
||||
onPress={onDownload}
|
||||
type="accent"
|
||||
title="Yes"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={cancel}
|
||||
type="error"
|
||||
title="Cancel"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (context, attachments, isNote) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
<DownloadAttachments
|
||||
close={close}
|
||||
attachments={attachments}
|
||||
isNote={isNote}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default DownloadAttachments;
|
||||
@@ -18,44 +18,60 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
isAudio,
|
||||
isDocument,
|
||||
isImage,
|
||||
isVideo
|
||||
} from "@notesnook/core/utils/filename";
|
||||
|
||||
export const AttachmentDialog = ({ data }) => {
|
||||
export const AttachmentDialog = ({ note }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [note, setNote] = useState(data);
|
||||
|
||||
const [attachments, setAttachments] = useState(
|
||||
data
|
||||
? db.attachments.ofNote(data.id, "all")
|
||||
note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])]
|
||||
);
|
||||
|
||||
const attachmentSearchValue = useRef();
|
||||
const searchTimer = useRef();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
|
||||
const onChangeText = (text) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments([...db.attachments.all]);
|
||||
setAttachments([...attachments]);
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => {
|
||||
let results = db.lookup.attachments(
|
||||
db.attachments.all,
|
||||
attachments,
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
@@ -67,6 +83,77 @@ export const AttachmentDialog = ({ data }) => {
|
||||
<AttachmentItem setAttachments={setAttachments} attachment={item} />
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
setLoading(true);
|
||||
const checkedAttachments = [];
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
if (result.failed) {
|
||||
await db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
checkedAttachments.push(
|
||||
db.attachments.attachment(attachment.metadata.hash)
|
||||
);
|
||||
setAttachments([...checkedAttachments]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Documents",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
filterBy: "audio"
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = (type) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
isDocument;
|
||||
switch (type) {
|
||||
case "all":
|
||||
return attachments;
|
||||
case "images":
|
||||
return attachments.filter((attachment) =>
|
||||
isImage(attachment.metadata.type)
|
||||
);
|
||||
case "video":
|
||||
return attachments.filter((attachment) =>
|
||||
isVideo(attachment.metadata.type)
|
||||
);
|
||||
case "audio":
|
||||
return attachments.filter((attachment) =>
|
||||
isAudio(attachment.metadata.type)
|
||||
);
|
||||
case "documents":
|
||||
return attachments.filter((attachment) =>
|
||||
isDocument(attachment.metadata.type)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -75,43 +162,71 @@ export const AttachmentDialog = ({ data }) => {
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={note ? "Attachments" : "Manage attachments"}
|
||||
paragraph="Tap on an attachment to view properties"
|
||||
button={{
|
||||
title: "Check all",
|
||||
type: "grayAccent",
|
||||
loading: loading,
|
||||
onPress: async () => {
|
||||
setLoading(true);
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(
|
||||
attachment.metadata.hash
|
||||
<SheetProvider context="attachments-list" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading>Attachments</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
name="check-all"
|
||||
customStyle={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
color={colors.pri}
|
||||
size={SIZE.lg}
|
||||
onPress={onCheck}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
customStyle={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
color={colors.pri}
|
||||
onPress={() => {
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
!!note
|
||||
);
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
{!note ? (
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
keyboardDismissMode="none"
|
||||
@@ -119,6 +234,45 @@ export const AttachmentDialog = ({ data }) => {
|
||||
maxToRenderPerBatch={10}
|
||||
initialNumToRender={10}
|
||||
windowSize={5}
|
||||
stickyHeaderIndices={[0]}
|
||||
ListHeaderComponent={
|
||||
<ScrollView
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.bg
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minWidth: "100%"
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) => (
|
||||
<Button
|
||||
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
||||
key={item.title}
|
||||
title={
|
||||
item.title +
|
||||
` (${filterAttachments(item.filterBy)?.length || 0})`
|
||||
}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
flexGrow: 1,
|
||||
borderBottomColor:
|
||||
currentFilter !== item.filterBy
|
||||
? "transparent"
|
||||
: colors.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(filterAttachments(item.filterBy));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -162,6 +316,6 @@ export const AttachmentDialog = ({ data }) => {
|
||||
|
||||
AttachmentDialog.present = (note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog data={note} />
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -126,11 +126,11 @@ const AuthModal = () => {
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title="Skip for now"
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={20}
|
||||
iconSize={16}
|
||||
type="gray"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
|
||||
@@ -18,9 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
@@ -30,13 +29,12 @@ import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SVG } from "./background";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -64,6 +62,7 @@ export const Login = ({ changeMode }) => {
|
||||
await sleep(500);
|
||||
Progress.present();
|
||||
});
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
@@ -82,62 +81,70 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
src={SVG(colors.night ? colors.icon : "black")}
|
||||
height={700}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15
|
||||
flex: 0.45,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.nav,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={30}
|
||||
color={colors.heading}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Welcome back!
|
||||
Login to your {"\n"}account
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
textAlign: "center",
|
||||
marginTop: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{"Don't have an account? Sign up"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
@@ -147,10 +154,9 @@ export const Login = ({ changeMode }) => {
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.bg,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -217,28 +223,22 @@ export const Login = ({ changeMode }) => {
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25,
|
||||
alignSelf: "center"
|
||||
marginTop: 25
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={
|
||||
loading
|
||||
? null
|
||||
: step === LoginSteps.emailAuth
|
||||
? "Login"
|
||||
: "Continue"
|
||||
}
|
||||
title={!loading ? "Continue" : null}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
@@ -261,9 +261,34 @@ export const Login = ({ changeMode }) => {
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.accent }}
|
||||
>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,8 +44,10 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return null;
|
||||
return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) {
|
||||
if (!email) return "";
|
||||
const [username, provider] = email.split("@");
|
||||
if (username.length === 1) return `****@${provider}`;
|
||||
return email.replace(/(.{1})(.*)(?=@)/, function (gp1, gp2, gp3) {
|
||||
for (let i = 0; i < gp3.length; i++) {
|
||||
gp2 += "*";
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
@@ -32,11 +31,10 @@ import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SVG } from "./background";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -50,6 +48,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
@@ -96,69 +95,77 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
flex: 0.35,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.nav,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
src={SVG(colors.night ? colors.icon : "black")}
|
||||
height={700}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Create your {"\n"}account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: Dimensions.get("window").height < 700 ? -75 : 15
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={30}
|
||||
color={colors.heading}
|
||||
>
|
||||
Create your account
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
textAlign: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
Already have an account? Log in
|
||||
</Paragraph>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
padding: 12,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.bg,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
alignSelf: "center",
|
||||
flex: 0.6
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -217,37 +224,13 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={5}
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
signup();
|
||||
}}
|
||||
type="accent"
|
||||
title={loading ? null : "Agree and continue"}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
alignSelf: "center",
|
||||
marginBottom: 20
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
@@ -263,7 +246,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}}
|
||||
color={colors.accent}
|
||||
>
|
||||
terms of service{" "}
|
||||
Terms of Service{" "}
|
||||
</Paragraph>
|
||||
and{" "}
|
||||
<Paragraph
|
||||
@@ -276,11 +259,46 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}}
|
||||
color={colors.accent}
|
||||
>
|
||||
privacy policy.
|
||||
</Paragraph>
|
||||
Privacy Policy.
|
||||
</Paragraph>{" "}
|
||||
You also agree to recieve marketing emails from us which you can
|
||||
opt-out of from app settings.
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
Already have an account?{" "}
|
||||
<Paragraph size={SIZE.xs + 1} style={{ color: colors.accent }}>
|
||||
Login
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -60,6 +61,7 @@ const DialogProvider = () => {
|
||||
{loading ? null : <Expiring />}
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { View } from "react-native";
|
||||
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
@@ -30,6 +31,7 @@ import Navigation from "../../../services/navigation";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eCloseAddTopicDialog,
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddTopicDialog
|
||||
} from "../../../utils/events";
|
||||
import { sleep } from "../../../utils/time";
|
||||
@@ -40,6 +42,7 @@ import DialogHeader from "../../dialog/dialog-header";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { Toast } from "../../toast";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
export class AddTopicDialog extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -78,9 +81,11 @@ export class AddTopicDialog extends React.Component {
|
||||
}
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate("Notebooks", "Notebook", "TopicNotes");
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
});
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
useRelationStore.getState().update();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
324
apps/mobile/app/components/dialogs/pdf-preview/index.js
Normal file
324
apps/mobile/app/components/dialogs/pdf-preview/index.js
Normal file
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Dimensions, TextInput, View } from "react-native";
|
||||
import {
|
||||
addOrientationListener,
|
||||
removeOrientationListener
|
||||
} from "react-native-orientation";
|
||||
import Pdf from "react-native-pdf";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import downloadAttachment from "../../../common/filesystem/download-attachment";
|
||||
import { cacheDir } from "../../../common/filesystem/utils";
|
||||
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import SheetProvider from "../../sheet-provider";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
|
||||
const attachmentSnapshotsKey = "___attachmentsnapshots";
|
||||
const usePDFSnapshot = (attachment) => {
|
||||
const snapshots = useRef(MMKV.getMap(attachmentSnapshotsKey) || {});
|
||||
const snapshot = useRef(snapshots[attachment?.id]);
|
||||
|
||||
function saveSnapshot(ss) {
|
||||
if (!attachment) return;
|
||||
snapshots.current[attachment.id] = ss;
|
||||
MMKV.setMap(attachmentSnapshotsKey, snapshots.current);
|
||||
snapshot.current = snapshots.current[attachment.id];
|
||||
}
|
||||
|
||||
return [snapshot, saveSnapshot];
|
||||
};
|
||||
|
||||
const PDFPreview = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pdfSource, setPDFSource] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [width, setWidth] = useState(WIN_WIDTH);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const inputRef = useRef();
|
||||
const pdfRef = useRef();
|
||||
const [attachment, setAttachment] = useState(null);
|
||||
const [password, setPassword] = useState("");
|
||||
const [progress] = useAttachmentProgress(attachment);
|
||||
const [snapshot, saveSnapshot] = usePDFSnapshot(attachment);
|
||||
const snapshotValue = useRef(snapshot.current);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("PDFPreview", open);
|
||||
return () => {
|
||||
eUnSubscribeEvent("PDFPreview", open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onOrientationChange = (o) => {
|
||||
if (o.includes("LANDSCAPE")) {
|
||||
setWidth(WIN_HEIGHT);
|
||||
} else {
|
||||
setWidth(WIN_WIDTH);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
addOrientationListener(onOrientationChange);
|
||||
return () => {
|
||||
removeOrientationListener(onOrientationChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = async (attachment) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
setAttachment(attachment);
|
||||
let hash = attachment.metadata.hash;
|
||||
if (!hash) return;
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
snapshotValue.current = snapshot.current;
|
||||
setPDFSource("file://" + path);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setPDFSource(null);
|
||||
setVisible(false);
|
||||
setPassword("");
|
||||
};
|
||||
|
||||
const onError = async (error) => {
|
||||
if (error?.message === "Password required or incorrect password.") {
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
context: attachment?.metadata?.hash,
|
||||
input: true,
|
||||
inputPlaceholder: "Enter password",
|
||||
positiveText: "Unlock",
|
||||
title: "Decrypt",
|
||||
paragraph: "Please input password to view pdf.",
|
||||
positivePress: (value) => {
|
||||
setTimeout(() => {
|
||||
setPassword(value);
|
||||
});
|
||||
},
|
||||
onClose: () => {
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
|
||||
<SheetProvider context={attachment?.metadata?.hash} />
|
||||
<Dialog context={attachment?.metadata?.hash} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "black"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Animated.View
|
||||
exiting={FadeOut}
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
indeterminate={!progress}
|
||||
color={colors.accent}
|
||||
borderColor="transparent"
|
||||
progress={parseInt(progress?.value || "100") / 100}
|
||||
/>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
color={colors.light}
|
||||
>
|
||||
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
|
||||
... Please wait
|
||||
</Paragraph>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
marginTop: insets.top,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 12,
|
||||
paddingLeft: 6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color={colors.light}
|
||||
name="arrow-left"
|
||||
onPress={close}
|
||||
customStyle={{
|
||||
marginRight: 12
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: 12
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
defaultValue={currentPage + ""}
|
||||
style={{
|
||||
color: colors.pri,
|
||||
padding: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
paddingVertical: 0,
|
||||
height: 25,
|
||||
backgroundColor: colors.nav,
|
||||
width: 40,
|
||||
textAlign: "center",
|
||||
marginRight: 4,
|
||||
borderRadius: 3,
|
||||
fontFamily: "OpenSans-Regular"
|
||||
}}
|
||||
selectTextOnFocus
|
||||
keyboardType="decimal-pad"
|
||||
onSubmitEditing={(event) => {
|
||||
setCurrentPage(event.nativeEvent.text);
|
||||
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
|
||||
}}
|
||||
blurOnSubmit
|
||||
/>
|
||||
<Paragraph color={colors.light}>/{numPages}</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color={colors.light}
|
||||
name="download"
|
||||
onPress={() => {
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{pdfSource ? (
|
||||
<Animated.View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
entering={FadeIn}
|
||||
>
|
||||
<Pdf
|
||||
source={{
|
||||
uri: pdfSource
|
||||
}}
|
||||
ref={pdfRef}
|
||||
onLoadComplete={(numberOfPages) => {
|
||||
setNumPages(numberOfPages);
|
||||
}}
|
||||
onPageChanged={(page) => {
|
||||
setCurrentPage(page);
|
||||
inputRef.current?.setNativeProps({
|
||||
text: page + ""
|
||||
});
|
||||
saveSnapshot({
|
||||
currentPage: page,
|
||||
scale: snapshot?.current?.scale
|
||||
});
|
||||
}}
|
||||
// scale={snapshotValue.current?.scale}
|
||||
// onScaleChanged={(scale) => {
|
||||
// saveSnapshot({
|
||||
// currentPage: snapshot?.current?.currentPage,
|
||||
// scale: scale
|
||||
// });
|
||||
// }}
|
||||
page={snapshotValue?.current?.currentPage}
|
||||
password={password}
|
||||
maxScale={6}
|
||||
onError={onError}
|
||||
onPressLink={(uri) => {
|
||||
console.log(`Link pressed: ${uri}`);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width,
|
||||
height: Dimensions.get("window").height
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</BaseDialog>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFPreview;
|
||||
@@ -86,7 +86,7 @@ export const ProFeatures = ({ count = 6 }) => {
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
}}
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.icon
|
||||
|
||||
@@ -207,13 +207,7 @@ export class VaultDialog extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"TopicNotes",
|
||||
"TaggedNotes",
|
||||
"ColoredNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,12 +17,14 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import notifee from "@notifee/react-native";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import { checkVersion } from "react-native-check-version";
|
||||
import Config from "react-native-config";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger, db, loadDatabase } from "../../common/database";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
@@ -35,10 +37,11 @@ import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { AndroidModule } from "../../utils";
|
||||
import { eOpenAnnouncementDialog } from "../../utils/events";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { SVG } from "../auth/background";
|
||||
import Migrate from "../sheets/migrate";
|
||||
import NewFeature from "../sheets/new-feature/index";
|
||||
import { Update } from "../sheets/update";
|
||||
@@ -46,13 +49,9 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import Config from "react-native-config";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
import notifee from "@notifee/react-native";
|
||||
|
||||
const Launcher = React.memo(
|
||||
function Launcher() {
|
||||
@@ -69,7 +68,8 @@ const Launcher = React.memo(
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const dbInitCompleted = useRef(false);
|
||||
const verifying = useRef(false);
|
||||
|
||||
const loadNotes = useCallback(async () => {
|
||||
if (verifyUser) {
|
||||
return;
|
||||
@@ -80,28 +80,25 @@ const Launcher = React.memo(
|
||||
initialize();
|
||||
setImmediate(() => {
|
||||
setLoading(false);
|
||||
if (!dbInitCompleted.current) {
|
||||
setImmediate(() => doAppLoadActions());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [doAppLoadActions, setLoading, verifyUser]);
|
||||
}, [setLoading, verifyUser]);
|
||||
|
||||
const init = useCallback(async () => {
|
||||
if (!dbInitCompleted.current) {
|
||||
await RNBootSplash.hide({ fade: true });
|
||||
await loadDatabase();
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
await db.init();
|
||||
dbInitCompleted.current = true;
|
||||
}
|
||||
|
||||
if (db.migrations.required() && !verifyUser) {
|
||||
presentSheet({
|
||||
component: <Migrate />,
|
||||
onClose: async () => {
|
||||
await db.init();
|
||||
if (!db.isInitialized) {
|
||||
await db.init();
|
||||
}
|
||||
loadNotes();
|
||||
},
|
||||
disableClosing: true
|
||||
@@ -120,9 +117,6 @@ const Launcher = React.memo(
|
||||
if (!loading) {
|
||||
doAppLoadActions();
|
||||
}
|
||||
return () => {
|
||||
dbInitCompleted.current = false;
|
||||
};
|
||||
}, [doAppLoadActions, loading]);
|
||||
|
||||
const doAppLoadActions = useCallback(async () => {
|
||||
@@ -136,7 +130,6 @@ const Launcher = React.memo(
|
||||
if (NewFeature.present()) return;
|
||||
if (await checkAppUpdateAvailable()) return;
|
||||
if (await checkForRateAppRequest()) return;
|
||||
if (await checkNeedsBackup()) return;
|
||||
if (await PremiumService.getRemainingTrialDaysStatus()) return;
|
||||
|
||||
if (introCompleted) {
|
||||
@@ -179,25 +172,13 @@ const Launcher = React.memo(
|
||||
return false;
|
||||
};
|
||||
|
||||
const checkNeedsBackup = async () => {
|
||||
return false;
|
||||
// let { nextBackupRequestTime, reminder } = SettingsService.get();
|
||||
// if (reminder === 'off' || !reminder) {
|
||||
// if (nextBackupRequestTime < Date.now()) {
|
||||
// presentSheet({
|
||||
// title: 'Backup & restore',
|
||||
// paragraph: 'Please enable automatic backups to keep your data safe',
|
||||
// component: <SettingsBackupAndRestore isSheet={true} />
|
||||
// });
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
};
|
||||
|
||||
const onUnlockBiometrics = useCallback(async () => {
|
||||
if (!(await BiometricService.isBiometryAvailable())) return;
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await AndroidModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
}
|
||||
|
||||
let verified = await BiometricService.validateUser(
|
||||
"Unlock to access your notes",
|
||||
""
|
||||
@@ -206,6 +187,9 @@ const Launcher = React.memo(
|
||||
setVerifyUser(false);
|
||||
enabled(false);
|
||||
password.current = null;
|
||||
setTimeout(() => {
|
||||
verifying.current = false;
|
||||
}, 1);
|
||||
}
|
||||
}, [setVerifyUser]);
|
||||
|
||||
@@ -214,7 +198,10 @@ const Launcher = React.memo(
|
||||
}, [init, verifyUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (verifying.current || useUserStore.getState().shouldBlockVerifyUser)
|
||||
return;
|
||||
if (verifyUser && appState === "active") {
|
||||
verifying.current = true;
|
||||
onUnlockBiometrics();
|
||||
}
|
||||
}, [appState, onUnlockBiometrics, verifyUser]);
|
||||
@@ -240,18 +227,10 @@ const Launcher = React.memo(
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<SvgView src={SVG(colors.night ? "white" : "black")} height={700} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -287,14 +266,13 @@ const Launcher = React.memo(
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Unlock to access your notes
|
||||
Unlock your notes
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
fontSize: SIZE.md,
|
||||
maxWidth: "90%"
|
||||
}}
|
||||
>
|
||||
@@ -305,8 +283,7 @@ const Launcher = React.memo(
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.bg,
|
||||
flexGrow: 1
|
||||
backgroundColor: colors.bg
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
@@ -346,14 +323,9 @@ const Launcher = React.memo(
|
||||
<Button
|
||||
title="Unlock with Biometrics"
|
||||
width={250}
|
||||
height={45}
|
||||
style={{
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={onUnlockBiometrics}
|
||||
icon={"fingerprint"}
|
||||
type={user ? "grayAccent" : "accent"}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { db } from "../../../common/database";
|
||||
import Notebook from "../../../screens/notebook";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
@@ -52,12 +53,14 @@ const showActionSheet = (item) => {
|
||||
|
||||
function getNotebook(item) {
|
||||
const isTrash = item.type === "trash";
|
||||
const currentId = useNavigationStore.getState().currentScreen.id;
|
||||
if (isTrash) return [];
|
||||
const items = [];
|
||||
const notebooks = db.relations.to(item, "notebook") || [];
|
||||
|
||||
for (let notebook of notebooks) {
|
||||
if (items.length > 1) break;
|
||||
if (notebook.id === currentId) continue;
|
||||
items.push(notebook);
|
||||
}
|
||||
|
||||
@@ -68,6 +71,7 @@ function getNotebook(item) {
|
||||
if (!notebook) continue;
|
||||
for (let topicId of nb.topics) {
|
||||
if (items.length > 1) break;
|
||||
if (topicId === currentId) continue;
|
||||
const topic = notebook.topics.find((t) => t.id === topicId);
|
||||
if (!topic) continue;
|
||||
items.push(topic);
|
||||
@@ -77,10 +81,20 @@ function getNotebook(item) {
|
||||
return items;
|
||||
}
|
||||
|
||||
function getTags(item) {
|
||||
const noteTags = item.tags?.slice(0, 3) || [];
|
||||
const tags = [];
|
||||
for (const tagName of noteTags) {
|
||||
const tag = db.tags.tag(tagName);
|
||||
if (!tag) continue;
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
const NoteItem = ({
|
||||
item,
|
||||
isTrash,
|
||||
tags,
|
||||
dateBy = "dateCreated",
|
||||
noOpen = false
|
||||
}) => {
|
||||
@@ -96,6 +110,7 @@ const NoteItem = ({
|
||||
const reminders = db.relations.from(item, "reminder");
|
||||
const reminder = getUpcomingReminder(reminders);
|
||||
const noteColor = COLORS_NOTE[item.color?.toLowerCase()];
|
||||
const tags = getTags(item);
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
@@ -117,7 +132,12 @@ const NoteItem = ({
|
||||
>
|
||||
{notebooks?.map((item) => (
|
||||
<Button
|
||||
title={item.title}
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "topic" ? "bookmark" : "book-outline"}
|
||||
|
||||
@@ -100,7 +100,7 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo(
|
||||
function NoteWrapper({ item, index, tags, dateBy, isSheet }) {
|
||||
function NoteWrapper({ item, index, dateBy, isSheet }) {
|
||||
const isTrash = item.type === "trash";
|
||||
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
|
||||
|
||||
@@ -113,7 +113,7 @@ export const NoteWrapper = React.memo(
|
||||
isSheet={isSheet}
|
||||
item={item}
|
||||
>
|
||||
<NoteItem item={item} dateBy={dateBy} tags={tags} isTrash={isTrash} />
|
||||
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
|
||||
</SelectionWrapper>
|
||||
);
|
||||
},
|
||||
@@ -125,10 +125,6 @@ export const NoteWrapper = React.memo(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (JSON.stringify(prev.tags) !== JSON.stringify(next.tags)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prev.item !== next.item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,16 +64,7 @@ export const openNotebookTopic = (item) => {
|
||||
negativeText: "Delete",
|
||||
positivePress: async () => {
|
||||
await db.trash.restore(item.id);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Tags",
|
||||
"Notes",
|
||||
"Notebooks",
|
||||
"Favorites",
|
||||
"Trash",
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
|
||||
@@ -108,7 +108,7 @@ const ReminderItem = React.memo(
|
||||
color={colors.errorText}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
@@ -133,7 +133,7 @@ const ReminderItem = React.memo(
|
||||
>
|
||||
<Icon name="reload" size={SIZE.md} color={colors.accent} />
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
@@ -147,7 +147,7 @@ const ReminderItem = React.memo(
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
checkIsActive={false}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
@@ -57,25 +57,10 @@ const RenderItem = ({ item, index, type, ...restArgs }) => {
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
const totalNotes = getTotalNotes(item);
|
||||
const tags =
|
||||
item.tags
|
||||
?.slice(0, 3)
|
||||
?.map((item) => {
|
||||
let tag = db.tags.tag(item);
|
||||
|
||||
if (!tag) return null;
|
||||
return {
|
||||
title: tag.title,
|
||||
id: tag.id,
|
||||
alias: tag.alias
|
||||
};
|
||||
})
|
||||
.filter((t) => t !== null) || [];
|
||||
|
||||
return (
|
||||
<Item
|
||||
item={item}
|
||||
tags={tags}
|
||||
dateBy={dateBy}
|
||||
index={index}
|
||||
type={type}
|
||||
@@ -154,13 +139,20 @@ const List = ({
|
||||
[screen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSendEvent(eScrollEvent, {
|
||||
y: 0,
|
||||
screen
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
let styles = {
|
||||
width: "100%",
|
||||
minHeight: 1,
|
||||
minWidth: 1
|
||||
};
|
||||
|
||||
const _keyExtractor = (item) => item.id || item.title;
|
||||
const ListView = ScrollComponent ? ScrollComponent : FlashList;
|
||||
return (
|
||||
<>
|
||||
@@ -226,12 +218,14 @@ const List = ({
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
{listData ? (
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -85,13 +85,7 @@ const MergeConflicts = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
if (editorController.current?.note?.id === note.id) {
|
||||
// reload the note in editor
|
||||
eSendEvent(eOnLoadNote, {
|
||||
|
||||
@@ -44,16 +44,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
async function restore() {
|
||||
if (note && note.type === "trash") {
|
||||
await db.trash.restore(note.id);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Tags",
|
||||
"Notes",
|
||||
"Notebooks",
|
||||
"Favorites",
|
||||
"Trash",
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
@@ -73,13 +64,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
}
|
||||
eSendEvent(eCloseSheet, "note_history");
|
||||
eSendEvent(eCloseSheet);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
ToastEvent.show({
|
||||
heading: "Note restored successfully",
|
||||
|
||||
@@ -18,13 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { ScrollView } from "react-native";
|
||||
import { FeatureBlock } from "./feature";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
export const CompactFeatures = ({
|
||||
vertical,
|
||||
features = [],
|
||||
maxHeight = 500,
|
||||
maxHeight = 600,
|
||||
scrollRef
|
||||
}) => {
|
||||
let data = vertical
|
||||
@@ -80,14 +80,11 @@ export const CompactFeatures = ({
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal={!vertical}
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={() => {
|
||||
scrollRef?.current?.handleChildScrollEnd();
|
||||
}}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxHeight: maxHeight
|
||||
maxHeight: maxHeight,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{data.map((item) => (
|
||||
|
||||
@@ -143,7 +143,7 @@ export const Expiring = () => {
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog, promo);
|
||||
}}
|
||||
size={SIZE.xs + 1}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.icon,
|
||||
|
||||
@@ -41,18 +41,19 @@ export const FeatureBlock = ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 10
|
||||
marginBottom: 10,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 10,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Icon color={colors.accent} name="check" size={SIZE.lg} />
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginLeft: 5,
|
||||
flexShrink: 1
|
||||
}}
|
||||
size={SIZE.md}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{content}
|
||||
</Paragraph>
|
||||
|
||||
@@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
|
||||
import { editorState } from "../../screens/editor/tiptap/utils";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -31,6 +31,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { getElevation } from "../../utils";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseSheet,
|
||||
eOpenPremiumDialog,
|
||||
eShowGetPremium
|
||||
} from "../../utils/events";
|
||||
@@ -39,12 +40,12 @@ import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [msg, setMsg] = useState(null);
|
||||
const timer = useRef();
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const open = useCallback(
|
||||
(event) => {
|
||||
@@ -72,7 +73,6 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowGetPremium, open);
|
||||
return () => {
|
||||
clearTimeout(timer.current);
|
||||
eUnSubscribeEvent(eShowGetPremium, open);
|
||||
};
|
||||
}, [open]);
|
||||
@@ -80,9 +80,7 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
const onPress = async () => {
|
||||
open(null);
|
||||
eSendEvent(eCloseActionSheet);
|
||||
if (editorState().isFocused) {
|
||||
//tiny.call(EditorWebView, tiny.blur);
|
||||
}
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
};
|
||||
@@ -102,9 +100,13 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
top: offset,
|
||||
top: offset + keyboard.keyboardHeight,
|
||||
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setMsg(null);
|
||||
clearTimeout(timer.current);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -232,8 +232,7 @@ export const PricingPlans = ({
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
marginBottom: 15
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -398,8 +397,7 @@ export const PricingPlans = ({
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 30,
|
||||
marginBottom: 10,
|
||||
borderRadius: 100
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
{Platform.OS !== "ios" &&
|
||||
|
||||
@@ -43,13 +43,7 @@ export const ColorTags = ({ item }) => {
|
||||
let _note = db.notes.note(note.id).data;
|
||||
setNote({ ..._note });
|
||||
setColorNotes();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(refreshNotesPage);
|
||||
};
|
||||
|
||||
|
||||
@@ -62,10 +62,10 @@ export const DateMeta = ({ item }) => {
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{timeConverter(item[key])}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -108,7 +108,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -115,12 +115,12 @@ export const Items = ({ item, buttons, close }) => {
|
||||
onPress={item.func}
|
||||
key={item.id}
|
||||
testID={"icon-" + item.id}
|
||||
activeOpacity={1}
|
||||
customStyle={{
|
||||
alignItems: "center",
|
||||
width: topBarItemWidth,
|
||||
marginBottom: 10,
|
||||
alignSelf: "flex-start",
|
||||
marginRight: isLast ? 0 : 10,
|
||||
backgroundColor: "transparent"
|
||||
paddingHorizontal: 0,
|
||||
width: topBarItemWidth
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
@@ -150,11 +150,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph
|
||||
size={SIZE.xxs + 1}
|
||||
style={{ textAlign: "center" }}
|
||||
textBreakStrategy="simple"
|
||||
>
|
||||
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
@@ -187,7 +183,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
horizontal
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
{topBarItems.map(renderTopBarItem)}
|
||||
|
||||
@@ -23,13 +23,18 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import Notebook from "../../screens/notebook";
|
||||
import { TopicNotes } from "../../screens/notes/topic-notes";
|
||||
import { presentSheet, ToastEvent } from "../../services/event-manager";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useNotebookStore } from "../../stores/use-notebook-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
|
||||
export default function Notebooks({ note, close, full }) {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -112,6 +117,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
size={SIZE.sm}
|
||||
onPress={() => {
|
||||
navigateNotebook(item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
@@ -134,6 +140,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
key={topic.id}
|
||||
onPress={() => {
|
||||
navigateTopic(topic.id, item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
onLongPress={async () => {
|
||||
@@ -145,13 +152,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
note
|
||||
);
|
||||
useNotebookStore.getState().setNotebooks();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
ToastEvent.show({
|
||||
heading: "Note removed from topic",
|
||||
context: "local",
|
||||
@@ -161,7 +162,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
title={topic.title}
|
||||
type="gray"
|
||||
height={30}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
icon="bookmark-outline"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
@@ -190,7 +191,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
{noteNotebooks.length > 1 && !full ? (
|
||||
<Button
|
||||
title={`See all linked notebooks`}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
marginRight: 12,
|
||||
|
||||
@@ -91,7 +91,7 @@ export const Synced = ({ item, close }) => {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
title="Learn more"
|
||||
height={30}
|
||||
type="grayAccent"
|
||||
|
||||
@@ -47,7 +47,7 @@ export const Tags = ({ item, close }) => {
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTagsSheet.present(item);
|
||||
ManageTagsSheet.present([item]);
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.accent
|
||||
@@ -57,7 +57,7 @@ export const Tags = ({ item, close }) => {
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
height={30}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
@@ -107,7 +107,7 @@ const TagItem = ({ tag, close }) => {
|
||||
title={"#" + tag}
|
||||
type="grayBg"
|
||||
height={20}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
style={style}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
|
||||
@@ -39,7 +39,7 @@ export const Topics = ({ item, close }) => {
|
||||
height={30}
|
||||
onPress={() => open(topic)}
|
||||
icon="bookmark-outline"
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
paddingHorizontal: 8,
|
||||
|
||||
@@ -21,21 +21,22 @@ import React, { useCallback, useEffect } from "react";
|
||||
import { BackHandler, Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SearchService from "../../services/search";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eOpenMoveNoteDialog } from "../../utils/events";
|
||||
import { deleteItems } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import MoveNoteSheet from "../sheets/add-to";
|
||||
import ExportNotesSheet from "../sheets/export-notes";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
|
||||
export const SelectionHeader = React.memo(() => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -64,13 +65,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
selectedItemsList.forEach((item) => {
|
||||
db.notes.note(item.id).favorite();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
};
|
||||
@@ -82,16 +77,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.restore(...noteIds);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Trash",
|
||||
"Notebooks",
|
||||
"Tags"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
clearSelection();
|
||||
ToastEvent.show({
|
||||
@@ -118,16 +104,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.delete(...noteIds);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Trash",
|
||||
"Notebooks",
|
||||
"Tags"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
},
|
||||
@@ -233,11 +210,25 @@ export const SelectionHeader = React.memo(() => {
|
||||
screen === "Notebook" ||
|
||||
screen === "Reminders" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await sleep(100);
|
||||
ManageTagsSheet.present(selectedItemsList);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.pri}
|
||||
tooltipText="Manage tags"
|
||||
tooltipPosition={4}
|
||||
name="pound"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
//setSelectionMode(false);
|
||||
await sleep(100);
|
||||
eSendEvent(eOpenMoveNoteDialog);
|
||||
MoveNoteSheet.present();
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
@@ -248,6 +239,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
name="plus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
@@ -288,15 +280,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
);
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -196,12 +196,11 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
accentColor={data.iconColor || "accent"}
|
||||
accentText="light"
|
||||
type="accent"
|
||||
height={45}
|
||||
height={40}
|
||||
width={250}
|
||||
style={{
|
||||
borderRadius: 100
|
||||
marginBottom: 25
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -25,22 +25,22 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import { ToastEvent, presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { ph, pv, SIZE } from "../../../utils/size";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
|
||||
let refs = [];
|
||||
export class AddNotebookSheet extends React.Component {
|
||||
@@ -92,7 +92,7 @@ export class AddNotebookSheet extends React.Component {
|
||||
|
||||
close = () => {
|
||||
refs = [];
|
||||
this.props.close();
|
||||
this.props.close(true);
|
||||
};
|
||||
|
||||
onDelete = (index) => {
|
||||
@@ -182,6 +182,7 @@ export class AddNotebookSheet extends React.Component {
|
||||
});
|
||||
|
||||
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
|
||||
this.close();
|
||||
} else {
|
||||
newNotebookId = await db.notebooks.add({
|
||||
title: this.title,
|
||||
@@ -191,14 +192,7 @@ export class AddNotebookSheet extends React.Component {
|
||||
});
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
|
||||
};
|
||||
@@ -251,13 +245,34 @@ export class AddNotebookSheet extends React.Component {
|
||||
willFocus && this.topicInputRef.current?.focus();
|
||||
};
|
||||
|
||||
renderTopicItem = ({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={this.props.colors}
|
||||
/>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
const { topics, topicInputFocused, notebook } = this.state;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "96%",
|
||||
maxHeight: DDS.isTab ? "90%" : "97%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
@@ -272,17 +287,33 @@ export class AddNotebookSheet extends React.Component {
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<DialogHeader
|
||||
title={
|
||||
notebook && notebook.dateCreated ? "Edit Notebook" : "New Notebook"
|
||||
}
|
||||
paragraph={
|
||||
notebook && notebook.dateCreated
|
||||
? "You are editing " + this.title + " notebook."
|
||||
: "Notebooks are the best way to organize your notes."
|
||||
}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook && notebook.dateCreated
|
||||
? "Edit Notebook"
|
||||
: "New Notebook"}
|
||||
</Heading>
|
||||
<Button
|
||||
title="Save"
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
onPress={this.addNewNotebook}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
@@ -350,48 +381,11 @@ export class AddNotebookSheet extends React.Component {
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
renderItem={({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={colors}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
width="100%"
|
||||
fontSize={SIZE.md}
|
||||
title={
|
||||
notebook && notebook.dateCreated
|
||||
? "Save changes"
|
||||
: "Create notebook"
|
||||
ListFooterComponent={
|
||||
topics.length === 0 ? null : <View style={{ height: 50 }} />
|
||||
}
|
||||
type="accent"
|
||||
onPress={this.addNewNotebook}
|
||||
renderItem={this.renderTopicItem}
|
||||
/>
|
||||
{/*
|
||||
{Platform.OS === 'ios' && (
|
||||
<View
|
||||
style={{
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,18 @@ import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SearchService from "../../../services/search";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { eOnTopicSheetUpdate } from "../../../utils/events";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
@@ -37,6 +42,7 @@ import { SelectionProvider } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { ListItem } from "./list-item";
|
||||
import { useItemSelectionStore } from "./store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -258,7 +264,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
if (itemState[id] === "selected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
db.relations.add(item, { id: noteId, type: "note" });
|
||||
await db.relations.add(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.addToNotebook(
|
||||
@@ -273,7 +279,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
} else if (itemState[id] === "deselected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
await db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
@@ -287,16 +293,11 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebook"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setNotebooks();
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
SearchService.updateAndSearch();
|
||||
useRelationStore.getState().update();
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
|
||||
const ExportNotesSheet = ({ notes, update }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -72,6 +73,7 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
update({ disableClosing: false });
|
||||
setComplete(true);
|
||||
setExporting(false);
|
||||
requestInAppReview();
|
||||
};
|
||||
|
||||
const actions = [
|
||||
|
||||
@@ -22,7 +22,7 @@ import { View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import { ToastEvent, presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useTagStore } from "../../../stores/use-tag-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
@@ -31,9 +31,10 @@ import Input from "../../ui/input";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
const ManageTagsSheet = (props) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [note, setNote] = useState(props.note);
|
||||
const [notes, setNotes] = useState(props.notes || []);
|
||||
const allTags = useTagStore((state) => state.tags);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [query, setQuery] = useState(null);
|
||||
@@ -42,7 +43,7 @@ const ManageTagsSheet = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
sortTags();
|
||||
}, [allTags, note, query, sortTags]);
|
||||
}, [allTags, notes, query, sortTags]);
|
||||
|
||||
const sortTags = useCallback(() => {
|
||||
let _tags = [...allTags];
|
||||
@@ -51,13 +52,14 @@ const ManageTagsSheet = (props) => {
|
||||
if (query) {
|
||||
_tags = _tags.filter((t) => t.title.startsWith(query));
|
||||
}
|
||||
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
|
||||
|
||||
if (!note || !note.tags) {
|
||||
if (!tagsMerged || !tagsMerged.length) {
|
||||
setTags(_tags);
|
||||
return;
|
||||
}
|
||||
let noteTags = [];
|
||||
for (let tag of note.tags) {
|
||||
for (let tag of tagsMerged) {
|
||||
let index = _tags.findIndex((t) => t.title === tag);
|
||||
if (index !== -1) {
|
||||
noteTags.push(_tags[index]);
|
||||
@@ -67,7 +69,7 @@ const ManageTagsSheet = (props) => {
|
||||
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
|
||||
let combinedTags = [...noteTags, ..._tags];
|
||||
setTags(combinedTags);
|
||||
}, [allTags, note, query]);
|
||||
}, [allTags, notes, query]);
|
||||
|
||||
useEffect(() => {
|
||||
useTagStore.getState().setTags();
|
||||
@@ -85,15 +87,23 @@ const ManageTagsSheet = (props) => {
|
||||
}
|
||||
|
||||
let tag = _query;
|
||||
setNote({ ...note, tags: note.tags ? [...note.tags, tag] : [tag] });
|
||||
setNotes(
|
||||
notes.map((note) => ({
|
||||
...note,
|
||||
tags: note.tags ? [...note.tags, tag] : [tag]
|
||||
}))
|
||||
);
|
||||
|
||||
setQuery(null);
|
||||
inputRef.current?.setNativeProps({
|
||||
text: ""
|
||||
});
|
||||
try {
|
||||
await db.notes.note(note.id).tag(tag);
|
||||
for (let note of notes) {
|
||||
await db.notes.note(note.id).tag(tag);
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
setNote(db.notes.note(note.id).data);
|
||||
setNotes(notes.map((note) => db.notes.note(note.id).data));
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Cannot add tag",
|
||||
@@ -103,15 +113,7 @@ const ManageTagsSheet = (props) => {
|
||||
});
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -188,49 +190,54 @@ const ManageTagsSheet = (props) => {
|
||||
) : null}
|
||||
|
||||
{tags.map((item) => (
|
||||
<TagItem key={item.title} tag={item} note={note} setNote={setNote} />
|
||||
<TagItem
|
||||
key={item.title}
|
||||
tag={item}
|
||||
notes={notes}
|
||||
setNotes={setNotes}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ManageTagsSheet.present = (note) => {
|
||||
ManageTagsSheet.present = (notes) => {
|
||||
presentSheet({
|
||||
component: (ref) => {
|
||||
return <ManageTagsSheet actionSheetRef={ref} note={note} />;
|
||||
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default ManageTagsSheet;
|
||||
|
||||
const TagItem = ({ tag, note, setNote }) => {
|
||||
const TagItem = ({ tag, notes, setNotes }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
|
||||
const someNotesTagged = notes.some(
|
||||
(note) => note.tags?.indexOf(tag.title) !== -1
|
||||
);
|
||||
const allNotesTagged = notes.every(
|
||||
(note) => note.tags?.indexOf(tag.title) !== -1
|
||||
);
|
||||
const onPress = async () => {
|
||||
let prevNote = { ...note };
|
||||
try {
|
||||
if (prevNote.tags.indexOf(tag.title) !== -1) {
|
||||
await db.notes
|
||||
.note(note.id)
|
||||
.untag(prevNote.tags[prevNote.tags.indexOf(tag.title)]);
|
||||
} else {
|
||||
await db.notes.note(note.id).tag(tag.title);
|
||||
for (let note of notes) {
|
||||
try {
|
||||
if (someNotesTagged) {
|
||||
await db.notes
|
||||
.note(note.id)
|
||||
.untag(note.tags[note.tags.indexOf(tag.title)]);
|
||||
} else {
|
||||
await db.notes.note(note.id).tag(tag.title);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
setNote(db.notes.note(note.id).data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
setNotes(notes.map((note) => db.notes.note(note.id).data));
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}, 1);
|
||||
};
|
||||
|
||||
@@ -239,39 +246,36 @@ const TagItem = ({ tag, note, setNote }) => {
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
justifyContent: "flex-start",
|
||||
height: 40
|
||||
}}
|
||||
onPress={onPress}
|
||||
type={
|
||||
note && note.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? "shade"
|
||||
: "grayBg"
|
||||
}
|
||||
type="gray"
|
||||
>
|
||||
<Heading
|
||||
size={SIZE.sm}
|
||||
color={
|
||||
note && note?.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? colors.accent
|
||||
: colors.pri
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={someNotesTagged || allNotesTagged ? colors.accent : colors.icon}
|
||||
testID={
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
>
|
||||
{"#" + tag.title}
|
||||
</Heading>
|
||||
<Icon
|
||||
name={
|
||||
note && note?.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? "minus"
|
||||
: "plus"
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
color={
|
||||
note && note?.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? colors.accent
|
||||
: colors.accent
|
||||
}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
<Paragraph size={SIZE.sm}>{"#" + tag.title}</Paragraph>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -122,15 +122,7 @@ export const MoveNotes = ({
|
||||
db.notebooks?.notebook(currentNotebook.id).data as NotebookType
|
||||
);
|
||||
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -166,11 +158,7 @@ export const MoveNotes = ({
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
{item.type == "note" && item.headline ? (
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={colors.icon}
|
||||
size={SIZE.xs + 1}
|
||||
>
|
||||
<Paragraph numberOfLines={1} color={colors.icon} size={SIZE.xs}>
|
||||
{item.headline}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
@@ -318,15 +306,7 @@ export const MoveNotes = ({
|
||||
},
|
||||
...selectedNoteIds
|
||||
);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
SearchService.updateAndSearch();
|
||||
eSendEvent(eCloseSheet);
|
||||
}}
|
||||
|
||||
@@ -35,6 +35,7 @@ import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
|
||||
const PublishNoteSheet = ({ note: item, update }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -63,15 +64,10 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
password: isLocked && passwordValue.current
|
||||
});
|
||||
setNote(db.notes.note(note.id)?.data);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setPublishLoading(false);
|
||||
}
|
||||
requestInAppReview();
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Could not publish note",
|
||||
@@ -95,13 +91,7 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
if (note?.id) {
|
||||
await db.monographs.unpublish(note.id);
|
||||
setNote(db.notes.note(note.id)?.data);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setPublishLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -43,8 +43,7 @@ import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import { QRCode } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
let RNFetchBlob;
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
class RecoveryKeySheet extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -115,7 +114,6 @@ class RecoveryKeySheet extends React.Component {
|
||||
this.svg.current?.toDataURL(async (data) => {
|
||||
try {
|
||||
let path;
|
||||
RNFetchBlob = (await import("rn-fetch-blob")).default;
|
||||
let fileName = "nn_" + this.user.email + "_recovery_key_qrcode";
|
||||
fileName = sanitizeFilename(fileName, { replacement: "_" });
|
||||
fileName = fileName + ".png";
|
||||
@@ -151,7 +149,6 @@ class RecoveryKeySheet extends React.Component {
|
||||
fileName = sanitizeFilename(fileName, { replacement: "_" });
|
||||
fileName = fileName + ".txt";
|
||||
|
||||
RNFetchBlob = (await import("rn-fetch-blob")).default;
|
||||
if (Platform.OS === "android") {
|
||||
let file = await ScopedStorage.createDocument(
|
||||
fileName,
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function ReminderNotify({
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1}>Remind me in:</Paragraph>
|
||||
<Paragraph size={SIZE.xs}>Remind me in:</Paragraph>
|
||||
{QuickActions.map((item) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -127,7 +127,7 @@ export default function ReminderNotify({
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
height={30}
|
||||
fontSize={SIZE.xs + 1}
|
||||
fontSize={SIZE.xs}
|
||||
style={{ marginLeft: 10, borderRadius: 100 }}
|
||||
onPress={() => onSnooze(item.time)}
|
||||
/>
|
||||
@@ -150,7 +150,7 @@ export default function ReminderNotify({
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.icon,
|
||||
fontSize: SIZE.xs + 1,
|
||||
fontSize: SIZE.xs,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -21,9 +21,9 @@ import { Platform, TextInput, View } from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import DateTimePickerModal from "react-native-modal-datetime-picker";
|
||||
import {
|
||||
presentSheet,
|
||||
PresentSheetOptions,
|
||||
ToastEvent
|
||||
ToastEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
@@ -33,6 +33,7 @@ import Input from "../../ui/input";
|
||||
import dayjs from "dayjs";
|
||||
import DatePicker from "react-native-date-picker";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import Notifications, { Reminder } from "../../../services/notifications";
|
||||
import PremiumService from "../../../services/premium";
|
||||
@@ -41,6 +42,7 @@ import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { NoteType } from "../../../utils/types";
|
||||
import { Dialog } from "../../dialog";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type ReminderSheetProps = {
|
||||
@@ -198,7 +200,8 @@ export default function ReminderSheet({
|
||||
|
||||
if (!_reminder) {
|
||||
ToastEvent.show({
|
||||
heading: "Failed to add a new reminder"
|
||||
heading: "Failed to add a new reminder",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
if (reference) {
|
||||
@@ -208,54 +211,67 @@ export default function ReminderSheet({
|
||||
});
|
||||
}
|
||||
Notifications.scheduleNotification(_reminder as Reminder);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"Notes",
|
||||
"NotesPage",
|
||||
"Reminders",
|
||||
"Favorites",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
close?.();
|
||||
} catch (e) {
|
||||
ToastEvent.error(e as Error);
|
||||
ToastEvent.error(e as Error, undefined, "local");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: DDS.isTab ? "90%" : "99.99%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>Set reminder</Heading>
|
||||
<Button
|
||||
title="Save"
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
onPress={saveReminder}
|
||||
/>
|
||||
</View>
|
||||
<Dialog context="local" />
|
||||
<ScrollView keyboardShouldPersistTaps="always">
|
||||
<ScrollView>
|
||||
<Input
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
placeholder="Remind me of..."
|
||||
onChangeText={(text) => (title.current = text)}
|
||||
containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
|
||||
wrapperStyle={{
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={
|
||||
reminder ? reminder?.description : referencedItem?.headline
|
||||
}
|
||||
placeholder="Add a quick note"
|
||||
placeholder="Add a short note"
|
||||
onChangeText={(text) => (details.current = text)}
|
||||
containerStyle={{
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
maxHeight: 80,
|
||||
marginTop: 10
|
||||
maxHeight: 80
|
||||
}}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
inputStyle={{
|
||||
height: 80
|
||||
minHeight: 80,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
height={80}
|
||||
wrapperStyle={{
|
||||
@@ -293,6 +309,16 @@ export default function ReminderSheet({
|
||||
mode as keyof typeof ReminderModes
|
||||
] as Reminder["mode"]
|
||||
);
|
||||
if (mode === "Repeat") {
|
||||
setSelectedDays((days) => {
|
||||
if (days.length > 0) return days;
|
||||
if (days.indexOf(date.getDay()) > -1) {
|
||||
return days;
|
||||
}
|
||||
days.push(date.getDay());
|
||||
return [...days];
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -440,6 +466,7 @@ export default function ReminderSheet({
|
||||
fadeToColor={colors.bg}
|
||||
theme={colors.night ? "dark" : "light"}
|
||||
is24hourSource="locale"
|
||||
androidVariant="nativeAndroid"
|
||||
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
|
||||
/>
|
||||
|
||||
@@ -522,7 +549,7 @@ export default function ReminderSheet({
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{recurringMode === RecurringModes.Daily
|
||||
? "Repeats daily " + `at ${dayjs(date).format("hh:mm A")}.`
|
||||
: selectedDays.length === 7 &&
|
||||
@@ -556,15 +583,6 @@ export default function ReminderSheet({
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
title="Save"
|
||||
type="accent"
|
||||
fontSize={SIZE.md}
|
||||
onPress={saveReminder}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -578,6 +596,7 @@ ReminderSheet.present = (
|
||||
presentSheet({
|
||||
context: isSheet ? "local" : undefined,
|
||||
enableGesturesInScrollView: true,
|
||||
noBottomPadding: true,
|
||||
component: (ref, close, update) => (
|
||||
<ReminderSheet
|
||||
actionSheetRef={ref}
|
||||
|
||||
@@ -44,7 +44,8 @@ import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
let RNFetchBlob;
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
@@ -213,7 +214,6 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
RNFetchBlob = (await import("rn-fetch-blob")).default;
|
||||
let path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import Navigation from "../../../services/navigation";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { GROUP, SORT } from "../../../utils/constants";
|
||||
import { refreshNotesPage } from "../../../utils/events";
|
||||
import layoutmanager from "../../../utils/layout-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
@@ -39,7 +38,6 @@ const Sort = ({ type, screen }) => {
|
||||
const updateGroupOptions = async (_groupOptions) => {
|
||||
await db.settings.setGroupOptions(type, _groupOptions);
|
||||
|
||||
layoutmanager.withSpringAnimation(600);
|
||||
setGroupOptions(_groupOptions);
|
||||
setTimeout(() => {
|
||||
if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen);
|
||||
|
||||
@@ -19,19 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import qclone from "qclone";
|
||||
import React, {
|
||||
createContext,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
View,
|
||||
RefreshControl,
|
||||
Platform
|
||||
} from "react-native";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import ActionSheet, {
|
||||
ActionSheetRef,
|
||||
FlatList
|
||||
@@ -59,18 +54,37 @@ import {
|
||||
import { normalize, SIZE } from "../../../utils/size";
|
||||
import { GroupHeader, NotebookType, TopicType } from "../../../utils/types";
|
||||
|
||||
import { groupArray } from "@notesnook/core/utils/grouping";
|
||||
import Config from "react-native-config";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { openEditor } from "../../../screens/notes/common";
|
||||
import { getTotalNotes, history } from "../../../utils";
|
||||
import { Properties } from "../../properties";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import Config from "react-native-config";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { Properties } from "../../properties";
|
||||
import Sort from "../sort";
|
||||
import { groupArray } from "@notesnook/core/utils/grouping";
|
||||
|
||||
type ConfigItem = { id: string; type: string };
|
||||
class TopicSheetConfig {
|
||||
static storageKey: "$$sp";
|
||||
|
||||
static makeId(item: ConfigItem) {
|
||||
return `${TopicSheetConfig.storageKey}:${item.type}:${item.id}`;
|
||||
}
|
||||
|
||||
static get(item: ConfigItem) {
|
||||
return MMKV.getInt(TopicSheetConfig.makeId(item)) || 0;
|
||||
}
|
||||
|
||||
static set(item: ConfigItem, index = 0) {
|
||||
MMKV.setInt(TopicSheetConfig.makeId(item), index);
|
||||
}
|
||||
}
|
||||
|
||||
export const TopicsSheet = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const canShow =
|
||||
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes";
|
||||
@@ -85,6 +99,7 @@ export const TopicsSheet = () => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const ref = useRef<ActionSheetRef>(null);
|
||||
const isTopic = currentScreen.name === "TopicNotes";
|
||||
const [topics, setTopics] = useState(
|
||||
notebook
|
||||
? qclone(
|
||||
@@ -92,26 +107,11 @@ export const TopicsSheet = () => {
|
||||
)
|
||||
: []
|
||||
);
|
||||
const [animations] = useState({
|
||||
translate: new Animated.Value(0),
|
||||
display: new Animated.Value(-5000),
|
||||
opacity: new Animated.Value(0)
|
||||
});
|
||||
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions("topics")
|
||||
);
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
setGroupOptions({ ...(db.settings?.getGroupOptions("topics") as any) });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [onUpdate]);
|
||||
|
||||
const onRequestUpdate = React.useCallback(
|
||||
(data?: NotebookScreenParams) => {
|
||||
if (!canShow) return;
|
||||
@@ -121,15 +121,33 @@ export const TopicsSheet = () => {
|
||||
if (_notebook) {
|
||||
setNotebook(_notebook);
|
||||
|
||||
setTopics(qclone(groupArray(_notebook.topics, groupOptions)));
|
||||
setTopics(
|
||||
qclone(
|
||||
groupArray(_notebook.topics, db.settings?.getGroupOptions("topics"))
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
[canShow, notebook, groupOptions]
|
||||
[canShow, notebook]
|
||||
);
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
setGroupOptions({ ...(db.settings?.getGroupOptions("topics") as any) });
|
||||
onRequestUpdate();
|
||||
}, [onRequestUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
const onTopicUpdate = () => {
|
||||
onRequestUpdate();
|
||||
setTimeout(() => {
|
||||
onRequestUpdate();
|
||||
}, 1);
|
||||
};
|
||||
eSubscribeEvent(eOnTopicSheetUpdate, onTopicUpdate);
|
||||
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
|
||||
@@ -157,7 +175,7 @@ export const TopicsSheet = () => {
|
||||
index: number;
|
||||
}) =>
|
||||
(item as GroupHeader).type === "header" ? null : (
|
||||
<TopicItem item={item as TopicType} index={index} />
|
||||
<TopicItem sheetRef={ref} item={item as TopicType} index={index} />
|
||||
);
|
||||
|
||||
const selectionContext = {
|
||||
@@ -185,29 +203,37 @@ export const TopicsSheet = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (canShow) {
|
||||
const isTopic = currentScreen.name === "TopicNotes";
|
||||
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
|
||||
if (!ref.current?.isOpen()) {
|
||||
animations.display.setValue(5000);
|
||||
animations.opacity.setValue(0);
|
||||
}
|
||||
if (id) {
|
||||
onRequestUpdate({
|
||||
item: db.notebooks?.notebook(id)?.data
|
||||
} as any);
|
||||
}
|
||||
ref.current?.show();
|
||||
setTimeout(() => {
|
||||
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
|
||||
const notebook = db.notebooks?.notebook(id as string)?.data;
|
||||
const snapPoint = isTopic
|
||||
? 0
|
||||
: TopicSheetConfig.get({
|
||||
type: isTopic ? "topic" : "notebook",
|
||||
id: currentScreen.id as string
|
||||
});
|
||||
|
||||
if (ref.current?.isOpen()) {
|
||||
ref.current?.snapToIndex(snapPoint);
|
||||
} else {
|
||||
ref.current?.show(snapPoint);
|
||||
}
|
||||
if (notebook) {
|
||||
onRequestUpdate({
|
||||
item: notebook
|
||||
} as any);
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
ref.current?.hide();
|
||||
}
|
||||
}, [
|
||||
animations.display,
|
||||
animations.opacity,
|
||||
canShow,
|
||||
currentScreen?.id,
|
||||
currentScreen.name,
|
||||
currentScreen?.notebookId,
|
||||
onRequestUpdate
|
||||
onRequestUpdate,
|
||||
isTopic
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -215,14 +241,27 @@ export const TopicsSheet = () => {
|
||||
ref={ref}
|
||||
isModal={false}
|
||||
containerStyle={{
|
||||
maxHeight: 400,
|
||||
maxHeight: 300,
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15,
|
||||
backgroundColor: colors.bg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: colors.nav,
|
||||
borderBottomWidth: 0
|
||||
}}
|
||||
openAnimationConfig={{
|
||||
friction: 10
|
||||
}}
|
||||
onSnapIndexChange={(index) => {
|
||||
setCollapsed(index === 0);
|
||||
TopicSheetConfig.set(
|
||||
{
|
||||
type: isTopic ? "topic" : "notebook",
|
||||
id: currentScreen.id as string
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
closable={!canShow}
|
||||
elevation={10}
|
||||
indicatorStyle={{
|
||||
@@ -230,70 +269,44 @@ export const TopicsSheet = () => {
|
||||
backgroundColor: colors.nav
|
||||
}}
|
||||
keyboardHandlerEnabled={false}
|
||||
snapPoints={
|
||||
Config.isTesting === "true"
|
||||
? [100]
|
||||
: [Platform.OS === "ios" ? 25 : 20, 100]
|
||||
}
|
||||
initialSnapIndex={0}
|
||||
snapPoints={Config.isTesting === "true" ? [100] : [25, 100]}
|
||||
initialSnapIndex={1}
|
||||
backgroundInteractionEnabled
|
||||
onChange={(position, height) => {
|
||||
animations.translate.setValue(position - 60);
|
||||
const h = Dimensions.get("window").height;
|
||||
const minPos = h - height;
|
||||
if (position - 100 < minPos || !canShow) {
|
||||
animations.display.setValue(5000);
|
||||
animations.opacity.setValue(0);
|
||||
} else {
|
||||
animations.display.setValue(0);
|
||||
setTimeout(() => {
|
||||
animations.opacity.setValue(1);
|
||||
}, 300);
|
||||
}
|
||||
}}
|
||||
gestureEnabled
|
||||
ExtraOverlayComponent={
|
||||
<Animated.View
|
||||
style={{
|
||||
top: animations.translate,
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
opacity: animations.opacity,
|
||||
transform: [
|
||||
{
|
||||
translateY: animations.display
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={"accent"}
|
||||
accentText="light"
|
||||
onPress={openEditor}
|
||||
customStyle={{
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: normalize(60),
|
||||
width: normalize(60)
|
||||
}}
|
||||
>
|
||||
<Icon name="plus" color="white" size={SIZE.xxl} />
|
||||
</View>
|
||||
</PressableButton>
|
||||
</Animated.View>
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
maxHeight: 400,
|
||||
height: 400,
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
marginTop: -80
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={"accent"}
|
||||
accentText="light"
|
||||
onPress={openEditor}
|
||||
customStyle={{
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: normalize(60),
|
||||
width: normalize(60)
|
||||
}}
|
||||
>
|
||||
<Icon name="plus" color="white" size={SIZE.xxl} />
|
||||
</View>
|
||||
</PressableButton>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
height: 300,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
@@ -326,7 +339,7 @@ export const TopicsSheet = () => {
|
||||
selection.length > 1 ? "topics" : "topics"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selection.length > 1 ? "these topicss?" : "this topics?"
|
||||
selection.length > 1 ? "these topics?" : "this topic?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
@@ -378,6 +391,25 @@ export const TopicsSheet = () => {
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={collapsed ? "chevron-up" : "chevron-down"}
|
||||
onPress={() => {
|
||||
if (ref.current?.currentSnapIndex() !== 0) {
|
||||
setCollapsed(true);
|
||||
ref.current?.snapToIndex(0);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
ref.current?.snapToIndex(1);
|
||||
}
|
||||
}}
|
||||
color={colors.pri}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
@@ -406,7 +438,7 @@ export const TopicsSheet = () => {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 300
|
||||
height: 200
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon}>No topics</Paragraph>
|
||||
@@ -428,12 +460,20 @@ const SelectionContext = createContext<{
|
||||
}>({
|
||||
selection: [],
|
||||
enabled: false,
|
||||
setEnabled: (value: boolean) => {},
|
||||
toggleSelection: (item: TopicType) => {}
|
||||
setEnabled: (_value: boolean) => {},
|
||||
toggleSelection: (_item: TopicType) => {}
|
||||
});
|
||||
const useSelection = () => useContext(SelectionContext);
|
||||
|
||||
const TopicItem = ({ item, index }: { item: TopicType; index: number }) => {
|
||||
const TopicItem = ({
|
||||
item,
|
||||
index,
|
||||
sheetRef
|
||||
}: {
|
||||
item: TopicType;
|
||||
index: number;
|
||||
sheetRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const screen = useNavigationStore((state) => state.currentScreen);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const selection = useSelection();
|
||||
|
||||
@@ -302,6 +302,7 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
translateX.value = value;
|
||||
})
|
||||
.onEnd((event) => {
|
||||
if (locked.value || forcedLock.value) return;
|
||||
if (currentTab.value === 2 && Platform.OS === "android") return;
|
||||
const velocityX =
|
||||
event.velocityX < 0 ? event.velocityX * -1 : event.velocityX;
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
@@ -34,24 +34,25 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
let toastMessages = [];
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [keyboard, setKeyboard] = useState(false);
|
||||
const [data, setData] = useState({});
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const hideTimeout = useRef();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef([]);
|
||||
|
||||
const showToastFunc = useCallback(
|
||||
const show = useCallback(
|
||||
async (data) => {
|
||||
if (!data) return;
|
||||
if (data.context !== context) return;
|
||||
if (toastMessages.findIndex((m) => m.message === data.message) >= 0) {
|
||||
if (
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
toastMessages.push(data);
|
||||
if (toastMessages?.length > 1) return;
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
setData(data);
|
||||
|
||||
setVisible(true);
|
||||
@@ -59,16 +60,16 @@ export const Toast = ({ context = "global" }) => {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToastFunc();
|
||||
hide();
|
||||
}, data.duration);
|
||||
},
|
||||
[context, hideToastFunc]
|
||||
[context, hide]
|
||||
);
|
||||
|
||||
const showNext = useCallback(
|
||||
const next = useCallback(
|
||||
(data) => {
|
||||
if (!data) {
|
||||
hideToastFunc();
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
setData(data);
|
||||
@@ -76,27 +77,28 @@ export const Toast = ({ context = "global" }) => {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToastFunc();
|
||||
hide();
|
||||
}, data?.duration);
|
||||
},
|
||||
[hideToastFunc]
|
||||
[hide]
|
||||
);
|
||||
|
||||
const hideToastFunc = useCallback(() => {
|
||||
const hide = useCallback(() => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
let msg = toastMessages.length > 1 ? toastMessages.shift() : null;
|
||||
let msg =
|
||||
toastMessages.current.length > 1 ? toastMessages.current.shift() : null;
|
||||
|
||||
if (msg) {
|
||||
setVisible(false);
|
||||
showNext(msg);
|
||||
next(msg);
|
||||
setTimeout(() => {
|
||||
setVisible(true);
|
||||
}, 300);
|
||||
} else {
|
||||
setVisible(false);
|
||||
toastMessages.shift();
|
||||
toastMessages.current.shift();
|
||||
setTimeout(() => {
|
||||
setData({});
|
||||
if (hideTimeout.current) {
|
||||
@@ -104,44 +106,22 @@ export const Toast = ({ context = "global" }) => {
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [showNext]);
|
||||
|
||||
const _onKeyboardShow = () => {
|
||||
setKeyboard(true);
|
||||
};
|
||||
|
||||
const _onKeyboardHide = () => {
|
||||
setKeyboard(false);
|
||||
};
|
||||
}, [next]);
|
||||
|
||||
useEffect(() => {
|
||||
toastMessages = [];
|
||||
let sub1 = Keyboard.addListener("keyboardDidShow", _onKeyboardShow);
|
||||
let sub2 = Keyboard.addListener("keyboardDidHide", _onKeyboardHide);
|
||||
eSubscribeEvent(eShowToast, showToastFunc);
|
||||
eSubscribeEvent(eHideToast, hideToastFunc);
|
||||
eSubscribeEvent(eShowToast, show);
|
||||
eSubscribeEvent(eHideToast, hide);
|
||||
return () => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
|
||||
toastMessages = [];
|
||||
sub1?.remove();
|
||||
sub2?.remove();
|
||||
eUnSubscribeEvent(eShowToast, showToastFunc);
|
||||
eUnSubscribeEvent(eHideToast, hideToastFunc);
|
||||
toastMessages.current = [];
|
||||
eUnSubscribeEvent(eShowToast, show);
|
||||
eUnSubscribeEvent(eHideToast, hide);
|
||||
};
|
||||
}, [hideToastFunc, keyboard, showToastFunc]);
|
||||
}, [hide, show]);
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideToastFunc();
|
||||
}}
|
||||
onPress={hide}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? 400 : "100%",
|
||||
@@ -209,7 +189,7 @@ export const Toast = ({ context = "global" }) => {
|
||||
color={colors.pri}
|
||||
size={SIZE.md}
|
||||
onPress={() => {
|
||||
hideToastFunc();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.heading}
|
||||
@@ -224,7 +204,7 @@ export const Toast = ({ context = "global" }) => {
|
||||
paddingRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
hideToastFunc();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.message}
|
||||
|
||||
@@ -40,6 +40,8 @@ import { getElevation } from "../../../utils";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import phone from "phone";
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
fwdRef?: RefObject<TextInput>;
|
||||
validationType?:
|
||||
@@ -117,7 +119,7 @@ const Input = ({
|
||||
? colors.red
|
||||
: focus
|
||||
? customColor || colors.accent
|
||||
: colors.nav;
|
||||
: colors.border;
|
||||
|
||||
const validate = async (value: string) => {
|
||||
if (!validationType) return;
|
||||
@@ -149,7 +151,6 @@ const Input = ({
|
||||
isError = customValidator && value === customValidator();
|
||||
break;
|
||||
case "phonenumber": {
|
||||
const { default: phone } = await import("phone");
|
||||
const result = phone(value, {
|
||||
strictDetection: true,
|
||||
validateMobilePrefix: true
|
||||
|
||||
@@ -65,7 +65,7 @@ export const Notice = ({
|
||||
flexShrink: 1
|
||||
}}
|
||||
selectable={selectable}
|
||||
size={isSmall ? SIZE.xs + 1 : SIZE.sm}
|
||||
size={isSmall ? SIZE.xs : SIZE.sm}
|
||||
>
|
||||
{text}
|
||||
</Paragraph>
|
||||
|
||||
@@ -105,6 +105,9 @@ const SheetWrapper = ({
|
||||
defaultOverlayOpacity={overlayOpacity}
|
||||
overlayColor={pitchBlack ? "#585858" : "#2b2b2b"}
|
||||
keyboardShouldPersistTaps="always"
|
||||
openAnimationConfig={{
|
||||
friction: 9
|
||||
}}
|
||||
ExtraOverlayComponent={
|
||||
<>
|
||||
{overlay}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { Platform, Text, TextProps } from "react-native";
|
||||
import { Platform, Text, TextProps, ViewStyle } from "react-native";
|
||||
import Animated, {
|
||||
ComplexAnimationBuilder,
|
||||
Layout
|
||||
@@ -30,15 +30,26 @@ interface HeadingProps extends TextProps {
|
||||
size?: number;
|
||||
layout?: ComplexAnimationBuilder;
|
||||
animated?: boolean;
|
||||
extraBold?: boolean;
|
||||
}
|
||||
|
||||
const AnimatedText = Animated.createAnimatedComponent(Text);
|
||||
|
||||
const extraBoldStyle = {
|
||||
fontFamily: Platform.OS === "android" ? "OpenSans-Bold" : undefined,
|
||||
fontWeight: Platform.OS === "ios" ? "800" : undefined
|
||||
};
|
||||
const boldStyle = {
|
||||
fontFamily: Platform.OS === "android" ? "OpenSans-SemiBold" : undefined,
|
||||
fontWeight: Platform.OS === "ios" ? "600" : undefined
|
||||
};
|
||||
|
||||
const Heading = ({
|
||||
color,
|
||||
size = SIZE.xl,
|
||||
style,
|
||||
animated,
|
||||
extraBold,
|
||||
...restProps
|
||||
}: HeadingProps) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -53,11 +64,9 @@ const Heading = ({
|
||||
style={[
|
||||
{
|
||||
fontSize: size || SIZE.xl,
|
||||
color: color || colors.heading,
|
||||
fontFamily:
|
||||
Platform.OS === "android" ? "OpenSans-SemiBold" : undefined,
|
||||
fontWeight: Platform.OS === "ios" ? "600" : undefined
|
||||
color: color || colors.heading
|
||||
},
|
||||
extraBold ? (extraBoldStyle as ViewStyle) : (boldStyle as ViewStyle),
|
||||
style
|
||||
]}
|
||||
></Component>
|
||||
|
||||
@@ -19,4 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { FeatureType } from "./components/sheets/new-feature";
|
||||
|
||||
export const features: FeatureType[] = [];
|
||||
export const features: FeatureType[] = [
|
||||
{
|
||||
title: "Attachments preview",
|
||||
body: "You can now preview PDFs & Images directly inside Notesnook."
|
||||
},
|
||||
{
|
||||
title: "New attachments manager",
|
||||
body: "The new attachments manager makes is much easier to view & interact with your attachments. It also allows you to download all (or some) of your attachments."
|
||||
},
|
||||
{
|
||||
title: "Assign tags to multiple notes",
|
||||
body: "You can now easily tag multiple notes without first opening them."
|
||||
}
|
||||
];
|
||||
|
||||
@@ -52,7 +52,11 @@ import { useThemeStore } from "../stores/use-theme-store";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import { toTXT } from "../utils";
|
||||
import { toggleDarkMode } from "../utils/color-scheme/utils";
|
||||
import { eOpenAddTopicDialog, eOpenLoginDialog } from "../utils/events";
|
||||
import {
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddTopicDialog,
|
||||
eOpenLoginDialog
|
||||
} from "../utils/events";
|
||||
import { deleteItems } from "../utils/functions";
|
||||
import { sleep } from "../utils/time";
|
||||
|
||||
@@ -144,13 +148,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
if (!item.id) return;
|
||||
close();
|
||||
await db.notes.note(item.id).favorite();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
|
||||
async function pinItem() {
|
||||
@@ -158,14 +156,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
close();
|
||||
let type = item.type;
|
||||
await db[`${type}s`][type](item.id).pin();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebooks"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
|
||||
async function pinToNotifications() {
|
||||
@@ -179,9 +170,17 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
checkNotifPinned();
|
||||
return;
|
||||
}
|
||||
if (item.locked) return;
|
||||
let html = await db.notes.note(item.id).content();
|
||||
let text = await toTXT(item);
|
||||
if (item.locked) {
|
||||
ToastEvent.show({
|
||||
heading: "Note is locked",
|
||||
type: "error",
|
||||
message: "Locked notes cannot be pinned to notifications",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
let text = await toTXT(item, false);
|
||||
let html = text.replace(/\n/g, "<br />");
|
||||
Notifications.displayNotification({
|
||||
title: item.title,
|
||||
message: item.headline || text,
|
||||
@@ -200,15 +199,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
if (!checkNoteSynced()) return;
|
||||
close();
|
||||
await db.trash.restore(item.id);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebooks",
|
||||
"Trash"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
let type = item.type === "trash" ? item.itemType : item.type;
|
||||
ToastEvent.show({
|
||||
heading:
|
||||
@@ -324,13 +315,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
let note = db.notes.note(item.id).data;
|
||||
if (note.locked) {
|
||||
close();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
close();
|
||||
@@ -396,16 +381,11 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
positivePress: async (value) => {
|
||||
if (!value || value === "" || value.trimStart().length == 0) return;
|
||||
await db.tags.rename(item.id, db.tags.sanitize(value));
|
||||
useTagStore.getState().setTags();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Tags"
|
||||
);
|
||||
setImmediate(() => {
|
||||
useTagStore.getState().setTags();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
});
|
||||
},
|
||||
input: true,
|
||||
defaultValue: alias,
|
||||
@@ -448,24 +428,16 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
? "This reminder will be removed"
|
||||
: "This tag will be removed from all notes.",
|
||||
positivePress: async () => {
|
||||
const routes = [];
|
||||
routes.push(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"Notes",
|
||||
"NotesPage",
|
||||
"Reminders",
|
||||
"Favorites"
|
||||
);
|
||||
if (item.type === "reminder") {
|
||||
await db.reminders.remove(item.id);
|
||||
} else {
|
||||
await db.tags.remove(item.id);
|
||||
useTagStore.getState().setTags();
|
||||
routes.push("Tags");
|
||||
}
|
||||
Navigation.queueRoutesForUpdate(...routes);
|
||||
useRelationStore.getState().update();
|
||||
setImmediate(() => {
|
||||
useTagStore.getState().setTags();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
});
|
||||
},
|
||||
positiveText: "Delete",
|
||||
positiveType: "errorShade"
|
||||
@@ -503,15 +475,8 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
},
|
||||
item.id
|
||||
);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -519,15 +484,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
const currentScreen = useNavigationStore.getState().currentScreen;
|
||||
if (currentScreen.name !== "Notebook") return;
|
||||
await db.relations.unlink({ type: "notebook", id: currentScreen.id }, item);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -542,12 +499,14 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
await db.trash.delete(item.id);
|
||||
Navigation.queueRoutesForUpdate("Trash");
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
setImmediate(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
});
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
@@ -561,23 +520,26 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
|
||||
async function showAttachments() {
|
||||
AttachmentDialog.present();
|
||||
AttachmentDialog.present(item);
|
||||
}
|
||||
|
||||
async function exportNote() {
|
||||
if (item.locked) {
|
||||
ToastEvent.show({
|
||||
heading: "Note is locked",
|
||||
type: "error",
|
||||
message: "Locked notes cannot be exported",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
ExportNotesSheet.present([item]);
|
||||
}
|
||||
|
||||
async function toggleLocalOnly() {
|
||||
if (!checkNoteSynced() || !user) return;
|
||||
db.notes.note(item.id).localOnly();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -588,26 +550,14 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
useEditorStore.getState().setReadonly(current);
|
||||
// tiny.call(EditorWebView, tiny.toogleReadMode(current ? 'readonly' : 'design'));
|
||||
}
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
close();
|
||||
};
|
||||
|
||||
const duplicateNote = async () => {
|
||||
if (!checkNoteSynced()) return;
|
||||
await db.notes.note(item.id).duplicate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
close();
|
||||
};
|
||||
const actions = [
|
||||
@@ -673,7 +623,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
},
|
||||
{
|
||||
id: "favorite",
|
||||
title: !item.favorite ? "Favorite" : "Unfavorite",
|
||||
title: item.favorite ? "Unfavorite" : "Favorite",
|
||||
icon: item.favorite ? "star-off" : "star-outline",
|
||||
func: addToFavorites,
|
||||
close: false,
|
||||
@@ -686,7 +636,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
id: "pin-to-notifications",
|
||||
title:
|
||||
notifPinned !== null
|
||||
? "Unpin from Notifications"
|
||||
? "Unpin from notifications"
|
||||
: "Pin to notifications",
|
||||
icon: "message-badge-outline",
|
||||
on: notifPinned !== null,
|
||||
@@ -751,7 +701,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
},
|
||||
{
|
||||
id: "read-only",
|
||||
title: "Read only",
|
||||
title: "Readonly",
|
||||
icon: "pencil-lock",
|
||||
func: toggleReadyOnlyMode,
|
||||
on: item.readonly
|
||||
@@ -835,15 +785,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
});
|
||||
Notifications.scheduleNotification(item);
|
||||
useRelationStore.getState().update();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"NotesPage",
|
||||
"Reminders"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,22 +17,37 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { EV, EVENTS, SYNC_CHECK_IDS } from "@notesnook/core/common";
|
||||
import { useEffect, useRef } from "react";
|
||||
import notifee from "@notifee/react-native";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
Appearance,
|
||||
AppState,
|
||||
Appearance,
|
||||
Keyboard,
|
||||
Linking,
|
||||
NativeEventEmitter,
|
||||
NativeModules,
|
||||
Platform,
|
||||
Keyboard
|
||||
Platform
|
||||
} from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import { Walkthrough } from "../components/walkthroughs";
|
||||
import { editorController, editorState } from "../screens/editor/tiptap/utils";
|
||||
import {
|
||||
clearAppState,
|
||||
editorController,
|
||||
editorState
|
||||
} from "../screens/editor/tiptap/utils";
|
||||
import { useDragState } from "../screens/settings/editor/state";
|
||||
import BackupService from "../services/backup";
|
||||
import {
|
||||
ToastEvent,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../services/event-manager";
|
||||
import {
|
||||
clearMessage,
|
||||
setEmailVerifyMessage,
|
||||
@@ -41,36 +56,23 @@ import {
|
||||
} from "../services/message";
|
||||
import PremiumService from "../services/premium";
|
||||
import SettingsService from "../services/settings";
|
||||
import Sync from "../services/sync";
|
||||
import { initAfterSync } from "../stores";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
import { useEditorStore } from "../stores/use-editor-store";
|
||||
import { useMessageStore } from "../stores/use-message-store";
|
||||
import { useNoteStore } from "../stores/use-notes-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { SyncStatus, useUserStore } from "../stores/use-user-store";
|
||||
import { updateStatusBarColor } from "../utils/color-scheme";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import {
|
||||
eClearEditor,
|
||||
eCloseSheet,
|
||||
eOnLoadNote,
|
||||
refreshNotesPage
|
||||
} from "../utils/events";
|
||||
import Sync from "../services/sync";
|
||||
import { initAfterSync } from "../stores";
|
||||
import { SyncStatus, useUserStore } from "../stores/use-user-store";
|
||||
import { useMessageStore } from "../stores/use-message-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
import { useNoteStore } from "../stores/use-notes-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../services/event-manager";
|
||||
import { useEditorStore } from "../stores/use-editor-store";
|
||||
import { useDragState } from "../screens/settings/editor/state";
|
||||
import { useCallback } from "react";
|
||||
import { clearAppState } from "../screens/editor/tiptap/utils";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import BackupService from "../services/backup";
|
||||
import { sleep } from "../utils/time";
|
||||
import notifee from "@notifee/react-native";
|
||||
|
||||
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
|
||||
export const useAppEvents = () => {
|
||||
@@ -489,7 +491,8 @@ export const useAppEvents = () => {
|
||||
if (
|
||||
SettingsService.get().appLockMode === "background" &&
|
||||
!useSettingStore.getState().requestBiometrics &&
|
||||
!useUserStore.getState().verifyUser
|
||||
!useUserStore.getState().verifyUser &&
|
||||
!useUserStore.getState().shouldBlockVerifyUser
|
||||
) {
|
||||
useUserStore.getState().setVerifyUser(true);
|
||||
if (Platform.OS === "ios") {
|
||||
@@ -558,10 +561,9 @@ export const useAppEvents = () => {
|
||||
let shareExtensionOpened = MMKV.getString("shareExtensionOpened");
|
||||
if (notesAddedFromIntent) {
|
||||
if (Platform.OS === "ios") {
|
||||
await db.init();
|
||||
await db.initCollections();
|
||||
await db.notes.init();
|
||||
}
|
||||
useNoteStore.getState().setNotes();
|
||||
eSendEvent(refreshNotesPage);
|
||||
MMKV.removeItem("notesAddedFromIntent");
|
||||
initAfterSync();
|
||||
@@ -571,8 +573,8 @@ export const useAppEvents = () => {
|
||||
if (notesAddedFromIntent || shareExtensionOpened) {
|
||||
let id = useEditorStore.getState().currentEditingNote;
|
||||
let note = id && db.notes.note(id).data;
|
||||
eSendEvent("loadingNote", note);
|
||||
eSendEvent("webview_reset");
|
||||
setTimeout(() => eSendEvent("loadingNote", note), 1);
|
||||
MMKV.removeItem("shareExtensionOpened");
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -20,32 +20,47 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
|
||||
export const useAttachmentProgress = (attachment, encryption) => {
|
||||
type AttachmentProgress = {
|
||||
type: string;
|
||||
value?: number;
|
||||
percent?: string;
|
||||
};
|
||||
|
||||
export const useAttachmentProgress = (
|
||||
attachment: any,
|
||||
encryption?: boolean
|
||||
) => {
|
||||
const progress = useAttachmentStore((state) => state.progress);
|
||||
const [currentProgress, setCurrentProgress] = useState(
|
||||
const [currentProgress, setCurrentProgress] = useState<
|
||||
AttachmentProgress | undefined
|
||||
>(
|
||||
encryption
|
||||
? {
|
||||
type: "encrypt"
|
||||
}
|
||||
: null
|
||||
: undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let prog = progress[attachment.metadata.hash];
|
||||
if (prog) {
|
||||
let type = prog.type;
|
||||
let loaded = prog.type === "download" ? prog.recieved : prog.sent;
|
||||
prog = loaded / prog.total;
|
||||
prog = (prog * 100).toFixed(0);
|
||||
const attachmentProgress = progress?.[attachment?.metadata?.hash];
|
||||
if (attachmentProgress) {
|
||||
const type = attachmentProgress.type;
|
||||
const loaded =
|
||||
attachmentProgress.type === "download"
|
||||
? attachmentProgress.recieved
|
||||
: attachmentProgress.sent;
|
||||
const value = loaded / attachmentProgress.total;
|
||||
setCurrentProgress({
|
||||
value: prog,
|
||||
percent: prog + "%",
|
||||
value: value * 100,
|
||||
percent: (value * 100).toFixed(0) + "%",
|
||||
type: type
|
||||
});
|
||||
} else {
|
||||
setCurrentProgress(null);
|
||||
setTimeout(() => {
|
||||
setCurrentProgress(undefined);
|
||||
}, 300);
|
||||
}
|
||||
}, [attachment.metadata.hash, progress]);
|
||||
}, [attachment, progress]);
|
||||
|
||||
return [currentProgress, setCurrentProgress];
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import BiometicService from "../services/biometrics";
|
||||
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
|
||||
import { db } from "../common/database";
|
||||
|
||||
const VaultStatusCache = {
|
||||
exists: false,
|
||||
biometryEnrolled: false,
|
||||
isBiometryAvailable: false
|
||||
};
|
||||
|
||||
export const useVaultStatus = () => {
|
||||
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusCache);
|
||||
|
||||
const checkVaultStatus = useCallback(() => {
|
||||
db.vault.exists().then(async (exists) => {
|
||||
let available = await BiometicService.isBiometryAvailable();
|
||||
let fingerprint = await BiometicService.hasInternetCredentials();
|
||||
if (
|
||||
VaultStatusCache.exists === exists &&
|
||||
VaultStatusCache.biometryEnrolled === fingerprint &&
|
||||
VaultStatusCache.isBiometryAvailable === available
|
||||
)
|
||||
return;
|
||||
setVaultStatus({
|
||||
exists: exists,
|
||||
biometryEnrolled: fingerprint,
|
||||
isBiometryAvailable: available ? true : false
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkVaultStatus();
|
||||
eSubscribeEvent("vaultUpdated", () => checkVaultStatus());
|
||||
return () => {
|
||||
eUnSubscribeEvent("vaultUpdated", () => checkVaultStatus());
|
||||
};
|
||||
}, [checkVaultStatus]);
|
||||
|
||||
return vaultStatus;
|
||||
};
|
||||
@@ -50,6 +50,7 @@ import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { useThemeStore } from "../stores/use-theme-store";
|
||||
import { history } from "../utils";
|
||||
import { rootNavigatorRef } from "../utils/global-refs";
|
||||
import Auth from "../components/auth";
|
||||
const NativeStack = createNativeStackNavigator();
|
||||
const IntroStack = createNativeStackNavigator();
|
||||
|
||||
@@ -64,6 +65,7 @@ const IntroStack = createNativeStackNavigator();
|
||||
|
||||
const IntroStackNavigator = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const height = useSettingStore((state) => state.dimensions.height);
|
||||
return (
|
||||
<IntroStack.Navigator
|
||||
screenOptions={{
|
||||
@@ -71,12 +73,14 @@ const IntroStackNavigator = () => {
|
||||
lazy: false,
|
||||
animation: "none",
|
||||
contentStyle: {
|
||||
backgroundColor: colors.bg
|
||||
backgroundColor: colors.bg,
|
||||
minHeight: height
|
||||
}
|
||||
}}
|
||||
initialRouteName={"Intro"}
|
||||
>
|
||||
<NativeStack.Screen name="Intro" component={Intro} />
|
||||
<NativeStack.Screen name="Auth" component={Auth} />
|
||||
<NativeStack.Screen name="AppLock" component={AppLock} />
|
||||
</IntroStack.Navigator>
|
||||
);
|
||||
@@ -120,7 +124,7 @@ const _Tabs = () => {
|
||||
animation: "none",
|
||||
contentStyle: {
|
||||
backgroundColor: colors.bg,
|
||||
height: !introCompleted ? undefined : screenHeight
|
||||
minHeight: !introCompleted ? undefined : screenHeight
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -271,13 +271,13 @@ const _TabsHolder = () => {
|
||||
setTimeout(() => {
|
||||
switch (current) {
|
||||
case "tablet":
|
||||
introCompleted && tabBarRef.current?.goToIndex(0, false);
|
||||
tabBarRef.current?.goToIndex(0, false);
|
||||
break;
|
||||
case "smallTablet":
|
||||
if (!fullscreen) {
|
||||
introCompleted && tabBarRef.current?.closeDrawer(false);
|
||||
tabBarRef.current?.closeDrawer(false);
|
||||
} else {
|
||||
introCompleted && tabBarRef.current?.openDrawer(false);
|
||||
tabBarRef.current?.openDrawer(false);
|
||||
}
|
||||
break;
|
||||
case "mobile":
|
||||
@@ -416,64 +416,66 @@ const _TabsHolder = () => {
|
||||
backgroundColor="transparent"
|
||||
/>
|
||||
|
||||
{deviceMode && widths[deviceMode] ? (
|
||||
<FluidTabs
|
||||
ref={tabBarRef}
|
||||
dimensions={dimensions}
|
||||
widths={!introCompleted ? widths["mobile"] : widths[deviceMode]}
|
||||
enabled={deviceMode !== "tablet" && !fullscreen}
|
||||
onScroll={onScroll}
|
||||
onChangeTab={onChangeTab}
|
||||
onDrawerStateChange={() => true}
|
||||
>
|
||||
<View
|
||||
key="1"
|
||||
style={{
|
||||
height: "100%",
|
||||
width: fullscreen
|
||||
? 0
|
||||
: widths[!introCompleted ? "mobile" : deviceMode]?.a
|
||||
}}
|
||||
>
|
||||
<SideMenu />
|
||||
</View>
|
||||
|
||||
<View
|
||||
key="2"
|
||||
style={{
|
||||
height: "100%",
|
||||
width: fullscreen
|
||||
? 0
|
||||
: widths[!introCompleted ? "mobile" : deviceMode]?.b
|
||||
}}
|
||||
>
|
||||
{deviceMode === "mobile" ? (
|
||||
<Animated.View
|
||||
onTouchEnd={() => {
|
||||
tabBarRef.current?.closeDrawer();
|
||||
animatedOpacity.value = withTiming(0);
|
||||
animatedTranslateY.value = withTiming(-9999);
|
||||
{!introCompleted ? (
|
||||
<NavigationStack />
|
||||
) : (
|
||||
<>
|
||||
{deviceMode && widths[deviceMode] ? (
|
||||
<FluidTabs
|
||||
ref={tabBarRef}
|
||||
dimensions={dimensions}
|
||||
widths={widths[deviceMode]}
|
||||
enabled={deviceMode !== "tablet" && !fullscreen}
|
||||
onScroll={onScroll}
|
||||
onChangeTab={onChangeTab}
|
||||
onDrawerStateChange={() => true}
|
||||
>
|
||||
<View
|
||||
key="1"
|
||||
style={{
|
||||
height: "100%",
|
||||
width: fullscreen ? 0 : widths[deviceMode]?.a
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: 999,
|
||||
backgroundColor: "rgba(0,0,0,0.2)"
|
||||
},
|
||||
animatedStyle
|
||||
]}
|
||||
ref={overlayRef}
|
||||
/>
|
||||
) : null}
|
||||
>
|
||||
<SideMenu />
|
||||
</View>
|
||||
|
||||
<NavigationStack />
|
||||
</View>
|
||||
<View
|
||||
key="2"
|
||||
style={{
|
||||
height: "100%",
|
||||
width: fullscreen ? 0 : widths[deviceMode]?.b
|
||||
}}
|
||||
>
|
||||
{deviceMode === "mobile" ? (
|
||||
<Animated.View
|
||||
onTouchEnd={() => {
|
||||
tabBarRef.current?.closeDrawer();
|
||||
animatedOpacity.value = withTiming(0);
|
||||
animatedTranslateY.value = withTiming(-9999);
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: 999,
|
||||
backgroundColor: "rgba(0,0,0,0.2)"
|
||||
},
|
||||
animatedStyle
|
||||
]}
|
||||
ref={overlayRef}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<EditorWrapper key="3" width={widths} dimensions={dimensions} />
|
||||
</FluidTabs>
|
||||
) : null}
|
||||
<NavigationStack />
|
||||
</View>
|
||||
|
||||
<EditorWrapper key="3" width={widths} dimensions={dimensions} />
|
||||
</FluidTabs>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,18 +4,16 @@
|
||||
"main": "./App.js",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"react": "18.0.0",
|
||||
"react-native": "0.69.7",
|
||||
"@flyerhq/react-native-link-preview": "^1.6.0",
|
||||
"@mdi/js": "^6.7.96",
|
||||
"absolutify": "^0.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.10.4",
|
||||
"entities": "^3.0.1",
|
||||
"html-to-text": "8.1.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.16",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.18",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
@@ -29,7 +27,9 @@
|
||||
"validator": "^13.5.2",
|
||||
"zustand": "^3.6.0",
|
||||
"fflate": "^0.7.3",
|
||||
"timeago.js": "4.0.2"
|
||||
"timeago.js": "4.0.2",
|
||||
"react-native-blob-util": "0.17.3",
|
||||
"react-native-swiper-flatlist": "3.2.2"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { Platform, ViewStyle } from "react-native";
|
||||
@@ -45,6 +46,7 @@ import { useEditor } from "./tiptap/use-editor";
|
||||
import { useEditorEvents } from "./tiptap/use-editor-events";
|
||||
import { editorController } from "./tiptap/utils";
|
||||
import { useLayoutEffect } from "react";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -89,7 +91,7 @@ const Editor = React.memo(
|
||||
noToolbar,
|
||||
noHeader
|
||||
});
|
||||
|
||||
const renderKey = useRef(`editor-0`);
|
||||
useImperativeHandle(ref, () => ({
|
||||
get: () => editor
|
||||
}));
|
||||
@@ -124,8 +126,9 @@ const Editor = React.memo(
|
||||
);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current =
|
||||
renderKey.current === `editor-0` ? `editor-1` : `editor-0`;
|
||||
editor.setLoading(true);
|
||||
setTimeout(() => editor.setLoading(false), 10);
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -152,6 +155,7 @@ const Editor = React.memo(
|
||||
testID={notesnook.editor.id}
|
||||
ref={editor.ref}
|
||||
onLoad={editor.onLoad}
|
||||
key={renderKey.current}
|
||||
onRenderProcessGone={onError}
|
||||
nestedScrollEnabled
|
||||
onError={onError}
|
||||
@@ -163,6 +167,7 @@ const Editor = React.memo(
|
||||
injectedJavaScript={`globalThis.sessionId="${editor.sessionId}";`}
|
||||
javaScriptEnabled={true}
|
||||
focusable={true}
|
||||
onContentProcessDidTerminate={onError}
|
||||
setSupportMultipleWindows={false}
|
||||
overScrollMode="never"
|
||||
scrollEnabled={false}
|
||||
@@ -228,6 +233,8 @@ const AppSection = ({
|
||||
|
||||
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
const readonly = useEditorStore((state) => state.readonly);
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const onPress = async () => {
|
||||
if (editor.note.current) {
|
||||
await db.notes?.note(editor.note.current.id).readonly();
|
||||
@@ -237,7 +244,7 @@ const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
}
|
||||
};
|
||||
|
||||
return readonly && IconButton ? (
|
||||
return readonly && IconButton && !keyboard.keyboardShown ? (
|
||||
<IconButton
|
||||
name="pencil-lock"
|
||||
type="grayBg"
|
||||
@@ -245,7 +252,7 @@ const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
color="accent"
|
||||
customStyle={{
|
||||
position: "absolute",
|
||||
bottom: 20,
|
||||
bottom: 60,
|
||||
width: 60,
|
||||
height: 60,
|
||||
right: 12,
|
||||
|
||||
@@ -28,12 +28,12 @@ import { Button } from "../../components/ui/button";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
@@ -42,7 +42,9 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [error, setError] = useState(false);
|
||||
const opacity = useSharedValue(1);
|
||||
const translateValue = useSharedValue(6000);
|
||||
const translateValue = useSharedValue(0);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const isTablet = deviceMode !== "mobile";
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const isDefaultEditor = editorId === "";
|
||||
const timers = useRef({
|
||||
@@ -50,6 +52,9 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
error: 0,
|
||||
closing: 0
|
||||
});
|
||||
const loadingState = useRef({
|
||||
startTime: 0
|
||||
});
|
||||
|
||||
const clearTimers = () => {
|
||||
clearTimeout(timers.current.loading);
|
||||
@@ -62,6 +67,7 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
editorState().overlay = true;
|
||||
clearTimers();
|
||||
if (_loading) {
|
||||
loadingState.current.startTime = Date.now();
|
||||
opacity.value = 1;
|
||||
translateValue.value = 0;
|
||||
timers.current.error = setTimeout(() => {
|
||||
@@ -69,7 +75,14 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
}, 15 * 1000);
|
||||
} else {
|
||||
clearTimers();
|
||||
setTimeout(() => {
|
||||
const timeDiffSinceLoadStarted =
|
||||
Date.now() - loadingState.current.startTime > 300 ? 300 : 0;
|
||||
if (!timeDiffSinceLoadStarted) {
|
||||
setError(false);
|
||||
editorState().overlay = false;
|
||||
opacity.value = 0;
|
||||
translateValue.value = 6000;
|
||||
} else {
|
||||
setError(false);
|
||||
editorState().overlay = false;
|
||||
opacity.value = withTiming(0, {
|
||||
@@ -78,19 +91,25 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
setTimeout(() => {
|
||||
translateValue.value = 6000;
|
||||
}, 500);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
[opacity, translateValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (!loadingState.current.startTime) {
|
||||
translateValue.value = 6000;
|
||||
opacity.value = 0;
|
||||
}
|
||||
}, 3000);
|
||||
eSubscribeEvent("loadingNote" + editorId, load);
|
||||
return () => {
|
||||
clearTimers();
|
||||
eUnSubscribeEvent("loadingNote" + editorId, load);
|
||||
};
|
||||
}, [editorId, load]);
|
||||
}, [editorId, load, translateValue]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
@@ -140,7 +159,7 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
paddingRight: 12
|
||||
}}
|
||||
>
|
||||
{DDS.isTablet() ? (
|
||||
{isTablet ? (
|
||||
<View />
|
||||
) : (
|
||||
<IconButton
|
||||
@@ -184,7 +203,9 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 10,
|
||||
marginTop: 10
|
||||
marginTop: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon} size={13}>
|
||||
|
||||
@@ -106,6 +106,7 @@ typeof globalThis.editorTitle !== "undefined" && editorTitle.current && editorTi
|
||||
if (editorController.content) editorController.content.current = null;
|
||||
editorController.onUpdate();
|
||||
editorController.setTitle(null);
|
||||
editorController.countWords(0);
|
||||
typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",saved:""});
|
||||
`
|
||||
);
|
||||
@@ -216,7 +217,7 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
);
|
||||
};
|
||||
|
||||
updateWebclip = async ({ src, hash }: ImageAttributes) => {
|
||||
updateWebclip = async ({ src, hash }: Partial<ImageAttributes>) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.updateWebClip(${JSON.stringify({
|
||||
hash
|
||||
@@ -224,7 +225,7 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
);
|
||||
};
|
||||
|
||||
updateImage = async ({ src, hash }: ImageAttributes) => {
|
||||
updateImage = async ({ src, hash }: Partial<ImageAttributes>) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.updateImage(${JSON.stringify({
|
||||
hash
|
||||
@@ -237,6 +238,10 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
'response = window.dispatchEvent(new Event("handleBackPress",{cancelable:true}));'
|
||||
);
|
||||
};
|
||||
|
||||
keyboardShown = async (keyboardShown: boolean) => {
|
||||
return this.doAsync(`globalThis['keyboardShown']=${keyboardShown};`);
|
||||
};
|
||||
//todo add replace image function
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { launchCamera, launchImageLibrary } from "react-native-image-picker";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { db } from "../../../common/database";
|
||||
import { compressToBase64 } from "../../../common/filesystem/compress";
|
||||
import { AttachmentItem } from "../../../components/attachments/attachment-item";
|
||||
@@ -34,8 +34,8 @@ import {
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { editorController, editorState } from "./utils";
|
||||
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
|
||||
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
|
||||
import { isImage } from "@notesnook/core/utils/filename";
|
||||
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
|
||||
|
||||
const showEncryptionSheet = (file) => {
|
||||
presentSheet({
|
||||
@@ -74,7 +74,7 @@ const file = async (fileOptions) => {
|
||||
mode: "import",
|
||||
allowMultiSelection: false
|
||||
};
|
||||
if (Platform.OS == "ios") {
|
||||
if (Platform.OS === "ios") {
|
||||
options.copyTo = "cachesDirectory";
|
||||
}
|
||||
await db.attachments.generateKey();
|
||||
@@ -87,14 +87,9 @@ const file = async (fileOptions) => {
|
||||
}
|
||||
|
||||
file = file[0];
|
||||
if (file.type.startsWith("image")) {
|
||||
ToastEvent.show({
|
||||
title: "Type not supported",
|
||||
message: "Please add images from gallery or camera picker.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
|
||||
|
||||
if (file.size > FILE_SIZE_LIMIT) {
|
||||
ToastEvent.show({
|
||||
title: "File too large",
|
||||
@@ -114,7 +109,6 @@ const file = async (fileOptions) => {
|
||||
return;
|
||||
}
|
||||
|
||||
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
|
||||
console.log("file uri: ", uri);
|
||||
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
|
||||
showEncryptionSheet(file);
|
||||
@@ -125,12 +119,24 @@ const file = async (fileOptions) => {
|
||||
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
|
||||
return;
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
editorController.current?.commands.insertAttachment({
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
type: file.type,
|
||||
size: file.size
|
||||
});
|
||||
if (isImage(file.type)) {
|
||||
editorController.current?.commands.insertImage({
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
dataurl: await db.attachments.read(hash, "base64"),
|
||||
title: file.name
|
||||
});
|
||||
} else {
|
||||
editorController.current?.commands.insertAttachment({
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
eSendEvent(eCloseSheet);
|
||||
}, 1000);
|
||||
@@ -255,7 +261,7 @@ const handleImageResponse = async (response, options) => {
|
||||
|
||||
editorController.current?.commands.insertImage({
|
||||
hash: hash,
|
||||
type: image.type,
|
||||
mime: image.type,
|
||||
title: fileName,
|
||||
dataurl: b64,
|
||||
size: image.fileSize,
|
||||
@@ -263,7 +269,7 @@ const handleImageResponse = async (response, options) => {
|
||||
});
|
||||
};
|
||||
|
||||
async function attachFile(uri, hash, type, filename, options) {
|
||||
export async function attachFile(uri, hash, type, filename, options) {
|
||||
try {
|
||||
let exists = db.attachments.exists(hash);
|
||||
let encryptionInfo;
|
||||
@@ -282,7 +288,7 @@ async function attachFile(uri, hash, type, filename, options) {
|
||||
let key = await db.attachments.generateKey();
|
||||
encryptionInfo = await Sodium.encryptFile(key, {
|
||||
uri: uri,
|
||||
type: "url",
|
||||
type: options.type || "url",
|
||||
hash: hash
|
||||
});
|
||||
encryptionInfo.type = type;
|
||||
|
||||
@@ -48,6 +48,8 @@ export type Settings = {
|
||||
keyboardShown?: boolean;
|
||||
doubleSpacedLines?: boolean;
|
||||
corsProxy: string;
|
||||
fontSize: string;
|
||||
fontFamily: string;
|
||||
};
|
||||
|
||||
export type EditorProps = {
|
||||
|
||||
@@ -25,6 +25,8 @@ import { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
BackHandler,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
KeyboardEventListener,
|
||||
NativeEventSubscription
|
||||
} from "react-native";
|
||||
import { WebViewMessageEvent } from "react-native-webview";
|
||||
@@ -32,13 +34,12 @@ import { db } from "../../../common/database";
|
||||
import ManageTagsSheet from "../../../components/sheets/manage-tags";
|
||||
import { RelationsList } from "../../../components/sheets/relations-list";
|
||||
import ReminderSheet from "../../../components/sheets/reminder";
|
||||
import useKeyboard from "../../../hooks/use-keyboard";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastEvent,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
@@ -135,8 +136,30 @@ export const useEditorEvents = (
|
||||
const doubleSpacedLines = useSettingStore(
|
||||
(state) => state.settings?.doubleSpacedLines
|
||||
);
|
||||
const defaultFontSize = useSettingStore(
|
||||
(state) => state.settings.defaultFontSize
|
||||
);
|
||||
const defaultFontFamily = useSettingStore(
|
||||
(state) => state.settings.defaultFontFamily
|
||||
);
|
||||
|
||||
const tools = useDragState((state) => state.data);
|
||||
const { keyboardShown } = useKeyboard();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyboardDidShow: KeyboardEventListener = () => {
|
||||
editor.commands.keyboardShown(true);
|
||||
};
|
||||
const handleKeyboardDidHide: KeyboardEventListener = () => {
|
||||
editor.commands.keyboardShown(false);
|
||||
};
|
||||
const subscriptions = [
|
||||
Keyboard.addListener("keyboardDidShow", handleKeyboardDidShow),
|
||||
Keyboard.addListener("keyboardDidHide", handleKeyboardDidHide)
|
||||
];
|
||||
return () => {
|
||||
subscriptions.forEach((subscription) => subscription.remove());
|
||||
};
|
||||
}, [editor.commands]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.commands.setSettings({
|
||||
@@ -147,9 +170,10 @@ export const useEditorEvents = (
|
||||
tools: tools || getDefaultPresets().default,
|
||||
noHeader: noHeader,
|
||||
noToolbar: readonly || editorPropReadonly || noToolbar,
|
||||
keyboardShown: keyboardShown || false,
|
||||
doubleSpacedLines: doubleSpacedLines,
|
||||
corsProxy: corsProxy
|
||||
corsProxy: corsProxy,
|
||||
fontSize: defaultFontSize,
|
||||
fontFamily: defaultFontFamily
|
||||
});
|
||||
}, [
|
||||
fullscreen,
|
||||
@@ -160,12 +184,13 @@ export const useEditorEvents = (
|
||||
deviceMode,
|
||||
tools,
|
||||
editor.commands,
|
||||
keyboardShown,
|
||||
doubleSpacedLines,
|
||||
editorPropReadonly,
|
||||
noHeader,
|
||||
noToolbar,
|
||||
corsProxy
|
||||
corsProxy,
|
||||
defaultFontSize,
|
||||
defaultFontFamily
|
||||
]);
|
||||
|
||||
const onBackPress = useCallback(async () => {
|
||||
@@ -189,12 +214,7 @@ export const useEditorEvents = (
|
||||
setImmediate(() => {
|
||||
useEditorStore.getState().setCurrentlyEditingNote(null);
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"ColoredNotes",
|
||||
"Notes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}, 500);
|
||||
});
|
||||
editorState().currentlyEditing = false;
|
||||
@@ -273,12 +293,28 @@ export const useEditorEvents = (
|
||||
(event: WebViewMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
const editorMessage = JSON.parse(data) as EditorMessage;
|
||||
|
||||
if (editorMessage.type === EventTypes.content) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
content: editorMessage.value as string,
|
||||
forSessionId: editorMessage.sessionId
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.title) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value as string,
|
||||
forSessionId: editorMessage.sessionId
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
editorMessage.sessionId !== editor.sessionId &&
|
||||
editorMessage.type !== EditorEvents.status
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (editorMessage.type) {
|
||||
case EventTypes.logger:
|
||||
logger.info("[WEBVIEW LOG]", editorMessage.value);
|
||||
@@ -286,21 +322,8 @@ export const useEditorEvents = (
|
||||
case EventTypes.contentchange:
|
||||
editor.onContentChanged();
|
||||
break;
|
||||
case EventTypes.content:
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
content: editorMessage.value as string
|
||||
});
|
||||
break;
|
||||
case EventTypes.selection:
|
||||
break;
|
||||
case EventTypes.title:
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value as string
|
||||
});
|
||||
break;
|
||||
|
||||
case EventTypes.reminders:
|
||||
if (!editor.note.current) {
|
||||
ToastEvent.show({
|
||||
@@ -326,7 +349,7 @@ export const useEditorEvents = (
|
||||
});
|
||||
return;
|
||||
}
|
||||
ManageTagsSheet.present(editor.note.current);
|
||||
ManageTagsSheet.present([editor.note.current]);
|
||||
break;
|
||||
case EventTypes.tag:
|
||||
if (editorMessage.value) {
|
||||
@@ -337,13 +360,7 @@ export const useEditorEvents = (
|
||||
.then(async () => {
|
||||
useTagStore.getState().setTags();
|
||||
await editor.commands.setTags(editor.note.current);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"ColoredNotes",
|
||||
"Notes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Tags"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -381,9 +398,18 @@ export const useEditorEvents = (
|
||||
openLinkInBrowser(editorMessage.value as string);
|
||||
break;
|
||||
|
||||
case EventTypes.previewAttachment:
|
||||
eSendEvent("ImagePreview", editorMessage.value);
|
||||
case EventTypes.previewAttachment: {
|
||||
const hash = (editorMessage.value as Attachment)?.hash;
|
||||
const attachment = db.attachments?.attachment(hash);
|
||||
if (attachment.metadata.type.startsWith("image/")) {
|
||||
eSendEvent("ImagePreview", editorMessage.value);
|
||||
} else {
|
||||
eSendEvent("PDFPreview", attachment);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -43,9 +43,9 @@ import { NoteType } from "../../../utils/types";
|
||||
import Commands from "./commands";
|
||||
import { Content, EditorState, Note, SavePayload } from "./types";
|
||||
import {
|
||||
EditorEvents,
|
||||
clearAppState,
|
||||
defaultState,
|
||||
EditorEvents,
|
||||
getAppState,
|
||||
isContentInvalid,
|
||||
isEditorLoaded,
|
||||
@@ -77,10 +77,11 @@ export const useEditor = (
|
||||
const lastContentChangeTime = useRef<number>(0);
|
||||
const lock = useRef(false);
|
||||
const loadedImages = useRef<{ [name: string]: boolean }>({});
|
||||
const lockedSessionId = useRef<string>();
|
||||
|
||||
const postMessage = useCallback(
|
||||
async <T>(type: string, data: T) =>
|
||||
await post(editorRef, sessionIdRef.current, type, data),
|
||||
async <T>(type: string, data: T, waitFor = 300) =>
|
||||
await post(editorRef, sessionIdRef.current, type, data, waitFor),
|
||||
[sessionIdRef]
|
||||
);
|
||||
|
||||
@@ -119,27 +120,11 @@ export const useEditor = (
|
||||
[editorId]
|
||||
);
|
||||
|
||||
const onReady = useCallback(async () => {
|
||||
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
|
||||
overlay(true);
|
||||
setLoading(true);
|
||||
}
|
||||
}, [overlay]);
|
||||
|
||||
useEffect(() => {
|
||||
state.current.saveCount = 0;
|
||||
async () => {
|
||||
await commands.setSessionId(sessionIdRef.current);
|
||||
if (sessionIdRef.current) {
|
||||
if (!state.current?.ready) return;
|
||||
await onReady();
|
||||
}
|
||||
};
|
||||
}, [sessionId, loading, commands, onReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
setLoading(false);
|
||||
} else {
|
||||
state.current.ready = false;
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
@@ -152,7 +137,7 @@ export const useEditor = (
|
||||
);
|
||||
|
||||
const reset = useCallback(
|
||||
async (resetState = true) => {
|
||||
async (resetState = true, resetContent = true) => {
|
||||
currentNote.current?.id && db.fs.cancel(currentNote.current.id);
|
||||
currentNote.current = null;
|
||||
loadedImages.current = {};
|
||||
@@ -161,10 +146,10 @@ export const useEditor = (
|
||||
sessionHistoryId.current = undefined;
|
||||
saveCount.current = 0;
|
||||
useEditorStore.getState().setReadonly(false);
|
||||
postMessage(EditorEvents.title, "");
|
||||
resetContent && postMessage(EditorEvents.title, "");
|
||||
lastContentChangeTime.current = 0;
|
||||
await commands.clearContent();
|
||||
await commands.clearTags();
|
||||
resetContent && (await commands.clearContent());
|
||||
resetContent && (await commands.clearTags());
|
||||
if (resetState) {
|
||||
isDefaultEditor &&
|
||||
useEditorStore.getState().setCurrentlyEditingNote(null);
|
||||
@@ -239,9 +224,15 @@ export const useEditor = (
|
||||
|
||||
if (
|
||||
useEditorStore.getState().currentEditingNote !== id &&
|
||||
isDefaultEditor
|
||||
isDefaultEditor &&
|
||||
state.current.currentlyEditing
|
||||
) {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
(currentNote.current?.id && currentNote.current?.id !== id) ||
|
||||
!state.current.currentlyEditing
|
||||
)
|
||||
return;
|
||||
id && useEditorStore.getState().setCurrentlyEditingNote(id);
|
||||
});
|
||||
}
|
||||
@@ -262,12 +253,7 @@ export const useEditor = (
|
||||
currentNote.current?.headline?.slice(0, 200) !==
|
||||
note.headline?.slice(0, 200)
|
||||
) {
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"ColoredNotes",
|
||||
"Notes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +340,6 @@ export const useEditor = (
|
||||
) => {
|
||||
state.current.currentlyEditing = true;
|
||||
const editorState = useEditorStore.getState();
|
||||
|
||||
if (item && item.type === "new") {
|
||||
currentNote.current && (await reset());
|
||||
const nextSessionId = makeSessionId(item as NoteType);
|
||||
@@ -362,28 +347,47 @@ export const useEditor = (
|
||||
sessionIdRef.current = nextSessionId;
|
||||
sessionHistoryId.current = Date.now();
|
||||
await commands.setSessionId(nextSessionId);
|
||||
await commands.focus();
|
||||
if (state.current?.ready) await commands.focus();
|
||||
lastContentChangeTime.current = 0;
|
||||
useEditorStore.getState().setReadonly(false);
|
||||
} else {
|
||||
if (!item.forced && currentNote.current?.id === item.id) return;
|
||||
state.current.movedAway = false;
|
||||
state.current.currentlyEditing = true;
|
||||
isDefaultEditor && editorState.setCurrentlyEditingNote(item.id);
|
||||
overlay(true, item);
|
||||
currentNote.current && (await reset(false));
|
||||
currentNote.current && (await reset(false, false));
|
||||
await loadContent(item as NoteType);
|
||||
if (
|
||||
!currentContent.current?.data ||
|
||||
currentContent.current?.data.length < 50000
|
||||
) {
|
||||
if (state.current.ready) overlay(false);
|
||||
} else {
|
||||
overlay(true);
|
||||
}
|
||||
lastContentChangeTime.current = item.dateEdited;
|
||||
const nextSessionId = makeSessionId(item as NoteType);
|
||||
lockedSessionId.current = nextSessionId;
|
||||
sessionHistoryId.current = Date.now();
|
||||
setSessionId(nextSessionId);
|
||||
commands.setSessionId(nextSessionId);
|
||||
sessionIdRef.current = nextSessionId;
|
||||
await commands.setSessionId(nextSessionId);
|
||||
currentNote.current = item as NoteType;
|
||||
await commands.setStatus(timeConverter(item.dateEdited), "Saved");
|
||||
await postMessage(EditorEvents.title, item.title);
|
||||
await postMessage(EditorEvents.html, currentContent.current?.data);
|
||||
await postMessage(
|
||||
EditorEvents.html,
|
||||
currentContent.current?.data,
|
||||
10000
|
||||
);
|
||||
useEditorStore.getState().setReadonly(item.readonly);
|
||||
await commands.setTags(currentNote.current);
|
||||
commands.setSettings();
|
||||
setTimeout(() => {
|
||||
if (lockedSessionId.current === nextSessionId) {
|
||||
lockedSessionId.current = undefined;
|
||||
}
|
||||
}, 300);
|
||||
overlay(false);
|
||||
loadImages();
|
||||
}
|
||||
@@ -490,14 +494,16 @@ export const useEditor = (
|
||||
({
|
||||
title,
|
||||
content,
|
||||
type
|
||||
type,
|
||||
forSessionId
|
||||
}: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
type: string;
|
||||
forSessionId: string;
|
||||
}) => {
|
||||
if (lock.current || lockedSessionId.current === forSessionId) return;
|
||||
lastContentChangeTime.current = Date.now();
|
||||
if (lock.current) return;
|
||||
if (type === EditorEvents.content) {
|
||||
currentContent.current = {
|
||||
data: content,
|
||||
@@ -505,22 +511,27 @@ export const useEditor = (
|
||||
noteId: currentNote.current?.id as string
|
||||
};
|
||||
}
|
||||
const noteIdFromSessionId =
|
||||
!forSessionId || forSessionId.startsWith("session")
|
||||
? null
|
||||
: forSessionId.split("_")[0];
|
||||
|
||||
const noteId = noteIdFromSessionId || currentNote.current?.id;
|
||||
const params = {
|
||||
title,
|
||||
data: content,
|
||||
type: "tiptap",
|
||||
sessionId,
|
||||
id: currentNote.current?.id,
|
||||
sessionId: forSessionId,
|
||||
id: noteId,
|
||||
sessionHistoryId: sessionHistoryId.current
|
||||
};
|
||||
|
||||
withTimer(
|
||||
currentNote.current?.id || "newnote",
|
||||
noteId || "newnote",
|
||||
() => {
|
||||
if (
|
||||
currentNote.current &&
|
||||
!params.id &&
|
||||
params.sessionId === sessionId
|
||||
params.sessionId === forSessionId
|
||||
) {
|
||||
params.id = currentNote.current?.id;
|
||||
}
|
||||
@@ -530,10 +541,10 @@ export const useEditor = (
|
||||
}
|
||||
saveNote(params);
|
||||
},
|
||||
500
|
||||
150
|
||||
);
|
||||
},
|
||||
[sessionId, withTimer, onChange, saveNote]
|
||||
[withTimer, onChange, saveNote]
|
||||
);
|
||||
|
||||
const restoreEditorState = useCallback(async () => {
|
||||
@@ -543,6 +554,7 @@ export const useEditor = (
|
||||
state.current.isRestoringState = true;
|
||||
state.current.currentlyEditing = true;
|
||||
state.current.movedAway = false;
|
||||
|
||||
if (!DDS.isTab) {
|
||||
tabBarRef.current?.goToPage(1, false);
|
||||
}
|
||||
@@ -562,10 +574,6 @@ export const useEditor = (
|
||||
state.current.isRestoringState = false;
|
||||
}, [loadNote, overlay]);
|
||||
|
||||
useEffect(() => {
|
||||
isDefaultEditor && restoreEditorState();
|
||||
}, [isDefaultEditor, restoreEditorState]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOnLoadNote + editorId, loadNote);
|
||||
return () => {
|
||||
@@ -573,19 +581,40 @@ export const useEditor = (
|
||||
};
|
||||
}, [editorId, loadNote, restoreEditorState, isDefaultEditor]);
|
||||
|
||||
const onLoad = useCallback(async () => {
|
||||
state.current.ready = true;
|
||||
onReady();
|
||||
postMessage(EditorEvents.theme, theme || useThemeStore.getState().colors);
|
||||
commands.setInsets(
|
||||
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
|
||||
);
|
||||
if (currentNote.current) {
|
||||
loadNote({ ...currentNote.current, forced: true });
|
||||
const onContentChanged = () => {
|
||||
lastContentChangeTime.current = Date.now();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
state.current.saveCount = 0;
|
||||
}, [sessionId, loading]);
|
||||
|
||||
const onReady = useCallback(async () => {
|
||||
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
|
||||
eSendEvent("webview_reset");
|
||||
} else {
|
||||
await commands.setPlaceholder(placeholderTip.current);
|
||||
isDefaultEditor && restoreEditorState();
|
||||
}
|
||||
commands.setSettings();
|
||||
}, [isDefaultEditor, restoreEditorState]);
|
||||
|
||||
const onLoad = useCallback(async () => {
|
||||
if (currentNote.current) overlay(true);
|
||||
clearTimeout(timers.current["editor:loaded"]);
|
||||
timers.current["editor:loaded"] = setTimeout(async () => {
|
||||
postMessage(EditorEvents.theme, theme || useThemeStore.getState().colors);
|
||||
commands.setInsets(
|
||||
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
|
||||
);
|
||||
await commands.setSessionId(sessionIdRef.current);
|
||||
await onReady();
|
||||
await commands.setSettings();
|
||||
if (currentNote.current) {
|
||||
loadNote({ ...currentNote.current, forced: true });
|
||||
} else {
|
||||
await commands.setPlaceholder(placeholderTip.current);
|
||||
}
|
||||
state.current.ready = true;
|
||||
}, 300);
|
||||
}, [
|
||||
onReady,
|
||||
postMessage,
|
||||
@@ -593,13 +622,10 @@ export const useEditor = (
|
||||
commands,
|
||||
isDefaultEditor,
|
||||
insets,
|
||||
loadNote
|
||||
loadNote,
|
||||
overlay
|
||||
]);
|
||||
|
||||
const onContentChanged = () => {
|
||||
lastContentChangeTime.current = Date.now();
|
||||
};
|
||||
|
||||
return {
|
||||
ref: editorRef,
|
||||
onLoad,
|
||||
@@ -615,6 +641,7 @@ export const useEditor = (
|
||||
saveContent,
|
||||
onContentChanged,
|
||||
editorId: editorId,
|
||||
markImageLoaded
|
||||
markImageLoaded,
|
||||
overlay
|
||||
};
|
||||
};
|
||||
|
||||
@@ -73,7 +73,8 @@ export async function post<T>(
|
||||
ref: RefObject<WebView>,
|
||||
sessionId: string,
|
||||
type: string,
|
||||
value: T | null = null
|
||||
value: T | null = null,
|
||||
waitFor = 300
|
||||
) {
|
||||
if (!sessionId) {
|
||||
console.warn("post called without sessionId of type:", type);
|
||||
@@ -85,7 +86,7 @@ export async function post<T>(
|
||||
sessionId: sessionId
|
||||
};
|
||||
setImmediate(() => ref.current?.postMessage(JSON.stringify(message)));
|
||||
const response = await getResponse(type);
|
||||
const response = await getResponse(type, waitFor);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -97,7 +98,8 @@ type WebviewResponseData = {
|
||||
};
|
||||
|
||||
export const getResponse = async (
|
||||
type: string
|
||||
type: string,
|
||||
waitFor = 300
|
||||
): Promise<WebviewResponseData | false> => {
|
||||
return new Promise((resolve) => {
|
||||
const callback = (data: WebviewResponseData) => {
|
||||
@@ -107,7 +109,7 @@ export const getResponse = async (
|
||||
eSubscribeEvent(type, callback);
|
||||
setTimeout(() => {
|
||||
resolve(false);
|
||||
}, 5000);
|
||||
}, waitFor);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { editorRef } from "../../utils/global-refs";
|
||||
import { ProgressBar } from "./progress";
|
||||
import { editorController, editorState, textInput } from "./tiptap/utils";
|
||||
import { editorController, textInput } from "./tiptap/utils";
|
||||
export const EditorWrapper = ({ width }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
@@ -48,9 +48,9 @@ export const EditorWrapper = ({ width }) => {
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const onAppStateChanged = async (state) => {
|
||||
if (editorState().movedAway) return;
|
||||
if (state === "active") {
|
||||
editorController.current.onReady();
|
||||
editorController.current.overlay(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ import { db } from "../../common/database";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import List from "../../components/list";
|
||||
import { NotebookHeader } from "../../components/list-items/headers/notebook-header";
|
||||
import { AddNotebookSheet } from "../../components/sheets/add-notebook";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
@@ -33,7 +33,7 @@ import SearchService from "../../services/search";
|
||||
import useNavigationStore, {
|
||||
NotebookScreenParams
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { eOnNewTopicAdded, eOpenAddNotebookDialog } from "../../utils/events";
|
||||
import { eOnNewTopicAdded } from "../../utils/events";
|
||||
import { NotebookType } from "../../utils/types";
|
||||
import { openEditor, setOnFirstSave } from "../notes/common";
|
||||
const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
@@ -118,6 +118,7 @@ const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
get: () => {
|
||||
const notebook = db.notebooks?.notebook(params?.current?.item?.id)
|
||||
?.data as NotebookType;
|
||||
if (!notebook) return [];
|
||||
return db.relations?.from(notebook, "note");
|
||||
}
|
||||
});
|
||||
@@ -148,7 +149,7 @@ const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
ListHeader={
|
||||
<NotebookHeader
|
||||
onEditNotebook={() => {
|
||||
eSendEvent(eOpenAddNotebookDialog, params.current.item);
|
||||
AddNotebookSheet.present(params.current.item);
|
||||
}}
|
||||
notebook={params.current.item}
|
||||
/>
|
||||
|
||||
@@ -94,15 +94,6 @@ async function onNoteCreated(id: string, params: FirstSaveData) {
|
||||
{ type: "notebook", id: params.id },
|
||||
{ type: "note", id: id }
|
||||
);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
editorState().onNoteCreated = null;
|
||||
break;
|
||||
}
|
||||
@@ -116,41 +107,18 @@ async function onNoteCreated(id: string, params: FirstSaveData) {
|
||||
id
|
||||
);
|
||||
editorState().onNoteCreated = null;
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
break;
|
||||
}
|
||||
case "tag": {
|
||||
await db.notes?.note(id).tag(params.id);
|
||||
editorState().onNoteCreated = null;
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
useTagStore.getState().setTags();
|
||||
break;
|
||||
}
|
||||
case "color": {
|
||||
await db.notes?.note(id).color(params.color);
|
||||
editorState().onNoteCreated = null;
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
useMenuStore.getState().setColorNotes();
|
||||
break;
|
||||
}
|
||||
@@ -158,4 +126,5 @@ async function onNoteCreated(id: string, params: FirstSaveData) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
|
||||
@@ -18,9 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { FloatingButton } from "../../components/container/floating-button";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import List from "../../components/list";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
@@ -34,7 +38,9 @@ import useNavigationStore, {
|
||||
RouteName
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { NoteType, TopicType } from "../../utils/types";
|
||||
import Notebook from "../notebook/index";
|
||||
import {
|
||||
getAlias,
|
||||
openEditor,
|
||||
@@ -42,13 +48,6 @@ import {
|
||||
setOnFirstSave,
|
||||
toCamelCase
|
||||
} from "./common";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Notebook from "../notebook/index";
|
||||
export const WARNING_DATA = {
|
||||
title: "Some notes in this topic are not synced"
|
||||
};
|
||||
@@ -99,7 +98,6 @@ const NotesPage = ({
|
||||
}: RouteProps<
|
||||
"NotesPage" | "TaggedNotes" | "Monographs" | "ColoredNotes" | "TopicNotes"
|
||||
>) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const params = useRef<NotesScreenParams>(route?.params);
|
||||
const [notes, setNotes] = useState<NoteType[]>(get(route.params, true));
|
||||
const loading = useNoteStore((state) => state.loading);
|
||||
@@ -183,8 +181,9 @@ const NotesPage = ({
|
||||
if (isNew) setLoadingNotes(true);
|
||||
const notes = get(params.current, true) as NoteType[];
|
||||
if (
|
||||
(item.type === "tag" || item.type === "color") &&
|
||||
(!notes || notes.length === 0)
|
||||
((item.type === "tag" || item.type === "color") &&
|
||||
(!notes || notes.length === 0)) ||
|
||||
(item.type === "topic" && !notes)
|
||||
) {
|
||||
return Navigation.goBack();
|
||||
}
|
||||
@@ -280,7 +279,9 @@ const NotesPage = ({
|
||||
placeholderData={placeholderData}
|
||||
/>
|
||||
|
||||
{notes?.length > 0 || (isFocused && !isMonograph) ? (
|
||||
{!isMonograph &&
|
||||
route.name !== "TopicNotes" &&
|
||||
(notes?.length > 0 || isFocused) ? (
|
||||
<FloatingButton title="Create a note" onPress={onPressFloatingButton} />
|
||||
) : null}
|
||||
</DelayLayout>
|
||||
|
||||
@@ -75,7 +75,11 @@ export const TopicNotes = ({
|
||||
|
||||
TopicNotes.get = (params: NotesScreenParams, grouped = true) => {
|
||||
const { id, notebookId } = params.item as TopicType;
|
||||
const notes = db.notebooks?.notebook(notebookId)?.topics.topic(id)?.all || [];
|
||||
const topic = db.notebooks?.notebook(notebookId)?.topics.topic(id);
|
||||
if (!topic) {
|
||||
return null;
|
||||
}
|
||||
const notes = topic?.all || [];
|
||||
return grouped
|
||||
? groupArray(notes, db.settings?.getGroupOptions("notes"))
|
||||
: notes;
|
||||
|
||||
@@ -28,7 +28,7 @@ import React, {
|
||||
import { ActivityIndicator, Linking, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { db } from "../../common/database";
|
||||
import Storage from "../../common/database/storage";
|
||||
import DialogHeader from "../../components/dialog/dialog-header";
|
||||
|
||||
@@ -17,33 +17,36 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Dimensions, LayoutAnimation, Platform, View } from "react-native";
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { SVG_Z } from "../../components/intro";
|
||||
import { WelcomeNotice } from "../../components/intro/welcome";
|
||||
import { AuthMode } from "../../components/auth";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { PressableButton } from "../../components/ui/pressable";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
import { SvgView } from "../../components/ui/svg";
|
||||
import { BouncingView } from "../../components/ui/transitions/bouncing-view";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import BiometicService from "../../services/biometrics";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { presentSheet, ToastEvent } from "../../services/event-manager";
|
||||
import {
|
||||
ToastEvent,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { getElevation } from "../../utils";
|
||||
import { eOpenLoginDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
const AppLock = ({ route }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const appLockMode = useSettingStore((state) => state.settings.appLockMode);
|
||||
const [step, setStep] = useState(0);
|
||||
const welcome = route?.params?.welcome;
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
const modes = [
|
||||
{
|
||||
@@ -76,13 +79,12 @@ const AppLock = ({ route }) => {
|
||||
exiting={!welcome ? undefined : FadeOutUp}
|
||||
entering={!welcome ? undefined : FadeInDown}
|
||||
style={{
|
||||
justifyContent: !welcome ? undefined : "center",
|
||||
height: !welcome ? undefined : "100%",
|
||||
width: !welcome ? undefined : "100%"
|
||||
}}
|
||||
>
|
||||
{step === 0 ? (
|
||||
<>
|
||||
<>
|
||||
{!welcome ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -125,121 +127,164 @@ const AppLock = ({ route }) => {
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
<Seperator />
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
width: DDS.isTab && welcome ? "50%" : "100%",
|
||||
alignSelf: "center"
|
||||
flex: 0.35,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.nav,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
}}
|
||||
>
|
||||
{modes.map((item) => (
|
||||
<PressableButton
|
||||
key={item.title}
|
||||
type={appLockMode === item.value ? "grayBg" : "transparent"}
|
||||
onPress={async () => {
|
||||
if (
|
||||
!(await BiometicService.isBiometryAvailable()) &&
|
||||
!useUserStore.getState().user
|
||||
) {
|
||||
ToastEvent.show({
|
||||
heading: "Biometrics not enrolled",
|
||||
type: "error",
|
||||
message:
|
||||
"To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone or create an account."
|
||||
});
|
||||
return;
|
||||
}
|
||||
SettingsService.set({ appLockMode: item.value });
|
||||
}}
|
||||
customStyle={{
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
marginTop: 0,
|
||||
marginBottom: 12,
|
||||
borderWidth: 1,
|
||||
borderColor:
|
||||
appLockMode === item.value ? item.activeColor : colors.nav
|
||||
}}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 10
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
color={
|
||||
appLockMode === item.value ? item.activeColor : colors.pri
|
||||
}
|
||||
style={{ maxWidth: "95%" }}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
color={
|
||||
appLockMode === item.value
|
||||
? item.activeColor
|
||||
: colors.icon
|
||||
}
|
||||
style={{ maxWidth: "95%" }}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{item.desc}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
))}
|
||||
|
||||
{welcome && (
|
||||
<Button
|
||||
fontSize={SIZE.md}
|
||||
height={45}
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
LayoutAnimation.configureNext({
|
||||
...LayoutAnimation.Presets.linear,
|
||||
delete: {
|
||||
duration: 50,
|
||||
property: "opacity",
|
||||
type: "linear"
|
||||
}
|
||||
});
|
||||
setStep(1);
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "center",
|
||||
borderRadius: 100,
|
||||
...getElevation(5),
|
||||
marginTop: 30
|
||||
}}
|
||||
type="accent"
|
||||
title="Next"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<WelcomeNotice />
|
||||
)}
|
||||
|
||||
{welcome && !colors.night ? (
|
||||
<BouncingView
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Protect your notes
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
>
|
||||
Choose how you want to secure your notes locally.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Seperator />
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: DDS.isTab ? -300 : -130,
|
||||
zIndex: -1
|
||||
paddingHorizontal: 12,
|
||||
width: DDS.isTab && welcome ? "50%" : "100%",
|
||||
alignSelf: "center",
|
||||
flex: 0.65
|
||||
}}
|
||||
animated={false}
|
||||
duration={3000}
|
||||
>
|
||||
<SvgView
|
||||
width={Dimensions.get("window").width}
|
||||
height={Dimensions.get("window").width}
|
||||
src={SVG_Z}
|
||||
/>
|
||||
</BouncingView>
|
||||
) : null}
|
||||
{modes.map((item) => (
|
||||
<PressableButton
|
||||
key={item.title}
|
||||
type={appLockMode === item.value ? "grayBg" : "transparent"}
|
||||
onPress={async () => {
|
||||
if (
|
||||
!(await BiometicService.isBiometryAvailable()) &&
|
||||
!useUserStore.getState().user
|
||||
) {
|
||||
ToastEvent.show({
|
||||
heading: "Biometrics not enrolled",
|
||||
type: "error",
|
||||
message:
|
||||
"To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone or create an account."
|
||||
});
|
||||
return;
|
||||
}
|
||||
SettingsService.set({ appLockMode: item.value });
|
||||
}}
|
||||
customStyle={{
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
marginTop: 0,
|
||||
marginBottom: 12,
|
||||
borderWidth: 1,
|
||||
borderColor:
|
||||
appLockMode === item.value ? item.activeColor : colors.nav
|
||||
}}
|
||||
style={{
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
color={
|
||||
appLockMode === item.value ? item.activeColor : colors.pri
|
||||
}
|
||||
style={{ maxWidth: "95%" }}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
color={
|
||||
appLockMode === item.value ? item.activeColor : colors.icon
|
||||
}
|
||||
style={{ maxWidth: "95%" }}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{item.desc}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
))}
|
||||
|
||||
{welcome && (
|
||||
<Button
|
||||
fontSize={SIZE.md}
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace(
|
||||
{
|
||||
name: "Notes"
|
||||
},
|
||||
{
|
||||
canGoBack: false
|
||||
}
|
||||
);
|
||||
}, 1000);
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "center",
|
||||
...getElevation(5),
|
||||
marginTop: 30,
|
||||
borderRadius: 100
|
||||
}}
|
||||
type="accent"
|
||||
title="Next"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Licenses } from "./licenses";
|
||||
import SoundPicker from "./sound-picker";
|
||||
import { Subscription } from "./subscription";
|
||||
import { TrashIntervalSelector } from "./trash-interval-selector";
|
||||
import { FontSelector } from "./font-selector";
|
||||
export const components: { [name: string]: ReactElement } = {
|
||||
colorpicker: <AccentColorPicker />,
|
||||
homeselector: <HomagePageSelector />,
|
||||
@@ -35,5 +36,6 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"debug-logs": <DebugLogs />,
|
||||
"sound-picker": <SoundPicker />,
|
||||
licenses: <Licenses />,
|
||||
"trash-interval-selector": <TrashIntervalSelector />
|
||||
"trash-interval-selector": <TrashIntervalSelector />,
|
||||
"font-selector": <FontSelector />
|
||||
};
|
||||
|
||||
@@ -18,12 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { LogMessage } from "@streetwriters/logger";
|
||||
import { LogMessage } from "@notesnook/logger";
|
||||
import { format, LogLevel, logManager } from "@notesnook/core/logger";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import Storage from "../../common/database/storage";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
|
||||
@@ -20,8 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { ToolId } from "@notesnook/editor/dist/toolbar";
|
||||
import React, { RefObject } from "react";
|
||||
import { View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { PressableButton } from "../../../components/ui/pressable";
|
||||
import { SvgView } from "../../../components/ui/svg";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
@@ -34,13 +32,14 @@ import {
|
||||
getToolIcon,
|
||||
getUngroupedTools
|
||||
} from "./toolbar-definition";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
export default function ToolSheet({
|
||||
group,
|
||||
fwdRef
|
||||
}: {
|
||||
group: DraggableItem;
|
||||
fwdRef: RefObject<ActionSheet>;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
}) {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const data = useDragState((state) => state.data);
|
||||
@@ -50,6 +49,7 @@ export default function ToolSheet({
|
||||
(item: ToolId) => {
|
||||
const tool = findToolById(item);
|
||||
const iconSvgString = tool ? getToolIcon(tool.icon as ToolId) : null;
|
||||
if (item === "none") return;
|
||||
return (
|
||||
<PressableButton
|
||||
key={item}
|
||||
@@ -109,12 +109,7 @@ export default function ToolSheet({
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
onMomentumScrollEnd={() => {
|
||||
fwdRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
nestedScrollEnabled={true}
|
||||
>
|
||||
<ScrollView nestedScrollEnabled={true}>
|
||||
{!ungrouped || ungrouped.length === 0 ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
|
||||
110
apps/mobile/app/screens/settings/font-selector.jsx
Normal file
110
apps/mobile/app/screens/settings/font-selector.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Menu, { MenuItem } from "react-native-reanimated-material-menu";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { PressableButton } from "../../components/ui/pressable";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { getFontById, getFonts } from "@notesnook/editor/dist/utils/font";
|
||||
|
||||
export const FontSelector = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const defaultFontFamily = useSettingStore(
|
||||
(state) => state.settings.defaultFontFamily
|
||||
);
|
||||
const menuRef = useRef();
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
const onChange = (item) => {
|
||||
menuRef.current?.hide();
|
||||
SettingsService.set({
|
||||
defaultFontFamily: item
|
||||
});
|
||||
};
|
||||
return (
|
||||
<View
|
||||
onLayout={(event) => {
|
||||
setWidth(event.nativeEvent.layout.width);
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
animationDuration={200}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.bg,
|
||||
width: width,
|
||||
marginTop: 60
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
menuRef.current?.hide();
|
||||
}}
|
||||
anchor={
|
||||
<PressableButton
|
||||
onPress={async () => {
|
||||
menuRef.current?.show();
|
||||
}}
|
||||
type="grayBg"
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 10,
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph>{getFontById(defaultFontFamily).title}</Paragraph>
|
||||
<Icon color={colors.icon} name="menu-down" size={SIZE.md} />
|
||||
</PressableButton>
|
||||
}
|
||||
>
|
||||
{getFonts().map((item) => (
|
||||
<MenuItem
|
||||
key={item.id}
|
||||
onPress={async () => {
|
||||
onChange(item.id);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor:
|
||||
defaultFontFamily === item.id ? colors.nav : "transparent",
|
||||
width: "100%",
|
||||
maxWidth: width
|
||||
}}
|
||||
textStyle={{
|
||||
fontSize: SIZE.md,
|
||||
color: defaultFontFamily === item.id ? colors.accent : colors.pri
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, { FadeInDown, FadeOutDown } from "react-native-reanimated";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import BaseDialog from "../../components/dialog/base-dialog";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
@@ -75,57 +75,55 @@ const Home = ({
|
||||
|
||||
return (
|
||||
<DelayLayout delay={300} type="settings">
|
||||
<View>
|
||||
{loading && (
|
||||
//@ts-ignore // Migrate to typescript required.
|
||||
<BaseDialog animated={false} bounce={false} visible={true}>
|
||||
{loading && (
|
||||
//@ts-ignore // Migrate to typescript required.
|
||||
<BaseDialog animated={false} bounce={false} visible={true}>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.bg,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.pri} size={SIZE.lg}>
|
||||
Logging out
|
||||
</Heading>
|
||||
<Paragraph color={colors.icon}>
|
||||
Please wait while we log out and clear app data.
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.bg,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.pri} size={SIZE.lg}>
|
||||
Logging out
|
||||
</Heading>
|
||||
<Paragraph color={colors.icon}>
|
||||
Please wait while we log out and clear app data.
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.nav}
|
||||
color={colors.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.nav}
|
||||
color={colors.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</BaseDialog>
|
||||
)}
|
||||
</View>
|
||||
</BaseDialog>
|
||||
)}
|
||||
|
||||
<Animated.FlatList
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutDown}
|
||||
data={settingsGroups}
|
||||
keyExtractor={keyExtractor}
|
||||
ListFooterComponent={<View style={{ height: 200 }} />}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
<Animated.FlatList
|
||||
entering={FadeInDown}
|
||||
data={settingsGroups}
|
||||
windowSize={1}
|
||||
keyExtractor={keyExtractor}
|
||||
ListFooterComponent={<View style={{ height: 200 }} />}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</DelayLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -268,10 +268,10 @@ export const LICENSES = [
|
||||
link: "https://github.com/RocketChat/rn-extensions-share"
|
||||
},
|
||||
{
|
||||
name: "rn-fetch-blob",
|
||||
name: "react-native-blob-util",
|
||||
licenseType: "MIT",
|
||||
author: "Joltup",
|
||||
link: "https://github.com/joltup/rn-fetch-blob"
|
||||
author: "RonRadtke",
|
||||
link: "https://github.com/RonRadtke/react-native-blob-util"
|
||||
},
|
||||
{
|
||||
name: "react-native-gzip",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user