mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 03:29:21 +02:00
Compare commits
201 Commits
fix-offlin
...
mobile-rel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2cf3e81af | ||
|
|
28fa133b6d | ||
|
|
37893ced80 | ||
|
|
900c48292c | ||
|
|
302ae5ddb4 | ||
|
|
24fca08fdc | ||
|
|
91077dc132 | ||
|
|
273bbea938 | ||
|
|
36c44ad201 | ||
|
|
46f5a2d7a7 | ||
|
|
9b359a91cc | ||
|
|
0af831d50b | ||
|
|
3efaeaeeb9 | ||
|
|
00a20dda48 | ||
|
|
ec45160b27 | ||
|
|
eda284ca25 | ||
|
|
1ed331b4bb | ||
|
|
ee74bb4fbf | ||
|
|
df104669a3 | ||
|
|
9621b43bf2 | ||
|
|
503c1a5c8d | ||
|
|
a2fa1d4d78 | ||
|
|
6c6ad9a766 | ||
|
|
fc8807f74d | ||
|
|
edec48f9d4 | ||
|
|
ce98551057 | ||
|
|
74a2cc5b19 | ||
|
|
9628a940e9 | ||
|
|
d48643cbb9 | ||
|
|
558beaf216 | ||
|
|
6cc5d2452b | ||
|
|
e934ef7ffd | ||
|
|
e2d8032ef4 | ||
|
|
81e6101fb5 | ||
|
|
070354920b | ||
|
|
404168fb86 | ||
|
|
d5fa9fd02b | ||
|
|
6cc66d91be | ||
|
|
20a3b3b95e | ||
|
|
9145ea4234 | ||
|
|
0fa79433bd | ||
|
|
5185273b38 | ||
|
|
eadd23de4d | ||
|
|
71d9738980 | ||
|
|
80328af888 | ||
|
|
5e83da5575 | ||
|
|
521afe90ca | ||
|
|
ee3b7b5d7d | ||
|
|
95a9775c26 | ||
|
|
93d1319b25 | ||
|
|
b2bac3808b | ||
|
|
ebf58e68cf | ||
|
|
dc3e909dfe | ||
|
|
bf09e9615a | ||
|
|
b2d30d47d6 | ||
|
|
b1296fd24f | ||
|
|
62d8c89300 | ||
|
|
80c44eee89 | ||
|
|
153eb126fc | ||
|
|
f1fc166713 | ||
|
|
dda36bb036 | ||
|
|
843ad08813 | ||
|
|
a0e3c923e4 | ||
|
|
3095b03b09 | ||
|
|
ea5a0e0a7c | ||
|
|
2b4ec5b03b | ||
|
|
59f7add065 | ||
|
|
9c7bfc2347 | ||
|
|
ef90f40be4 | ||
|
|
bca6a4f22c | ||
|
|
1d3e2b56b2 | ||
|
|
a93a20ed5b | ||
|
|
971a80c183 | ||
|
|
125b60facc | ||
|
|
ef99b47178 | ||
|
|
d65f870213 | ||
|
|
5b3f1c9de2 | ||
|
|
4afb090cb1 | ||
|
|
8cf6e65ed8 | ||
|
|
c85c51ec08 | ||
|
|
dbeabea6c5 | ||
|
|
dfbe2d0f0f | ||
|
|
ea2277ea4e | ||
|
|
ad78105a6e | ||
|
|
36584175e2 | ||
|
|
084b1fdfd2 | ||
|
|
bed091e20f | ||
|
|
4420663687 | ||
|
|
677ae82ffa | ||
|
|
48a3fac9e0 | ||
|
|
e797d37b01 | ||
|
|
ffafd2e787 | ||
|
|
8500c971eb | ||
|
|
46a67a56fb | ||
|
|
5dc62e6df2 | ||
|
|
4479d6cf56 | ||
|
|
1faaf1f6b1 | ||
|
|
1f33ef191a | ||
|
|
d5bf8a6e3c | ||
|
|
5c252c7843 | ||
|
|
d2e10ccaf6 | ||
|
|
89d2b6870f | ||
|
|
26a9086d96 | ||
|
|
9fc8e6b1e5 | ||
|
|
6718650619 | ||
|
|
a622949f23 | ||
|
|
5a2df5b2c1 | ||
|
|
ae751cdd65 | ||
|
|
0d00bb2866 | ||
|
|
4299965064 | ||
|
|
6f3fc8db3d | ||
|
|
db731856f8 | ||
|
|
217f1dd6d4 | ||
|
|
cf87929476 | ||
|
|
d343a0720e | ||
|
|
51bd58edb2 | ||
|
|
f778d83316 | ||
|
|
b169206c68 | ||
|
|
4cea69e71d | ||
|
|
04d422b411 | ||
|
|
49dec52aa0 | ||
|
|
b83a62814d | ||
|
|
ce5449f25a | ||
|
|
aa769a8921 | ||
|
|
a8d4e6c754 | ||
|
|
f2402cf086 | ||
|
|
07a481f002 | ||
|
|
f41638db6d | ||
|
|
82baf41d8e | ||
|
|
e60370d4dc | ||
|
|
3e412323ee | ||
|
|
d635c1a056 | ||
|
|
f02db2a024 | ||
|
|
8c74324ff8 | ||
|
|
f2230eb39b | ||
|
|
ab900ae5da | ||
|
|
1cf28c56c2 | ||
|
|
08cd33a0cf | ||
|
|
7b0d2a239d | ||
|
|
5d922d934c | ||
|
|
5bab9ea914 | ||
|
|
da06d7e636 | ||
|
|
aece140473 | ||
|
|
656f87e3c6 | ||
|
|
36a95616d9 | ||
|
|
60bdc4715d | ||
|
|
7da168ab52 | ||
|
|
b20ad48b31 | ||
|
|
8b17495f0e | ||
|
|
e113e1b59c | ||
|
|
6bd4924c0b | ||
|
|
a2939aa183 | ||
|
|
effa726b65 | ||
|
|
20d9bf1f23 | ||
|
|
09e5d014df | ||
|
|
add1b8a68a | ||
|
|
fe143f66b7 | ||
|
|
75c8ca29ef | ||
|
|
c52779df23 | ||
|
|
b7b77646e6 | ||
|
|
1157c6b7cd | ||
|
|
ab824d7f66 | ||
|
|
702e28a6e6 | ||
|
|
0e0ab58959 | ||
|
|
e5cdc9ea3d | ||
|
|
d3c9744e65 | ||
|
|
6b02144b30 | ||
|
|
91cbc5ddf0 | ||
|
|
93aa1ca5aa | ||
|
|
6ae13ebb2f | ||
|
|
73cdbf64c7 | ||
|
|
0aeea965c0 | ||
|
|
a63b15637f | ||
|
|
5153b43bc6 | ||
|
|
9ea7d09722 | ||
|
|
b7b474b245 | ||
|
|
e2184a8bdf | ||
|
|
9ea84ca9ae | ||
|
|
50136e3689 | ||
|
|
e87f5e5f89 | ||
|
|
d44792d132 | ||
|
|
50b01e0b1b | ||
|
|
122c3a96c7 | ||
|
|
4d407bd646 | ||
|
|
16b6b37b94 | ||
|
|
8c766ef4f0 | ||
|
|
8d9df5030c | ||
|
|
4a158b2547 | ||
|
|
e381e54bb7 | ||
|
|
7001f98353 | ||
|
|
a836642d74 | ||
|
|
a3d3add48f | ||
|
|
994060cce2 | ||
|
|
75103dc706 | ||
|
|
cf63c2fdc5 | ||
|
|
f15b4cf634 | ||
|
|
6430611e3e | ||
|
|
afe57e41ce | ||
|
|
86f3f90143 | ||
|
|
d3ea51cc01 | ||
|
|
94dbec4486 |
30
.github/pull_request_template.md
vendored
Normal file
30
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
## Description
|
||||
<!-- Add a detailed summary of what this feature/bugfix does -->
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature
|
||||
|
||||
## Visuals
|
||||
- [ ] Attached relevant screenshots / screen recording / GIF
|
||||
- [ ] N/A (not a feature or no UI changes)
|
||||
|
||||
## Testing
|
||||
- [ ] Ran all E2E tests
|
||||
- [ ] Ran all integration tests
|
||||
- [ ] Added/updated tests for this change (if needed)
|
||||
- [ ] N/A (tests not needed — explanation provided below)
|
||||
|
||||
### If tests were not added, explain why
|
||||
<!-- explanation -->
|
||||
|
||||
## Platform
|
||||
<!-- Describe which platforms this PR is related to -->
|
||||
|
||||
- [ ] Web
|
||||
- [ ] Mobile
|
||||
- [ ] Desktop
|
||||
|
||||
## Sign-off
|
||||
- [ ] QA passed
|
||||
- [ ] UI/UX passed
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.8",
|
||||
"version": "3.3.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.8",
|
||||
"version": "3.3.11",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.8",
|
||||
"version": "3.3.11",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -252,6 +252,7 @@ export const osIntegrationRouter = t.router({
|
||||
if (menuItem) menu.append(menuItem);
|
||||
}
|
||||
if (menu.items.length > 0) menu.popup();
|
||||
menu.on("menu-will-close", () => emit.next([]));
|
||||
return () => {
|
||||
menu.removeAllListeners();
|
||||
menu.closePopup();
|
||||
|
||||
@@ -113,7 +113,8 @@ export class SQLite {
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
if (e instanceof Error)
|
||||
throw rewriteError(e, `${e.message} (query: ${sql})`);
|
||||
throw e;
|
||||
} finally {
|
||||
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
|
||||
@@ -212,3 +213,11 @@ function getExtensionPath(extensionName: string, entryPoint: string) {
|
||||
}
|
||||
return loadablePath;
|
||||
}
|
||||
|
||||
function rewriteError(e: Error, message: string) {
|
||||
const error = new Error(message);
|
||||
error.stack = e.stack;
|
||||
error.name = e.name;
|
||||
error.cause = e.cause;
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,17 @@ export const windowRouter = t.router({
|
||||
}),
|
||||
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
|
||||
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
|
||||
onClose: t.procedure.subscription(() => {
|
||||
return observable<void>((emit) => {
|
||||
function listener() {
|
||||
emit.next();
|
||||
}
|
||||
globalThis.window?.addListener("close", listener);
|
||||
return () => {
|
||||
globalThis.window?.removeListener("close", listener);
|
||||
};
|
||||
});
|
||||
}),
|
||||
onWindowStateChanged: t.procedure.subscription(() => {
|
||||
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
|
||||
function listener() {
|
||||
|
||||
@@ -52,6 +52,9 @@ locale.then(({ default: locale }) => {
|
||||
});
|
||||
setI18nGlobal(i18n);
|
||||
|
||||
const appHostnames = isDevelopment()
|
||||
? ["localhost", "127.0.0.1"]
|
||||
: ["app.notesnook.com"];
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
console.log("Another instance is already running!");
|
||||
@@ -173,6 +176,19 @@ async function createWindow() {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
mainWindow.webContents.on("will-navigate", (event, url) => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (!appHostnames.includes(parsedUrl.hostname)) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("will-navigate: failed to parse URL", url, e);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
nativeTheme.on("updated", () => {
|
||||
setupTray();
|
||||
setupJumplist();
|
||||
|
||||
@@ -62,7 +62,17 @@ function registerProtocol() {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
return net.fetch(request, { bypassCustomProtocolHandlers: true });
|
||||
const requestHeaders = new Headers(request.headers);
|
||||
if (!requestHeaders.has("referer"))
|
||||
requestHeaders.set("referer", PROTOCOL_URL);
|
||||
return await net.fetch(
|
||||
new Request(request, {
|
||||
headers: requestHeaders
|
||||
}),
|
||||
{
|
||||
bypassCustomProtocolHandlers: true
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
</p>
|
||||
|
||||
<h1 align="center">Notesnook Mobile</h1>
|
||||
<h3 align="center">The mobile app is built using React Native, Typescript & Javascript for both iOS & Android.</h3>
|
||||
<p align="center">
|
||||
<a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
|
||||
</p>
|
||||
<h3 align="center">The mobile app is built with React Native for both iOS and Android.</h3>
|
||||
<p align="center"><a href="#build-instructions">Build instructions</a> | <a href="#developer-guide">Developer guide</a> | <a href="#running-e2e-tests-detox">E2E tests</a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.streetwriters.notesnook">
|
||||
@@ -25,21 +23,21 @@
|
||||
|
||||
Requirements:
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/download/)
|
||||
1. [Node.js](https://nodejs.org/en/download/) 20+ (the repo is pinned to Node `22.20.0` via Volta)
|
||||
2. [git](https://git-scm.com/downloads)
|
||||
3. NPM (not yarn or pnpm)
|
||||
4. [React Native](https://reactnative.dev/docs/environment-setup)
|
||||
3. `npm`
|
||||
4. [React Native environment setup](https://reactnative.dev/docs/set-up-your-environment)
|
||||
|
||||
To run the app locally, you will need to setup React Native on your system:
|
||||
To run the app locally, first complete React Native native tooling setup:
|
||||
|
||||
1. Open the official [environment setup guide here](https://reactnative.dev/docs/environment-setup)
|
||||
1. Open [React Native environment setup](https://reactnative.dev/docs/set-up-your-environment)
|
||||
2. Select `React Native CLI Quickstart`
|
||||
3. Select your OS & the platform to run the app on (iOS or Android)
|
||||
3. Select your OS and target platform(s): iOS and/or Android
|
||||
4. Follow the steps listed.
|
||||
|
||||
> Please keep in mind that **Expo is not supported**.
|
||||
> Expo is not used in this project.
|
||||
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
Clone the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
@@ -48,26 +46,27 @@ git clone https://github.com/streetwriters/notesnook.git
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
Install dependencies and bootstrap the mobile workspace:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
npm run bootstrap -- --scope=mobile
|
||||
```
|
||||
|
||||
### 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:
|
||||
[Set up an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) (or connect a physical device), then run:
|
||||
|
||||
```bash
|
||||
npm run start:android
|
||||
```
|
||||
|
||||
If you want to run the app on your phone, make sure to [enable USB debugging](https://developer.android.com/studio/debug/dev-options).
|
||||
If you are using a physical device, enable [USB debugging](https://developer.android.com/studio/debug/dev-options).
|
||||
|
||||
### Running the app on iOS
|
||||
|
||||
To run the app on iOS:
|
||||
Install CocoaPods dependencies first, then run the iOS app:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
@@ -76,90 +75,87 @@ npm run prepare:ios
|
||||
npm run start:ios
|
||||
```
|
||||
|
||||
### Useful development commands
|
||||
|
||||
```bash
|
||||
# start Metro only
|
||||
npm run start:metro
|
||||
|
||||
# start Re.Pack bundler
|
||||
npm run start:repack
|
||||
```
|
||||
|
||||
## 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!
|
||||
> The mobile app is a mixed TypeScript/JavaScript codebase.
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible:
|
||||
|
||||
1. React Native
|
||||
2. Typescript/Javascript
|
||||
3. Zustand: State management
|
||||
4. Detox: Runs all our e2e tests
|
||||
5. React Native MMKV: Database & persistence
|
||||
6. libsodium: Encryption
|
||||
1. React Native `0.82`
|
||||
2. React `19`
|
||||
3. TypeScript + JavaScript
|
||||
4. Zustand (state management)
|
||||
5. Detox (end-to-end testing)
|
||||
6. libsodium (encryption)
|
||||
|
||||
### Project structure
|
||||
|
||||
The app codebase is distributed over two primary directories. `native/` and `app/`.
|
||||
Top-level directories in `apps/mobile/`:
|
||||
|
||||
- `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/`: Main React Native app source (`components`, `common`, `hooks`, `navigation`, `screens`, `services`, `stores`, `utils`, etc.)
|
||||
- `android/`: Android native project
|
||||
- `ios/`: iOS native project
|
||||
- `e2e/`: Detox test suite and config
|
||||
- `patches/`: `patch-package` patches
|
||||
- `scripts/`: Mobile-specific scripts
|
||||
|
||||
- `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.
|
||||
- `screens`: Navigator screens.
|
||||
- `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.
|
||||
## Running E2E tests (Detox)
|
||||
|
||||
There are several other folders at the root:
|
||||
Detox device defaults in this repo:
|
||||
|
||||
- `share/`: Code for the iOS Share Extension and Android widget.
|
||||
- `e2e/`: Detox End to end tests
|
||||
- `patches/`: Patches for various react native dependencies.
|
||||
|
||||
### Running the tests
|
||||
|
||||
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 emulator: `Pixel_5_API_36`
|
||||
- iOS simulator: `iPhone 17 Pro Max`
|
||||
|
||||
### Android
|
||||
|
||||
To run the tests on Android, you will need to create an emulator device on your system:
|
||||
Build and run Android Detox tests:
|
||||
|
||||
```
|
||||
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm run build:android
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
|
||||
```
|
||||
npm run test:android
|
||||
```
|
||||
|
||||
For debug configuration:
|
||||
|
||||
```bash
|
||||
npm run build:android:debug
|
||||
npm run start:metro
|
||||
npm run test:android:debug
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
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):
|
||||
Build and run iOS Detox tests:
|
||||
|
||||
```bash
|
||||
npm run build:ios
|
||||
npm run test:ios
|
||||
```
|
||||
|
||||
If simulator tooling is missing, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
|
||||
|
||||
```bash
|
||||
brew tap wix/brew
|
||||
brew install applesimutils
|
||||
```
|
||||
|
||||
Now build the iOS app for testing:
|
||||
## Release commands
|
||||
|
||||
```
|
||||
npm run build:ios
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
Android release helpers:
|
||||
|
||||
```bash
|
||||
npm run release:android
|
||||
npm run release:android:bundle
|
||||
```
|
||||
npm run test:ios
|
||||
```
|
||||
|
||||
All tests on iOS are configured to run on `iPhone 8` simulator.
|
||||
|
||||
@@ -125,7 +125,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3091
|
||||
versionCode 3098
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -39,28 +39,32 @@ public class BootTaskService extends HeadlessJsTaskService {
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel("com.streetwriters.notesnook",
|
||||
"Default",
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel("com.streetwriters.notesnook",
|
||||
"Default",
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
|
||||
notification = new NotificationCompat.Builder(this, channel.getId())
|
||||
.setContentTitle("Sync on boot")
|
||||
.setContentText("")
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.build();
|
||||
if (android.os.Build.VERSION.SDK_INT >= 34) {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification);
|
||||
}
|
||||
|
||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
|
||||
notification = new NotificationCompat.Builder(this, channel.getId())
|
||||
.setContentTitle("Sync on boot")
|
||||
.setContentText("")
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.build();
|
||||
if (android.os.Build.VERSION.SDK_INT >= 34) {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification);
|
||||
}
|
||||
|
||||
} catch (Exception ignored) {
|
||||
stopSelf(startId);
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
|
||||
@@ -4,4 +4,10 @@
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
</domain-config>
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -1,13 +1,3 @@
|
||||
- Add notebooks, tags and colors to Home Screen Shortcuts
|
||||
- Change day format and use /day in notes
|
||||
- Add Setting to change default editor line height
|
||||
- Set a custom title for monographs
|
||||
- Add webpage title and date clipped to web clips
|
||||
- Configure Week to start from Sunday or Monday
|
||||
- Change Note's creation date
|
||||
- Set expiry date on notes
|
||||
- Temporarily disable password change and recovery options
|
||||
- Note history now includes note title
|
||||
- Minor bug fixes
|
||||
- Minor bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "Notesnook",
|
||||
"displayName": "Notesnook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +127,12 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
? useThemeStore.setState({
|
||||
darkTheme: theme
|
||||
})
|
||||
: useThemeStore.setState({
|
||||
lightTheme: theme
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -138,9 +142,9 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
useThemeStore
|
||||
.getState()
|
||||
.setColorScheme(colorScheme as "light" | "dark");
|
||||
useThemeStore.setState({
|
||||
colorScheme: colorScheme as "light" | "dark"
|
||||
});
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
|
||||
@@ -205,8 +205,9 @@ export async function getDatabaseKey(appLockPassword?: string) {
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials =
|
||||
await Keychain.getInternetCredentials("notesnook");
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook"
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher: Cipher = (await encrypt(
|
||||
|
||||
@@ -72,7 +72,7 @@ export async function setupDatabase(password?: string) {
|
||||
({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}) as ICompressor,
|
||||
} as ICompressor),
|
||||
batchSize: 50,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
|
||||
@@ -30,7 +30,8 @@ import {
|
||||
getCacheSize,
|
||||
hashBase64,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64
|
||||
writeEncryptedBase64,
|
||||
bulkDeleteFiles
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import {
|
||||
@@ -61,5 +62,6 @@ export const FileStorage: IFileStorage = {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
bulkExists
|
||||
bulkExists,
|
||||
bulkDeleteFiles
|
||||
};
|
||||
|
||||
@@ -30,7 +30,14 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import {
|
||||
ABYTES,
|
||||
cacheDir,
|
||||
cacheDirOld,
|
||||
getRandomId,
|
||||
isSuccessStatusCode,
|
||||
parseS3Error
|
||||
} from "./utils";
|
||||
|
||||
export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
filename: string,
|
||||
@@ -102,16 +109,41 @@ export async function writeEncryptedBase64(
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteLocalFile(filename: string) {
|
||||
try {
|
||||
await createCacheDir();
|
||||
let path = cacheDir + `/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
if (Platform.OS === "ios" && !exists) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
if (await RNFetchBlob.fs.exists(appGroupPath)) {
|
||||
RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (exists) {
|
||||
RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error, "deleteLocalFile");
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
await createCacheDir();
|
||||
const localFilePath = cacheDir + `/${filename}`;
|
||||
if (!requestOptions) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
deleteLocalFile(filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -122,9 +154,7 @@ export async function deleteFile(
|
||||
const status = response.info().status;
|
||||
const ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
deleteLocalFile(filename);
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
@@ -135,6 +165,49 @@ export async function deleteFile(
|
||||
}
|
||||
}
|
||||
|
||||
export async function bulkDeleteFiles(
|
||||
filenames: string[],
|
||||
requestOptions?: RequestOptions
|
||||
) {
|
||||
await createCacheDir();
|
||||
if (!requestOptions) {
|
||||
filenames.forEach((filename) => {
|
||||
deleteLocalFile(filename);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const { url, headers } = requestOptions;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...headers,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
names: filenames
|
||||
})
|
||||
});
|
||||
|
||||
const result = isSuccessStatusCode(response.status);
|
||||
if (result) {
|
||||
filenames.forEach((filename) => {
|
||||
deleteLocalFile(filename);
|
||||
});
|
||||
} else {
|
||||
throw await response.text();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(
|
||||
typeof e === "string" ? parseS3Error(e as string) : (e as Error),
|
||||
"Could not bulk delete files"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
} from "./utils";
|
||||
import Upload from "@ammarahmed/react-native-upload";
|
||||
import { CloudUploader } from "react-native-nitro-cloud-uploader";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { sleep } from "../../utils/time";
|
||||
|
||||
// Upload constants
|
||||
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
@@ -212,6 +214,9 @@ export async function uploadFile(
|
||||
);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
const status = await PermissionsAndroid.request(
|
||||
"android.permission.POST_NOTIFICATIONS"
|
||||
);
|
||||
@@ -221,6 +226,10 @@ export async function uploadFile(
|
||||
type: "info"
|
||||
});
|
||||
}
|
||||
await sleep(500);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}
|
||||
|
||||
let uploaded = false;
|
||||
|
||||
@@ -161,10 +161,10 @@ export async function checkUpload(
|
||||
size === 0
|
||||
? `File size is 0.`
|
||||
: size === -1
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
@@ -193,3 +193,7 @@ export async function checkAndCreateDir(path: string) {
|
||||
export const santizeUri = (uri: string) => {
|
||||
return Platform.OS === "ios" ? decodeURI(uri).replace("file:///", "/") : uri;
|
||||
};
|
||||
|
||||
export function isSuccessStatusCode(statusCode: number) {
|
||||
return statusCode >= 200 && statusCode <= 299;
|
||||
}
|
||||
|
||||
@@ -239,8 +239,8 @@ const AppLocked = () => {
|
||||
deviceMode !== "mobile"
|
||||
? "50%"
|
||||
: Platform.OS == "ios"
|
||||
? "95%"
|
||||
: "100%",
|
||||
? "95%"
|
||||
: "100%",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15,
|
||||
|
||||
@@ -59,6 +59,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Navigation from "../../services/navigation";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -304,9 +305,9 @@ const Actions = ({
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
await sleep(150);
|
||||
close?.();
|
||||
eSendEvent(eCloseAttachmentDialog);
|
||||
await sleep(300);
|
||||
Navigation.navigate("FluidPanelsView");
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
style={{
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, FlatList, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import create from "zustand";
|
||||
@@ -313,6 +313,10 @@ export const AttachmentDialog = ({
|
||||
});
|
||||
};
|
||||
|
||||
db.attachments.orphaned.items().then((r) => {
|
||||
console.log(r);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSheet ? (
|
||||
@@ -509,7 +513,7 @@ export const AttachmentDialog = ({
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<LegendList
|
||||
<FlatList
|
||||
renderScrollComponent={(props) => <ScrollView {...props} />}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
@@ -543,7 +547,6 @@ export const AttachmentDialog = ({
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={loading ? [] : attachments?.placeholders || []}
|
||||
extraData={attachments}
|
||||
renderItem={renderItem}
|
||||
|
||||
@@ -73,7 +73,15 @@ export const ChangePassword = () => {
|
||||
throw new Error(strings.backupFailed() + `: ${result.error}`);
|
||||
}
|
||||
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
const passwordChanged = await db.user.changePassword(
|
||||
oldPassword.current,
|
||||
password.current
|
||||
);
|
||||
|
||||
if (!passwordChanged) {
|
||||
throw new Error("Could not change user account password.");
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: strings.passwordChangedSuccessfully(),
|
||||
type: "success",
|
||||
|
||||
@@ -39,7 +39,7 @@ export function hideAuth(context?: AuthParams["context"]) {
|
||||
initialAuthMode.current === AuthMode.welcomeLogin ||
|
||||
context === "intro"
|
||||
) {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -35,9 +34,9 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const email = useRef<string>(undefined);
|
||||
const email = useRef<string>(userEmail);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -87,94 +86,76 @@ export const ForgotPassword = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionSheet
|
||||
onBeforeShow={(data) => (email.current = data)}
|
||||
onClose={() => {
|
||||
setSent(false);
|
||||
setLoading(false);
|
||||
}}
|
||||
onOpen={() => {
|
||||
emailInputRef.current?.setNativeProps({
|
||||
text: email.current
|
||||
});
|
||||
}}
|
||||
indicatorStyle={{
|
||||
width: 100
|
||||
}}
|
||||
gestureEnabled
|
||||
id="forgotpassword_sheet"
|
||||
>
|
||||
{sent ? (
|
||||
<View
|
||||
{sent ? (
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 50
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 50
|
||||
width: null,
|
||||
height: null
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
name="email"
|
||||
size={50}
|
||||
/>
|
||||
<Heading>{strings.recoveryEmailSent()}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
width: null,
|
||||
height: null
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
name="email"
|
||||
size={50}
|
||||
/>
|
||||
<Heading>{strings.recoveryEmailSent()}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{strings.recoveryEmailSentDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
{strings.recoveryEmailSentDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
>
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
<Seperator />
|
||||
defaultValue={email.current}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={sendRecoveryEmail}
|
||||
type="accent"
|
||||
title={loading ? null : strings.next()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ActionSheet>
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={sendRecoveryEmail}
|
||||
type="accent"
|
||||
title={loading ? null : strings.next()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,11 @@ import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -110,7 +114,6 @@ export const Login = ({
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<ForgotPassword />
|
||||
<Dialog context="two_factor_verify" />
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
@@ -192,8 +195,8 @@ export const Login = ({
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
|
||||
@@ -257,13 +260,10 @@ export const Login = ({
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
message:
|
||||
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
|
||||
if (loading || !email.current) return;
|
||||
presentSheet({
|
||||
component: <ForgotPassword userEmail={email.current} />
|
||||
});
|
||||
// if (loading || !email.current) return;
|
||||
// SheetManager.show("forgotpassword_sheet");
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
|
||||
@@ -97,7 +97,7 @@ export const SessionExpired = () => {
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
|
||||
const key = await db.user.getEncryptionKey();
|
||||
const key = await db.user.getDataEncryptionKeys();
|
||||
if (!key) throw new Error("No encryption key found.");
|
||||
|
||||
Sync.run("global", false, "full", async (complete) => {
|
||||
|
||||
@@ -161,10 +161,10 @@ const FloatingButton = ({
|
||||
icon
|
||||
? icon
|
||||
: route.name === "Notebooks"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
}
|
||||
color={color || colors.primary.accent}
|
||||
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}
|
||||
|
||||
@@ -136,8 +136,8 @@ const BaseDialog = ({
|
||||
backgroundColor: background
|
||||
? background
|
||||
: transparent
|
||||
? "transparent"
|
||||
: "rgba(0,0,0,0.3)"
|
||||
? "transparent"
|
||||
: "rgba(0,0,0,0.3)"
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
@@ -155,8 +155,8 @@ const BaseDialog = ({
|
||||
justifyContent: centered
|
||||
? "center"
|
||||
: bottom
|
||||
? "flex-end"
|
||||
: "flex-start"
|
||||
? "flex-end"
|
||||
: "flex-start"
|
||||
}
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -201,14 +201,12 @@ export const AppLockPassword = () => {
|
||||
accountPass
|
||||
? strings.enterAccountPassword()
|
||||
: mode === "change"
|
||||
? keyboardType === "pin"
|
||||
? strings.newPin()
|
||||
: strings.newPassword()
|
||||
: `${
|
||||
keyboardType === "pin"
|
||||
? strings.pin()
|
||||
: strings.password()
|
||||
}`
|
||||
? keyboardType === "pin"
|
||||
? strings.newPin()
|
||||
: strings.newPassword()
|
||||
: `${
|
||||
keyboardType === "pin" ? strings.pin() : strings.password()
|
||||
}`
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -385,8 +383,8 @@ export const AppLockPassword = () => {
|
||||
mode === "remove"
|
||||
? strings.remove()
|
||||
: mode === "change"
|
||||
? strings.change()
|
||||
: strings.save()
|
||||
? strings.change()
|
||||
: strings.save()
|
||||
}
|
||||
negativeTitle={strings.cancel()}
|
||||
positiveType="transparent"
|
||||
|
||||
@@ -28,6 +28,7 @@ import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
Vault,
|
||||
VaultRequestType,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
@@ -55,151 +56,119 @@ import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { Note, NoteContent, VAULT_ERRORS } from "@notesnook/core";
|
||||
import {
|
||||
isEncryptedContent,
|
||||
Note,
|
||||
NoteContent,
|
||||
VAULT_ERRORS
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
type VaultDialogData = {
|
||||
item: Note;
|
||||
} & Partial<Omit<Vault, "item">>;
|
||||
|
||||
interface VaultDialogState {
|
||||
visible: boolean;
|
||||
wrongPassword: boolean;
|
||||
loading: boolean;
|
||||
note?: Note;
|
||||
vault: boolean;
|
||||
locked: boolean;
|
||||
permanant: boolean;
|
||||
goToEditor: boolean;
|
||||
share: boolean;
|
||||
passwordsDontMatch: boolean;
|
||||
deleteNote: boolean;
|
||||
focusIndex: number | null;
|
||||
biometricUnlock: boolean;
|
||||
isBiometryEnrolled: boolean;
|
||||
isBiometryAvailable: boolean;
|
||||
fingerprintAccess: boolean;
|
||||
changePassword: boolean;
|
||||
copyNote: boolean;
|
||||
revokeFingerprintAccess: boolean;
|
||||
title: string;
|
||||
description: string | null;
|
||||
clearVault: boolean;
|
||||
deleteVault: boolean;
|
||||
deleteAll: boolean;
|
||||
noteLocked: boolean;
|
||||
novault: boolean;
|
||||
customActionTitle: string | null;
|
||||
customActionParagraph: string | null;
|
||||
customAction: boolean;
|
||||
onUnlock?: (
|
||||
item: Note & {
|
||||
content?: NoteContent<false>;
|
||||
},
|
||||
password: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const VaultDialog: React.FC = () => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const [state, setState] = useState<VaultDialogState>({
|
||||
visible: false,
|
||||
wrongPassword: false,
|
||||
loading: false,
|
||||
note: undefined,
|
||||
vault: false,
|
||||
locked: true,
|
||||
permanant: false,
|
||||
goToEditor: false,
|
||||
share: false,
|
||||
passwordsDontMatch: false,
|
||||
deleteNote: false,
|
||||
focusIndex: null,
|
||||
biometricUnlock: false,
|
||||
isBiometryEnrolled: false,
|
||||
isBiometryAvailable: false,
|
||||
fingerprintAccess: false,
|
||||
changePassword: false,
|
||||
copyNote: false,
|
||||
revokeFingerprintAccess: false,
|
||||
title: strings.goToEditor(),
|
||||
description: null,
|
||||
clearVault: false,
|
||||
deleteVault: false,
|
||||
deleteAll: false,
|
||||
noteLocked: false,
|
||||
novault: false,
|
||||
customActionTitle: null,
|
||||
customActionParagraph: null,
|
||||
customAction: false,
|
||||
onUnlock: undefined
|
||||
});
|
||||
// UI State
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [wrongPassword, setWrongPassword] = useState(false);
|
||||
const [passwordsDontMatch, setPasswordsDontMatch] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
const [biometricUnlock, setBiometricUnlock] = useState(false);
|
||||
const [isBiometryAvailable, setIsBiometryAvailable] = useState(false);
|
||||
const [isBiometryEnrolled, setIsBiometryEnrolled] = useState(false);
|
||||
|
||||
// Refs for non-UI state
|
||||
const requestTypeRef = useRef<VaultRequestType | null>(null);
|
||||
const noteRef = useRef<Note | undefined>(undefined);
|
||||
const titleRef = useRef<string>(strings.goToEditor());
|
||||
const descriptionRef = useRef<string | null>(null);
|
||||
const paragraphRef = useRef<string | null>(null);
|
||||
const buttonTitleRef = useRef<string | null>(null);
|
||||
const positiveButtonTypeRef = useRef<"errorShade" | "transparent" | "accent">(
|
||||
"transparent"
|
||||
);
|
||||
const customActionTitleRef = useRef<string | null>(null);
|
||||
const customActionParagraphRef = useRef<string | null>(null);
|
||||
const noteLockedRef = useRef(false);
|
||||
const onUnlockRef = useRef<
|
||||
| ((
|
||||
item: Note & {
|
||||
content?: NoteContent<false>;
|
||||
},
|
||||
password: string
|
||||
) => void)
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
// Input refs
|
||||
const passInputRef = useRef<TextInput>(null);
|
||||
const confirmPassRef = useRef<TextInput>(null);
|
||||
const changePassInputRef = useRef<TextInput>(null);
|
||||
|
||||
// Password refs
|
||||
const passwordRef = useRef<string | null>(null);
|
||||
const confirmPasswordRef = useRef<string | null>(null);
|
||||
const newPasswordRef = useRef<string | null>(null);
|
||||
|
||||
const open = useCallback(async (data: VaultDialogData) => {
|
||||
const open = useCallback(async (data: Vault) => {
|
||||
const biometry = await BiometricService.isBiometryAvailable();
|
||||
const available = !!biometry;
|
||||
const fingerprint = await BiometricService.hasInternetCredentials();
|
||||
|
||||
const noteLocked = data.item
|
||||
? await db.vaults.itemExists(data.item)
|
||||
: false;
|
||||
if (data.item) {
|
||||
const locked = await db.vaults.itemExists(data.item);
|
||||
noteLockedRef.current = locked;
|
||||
if (!locked) {
|
||||
const content = await db.content.findByNoteId(data.item!.id);
|
||||
if (content && isEncryptedContent(content)) {
|
||||
noteLockedRef.current = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
note: data.item,
|
||||
novault: data.novault || false,
|
||||
locked: data.locked || false,
|
||||
permanant: data.permanant || false,
|
||||
goToEditor: data.goToEditor || false,
|
||||
share: data.share || false,
|
||||
deleteNote: data.deleteNote || false,
|
||||
copyNote: data.copyNote || false,
|
||||
isBiometryAvailable: available,
|
||||
biometricUnlock: fingerprint,
|
||||
isBiometryEnrolled: fingerprint,
|
||||
fingerprintAccess: data.fingerprintAccess || false,
|
||||
changePassword: data.changePassword || false,
|
||||
revokeFingerprintAccess: data.revokeFingerprintAccess || false,
|
||||
title: data.title || strings.goToEditor(),
|
||||
description: data.description || null,
|
||||
clearVault: data.clearVault || false,
|
||||
deleteVault: data.deleteVault || false,
|
||||
noteLocked,
|
||||
customActionTitle: data.customActionTitle || null,
|
||||
customActionParagraph: data.customActionParagraph || null,
|
||||
customAction: !!(data.customActionTitle && data.customActionParagraph),
|
||||
onUnlock: data.onUnlock
|
||||
}));
|
||||
// Set refs
|
||||
noteRef.current = data.item;
|
||||
titleRef.current = data.title || strings.goToEditor();
|
||||
descriptionRef.current = data.description || null;
|
||||
paragraphRef.current = data.paragraph || null;
|
||||
buttonTitleRef.current = data.buttonTitle || null;
|
||||
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
|
||||
customActionTitleRef.current = data.customActionTitle || null;
|
||||
customActionParagraphRef.current = data.customActionParagraph || null;
|
||||
onUnlockRef.current = data.onUnlock;
|
||||
requestTypeRef.current = data.requestType;
|
||||
|
||||
if (
|
||||
// Set UI state
|
||||
setIsBiometryAvailable(available);
|
||||
setIsBiometryEnrolled(fingerprint);
|
||||
setBiometricUnlock(fingerprint);
|
||||
setWrongPassword(false);
|
||||
setPasswordsDontMatch(false);
|
||||
setDeleteAll(false);
|
||||
setLoading(false);
|
||||
|
||||
// Auto-unlock with fingerprint if applicable
|
||||
const canAutoUnlock =
|
||||
fingerprint &&
|
||||
data.novault &&
|
||||
!data.fingerprintAccess &&
|
||||
!data.revokeFingerprintAccess &&
|
||||
!data.changePassword &&
|
||||
!data.clearVault &&
|
||||
!data.deleteVault &&
|
||||
!data.customActionTitle
|
||||
) {
|
||||
data.requestType !== VaultRequestType.EnableFingerprint &&
|
||||
data.requestType !== VaultRequestType.RevokeFingerprint &&
|
||||
data.requestType !== VaultRequestType.ChangePassword &&
|
||||
data.requestType !== VaultRequestType.ClearVault &&
|
||||
data.requestType !== VaultRequestType.DeleteVault &&
|
||||
data.requestType !== VaultRequestType.CustomAction &&
|
||||
data.requestType !== VaultRequestType.PermanentUnlock;
|
||||
|
||||
if (canAutoUnlock) {
|
||||
await onPressFingerprintAuth(data.title, data.description);
|
||||
} else {
|
||||
setState((prev) => ({ ...prev, visible: true }));
|
||||
setVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (state.loading) {
|
||||
if (loading) {
|
||||
ToastManager.show({
|
||||
heading: state.title,
|
||||
heading: titleRef.current,
|
||||
message: strings.pleaseWait() + "...",
|
||||
type: "success",
|
||||
context: "local"
|
||||
@@ -209,46 +178,37 @@ export const VaultDialog: React.FC = () => {
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
// Reset password refs
|
||||
passwordRef.current = null;
|
||||
confirmPasswordRef.current = null;
|
||||
newPasswordRef.current = null;
|
||||
|
||||
setState({
|
||||
visible: false,
|
||||
wrongPassword: false,
|
||||
loading: false,
|
||||
note: undefined,
|
||||
vault: false,
|
||||
locked: false,
|
||||
permanant: false,
|
||||
goToEditor: false,
|
||||
share: false,
|
||||
passwordsDontMatch: false,
|
||||
deleteNote: false,
|
||||
focusIndex: null,
|
||||
biometricUnlock: false,
|
||||
isBiometryEnrolled: false,
|
||||
isBiometryAvailable: false,
|
||||
fingerprintAccess: false,
|
||||
changePassword: false,
|
||||
copyNote: false,
|
||||
revokeFingerprintAccess: false,
|
||||
title: strings.goToEditor(),
|
||||
description: null,
|
||||
clearVault: false,
|
||||
deleteVault: false,
|
||||
deleteAll: false,
|
||||
noteLocked: false,
|
||||
novault: false,
|
||||
customActionTitle: null,
|
||||
customActionParagraph: null,
|
||||
customAction: false,
|
||||
onUnlock: undefined
|
||||
});
|
||||
}, [state.loading, state.title]);
|
||||
// Reset refs
|
||||
requestTypeRef.current = null;
|
||||
noteRef.current = undefined;
|
||||
titleRef.current = strings.goToEditor();
|
||||
descriptionRef.current = null;
|
||||
paragraphRef.current = null;
|
||||
buttonTitleRef.current = null;
|
||||
positiveButtonTypeRef.current = "transparent";
|
||||
customActionTitleRef.current = null;
|
||||
customActionParagraphRef.current = null;
|
||||
noteLockedRef.current = false;
|
||||
onUnlockRef.current = undefined;
|
||||
|
||||
// Reset UI state
|
||||
setVisible(false);
|
||||
setLoading(false);
|
||||
setWrongPassword(false);
|
||||
setPasswordsDontMatch(false);
|
||||
setDeleteAll(false);
|
||||
setBiometricUnlock(false);
|
||||
setIsBiometryAvailable(false);
|
||||
setIsBiometryEnrolled(false);
|
||||
}, [loading]);
|
||||
|
||||
const deleteVault = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, loading: true }));
|
||||
setLoading(true);
|
||||
try {
|
||||
let verified = true;
|
||||
if (await db.user.getUser()) {
|
||||
@@ -256,7 +216,7 @@ export const VaultDialog: React.FC = () => {
|
||||
}
|
||||
if (verified) {
|
||||
let noteIds: string[] = [];
|
||||
if (state.deleteAll) {
|
||||
if (deleteAll) {
|
||||
const vault = await db.vaults.default();
|
||||
const relations = await db.relations
|
||||
.from(
|
||||
@@ -269,9 +229,9 @@ export const VaultDialog: React.FC = () => {
|
||||
.get();
|
||||
noteIds = relations.map((item) => item.toId);
|
||||
}
|
||||
await db.vault.delete(state.deleteAll);
|
||||
await db.vault.delete(deleteAll);
|
||||
|
||||
if (state.deleteAll) {
|
||||
if (deleteAll) {
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
@@ -284,7 +244,7 @@ export const VaultDialog: React.FC = () => {
|
||||
});
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
close();
|
||||
}, 100);
|
||||
@@ -298,10 +258,10 @@ export const VaultDialog: React.FC = () => {
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, [state.deleteAll, close]);
|
||||
}, [deleteAll, close]);
|
||||
|
||||
const clearVault = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, loading: true }));
|
||||
setLoading(true);
|
||||
try {
|
||||
const vault = await db.vaults.default();
|
||||
const relations = await db.relations.from(vault!, "note").get();
|
||||
@@ -319,7 +279,7 @@ export const VaultDialog: React.FC = () => {
|
||||
true
|
||||
);
|
||||
});
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
setLoading(false);
|
||||
close();
|
||||
eSendEvent("vaultUpdated");
|
||||
} catch (e) {
|
||||
@@ -329,7 +289,7 @@ export const VaultDialog: React.FC = () => {
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
setLoading(false);
|
||||
}, [close]);
|
||||
|
||||
const lockNote = useCallback(async () => {
|
||||
@@ -341,9 +301,9 @@ export const VaultDialog: React.FC = () => {
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
await db.vault.add(state.note!.id);
|
||||
await db.vault.add(noteRef.current!.id);
|
||||
|
||||
eSendEvent(eUpdateNoteInEditor, state.note, true);
|
||||
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
|
||||
|
||||
close();
|
||||
ToastManager.show({
|
||||
@@ -351,26 +311,29 @@ export const VaultDialog: React.FC = () => {
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
setLoading(false);
|
||||
}
|
||||
}, [state.note, close]);
|
||||
}, [close]);
|
||||
|
||||
const permanantUnlock = useCallback(() => {
|
||||
db.vault
|
||||
.remove(state.note!.id, passwordRef.current || "")
|
||||
.then(() => {
|
||||
.remove(noteRef.current!.id, passwordRef.current || "")
|
||||
.then(async () => {
|
||||
ToastManager.show({
|
||||
heading: strings.noteUnlocked(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
eSendEvent(eUpdateNoteInEditor, state.note, true);
|
||||
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
|
||||
if (biometricUnlock && !isBiometryEnrolled) {
|
||||
await enrollFingerprint(passwordRef.current || "");
|
||||
}
|
||||
close();
|
||||
})
|
||||
.catch((e) => {
|
||||
takeErrorAction();
|
||||
});
|
||||
}, [state.note, close]);
|
||||
}, [close, biometricUnlock, isBiometryEnrolled]);
|
||||
|
||||
const openInEditor = useCallback(
|
||||
(note: Note & { content?: NoteContent<false> }) => {
|
||||
@@ -418,20 +381,17 @@ export const VaultDialog: React.FC = () => {
|
||||
|
||||
const deleteNote = useCallback(async () => {
|
||||
try {
|
||||
await db.vault.remove(state.note!.id, passwordRef.current || "");
|
||||
await deleteItems("note", [state.note!.id]);
|
||||
await db.vault.remove(noteRef.current!.id, passwordRef.current || "");
|
||||
await deleteItems("note", [noteRef.current!.id]);
|
||||
close();
|
||||
} catch (e) {
|
||||
takeErrorAction();
|
||||
}
|
||||
}, [state.note, close]);
|
||||
}, [close]);
|
||||
|
||||
const takeErrorAction = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
wrongPassword: true,
|
||||
visible: true
|
||||
}));
|
||||
setWrongPassword(true);
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
@@ -445,39 +405,41 @@ export const VaultDialog: React.FC = () => {
|
||||
try {
|
||||
if (!passwordRef.current) throw new Error("Invalid password");
|
||||
|
||||
const note = await db.vault.open(state.note!.id, passwordRef.current);
|
||||
const note = await db.vault.open(
|
||||
noteRef.current!.id,
|
||||
passwordRef.current
|
||||
);
|
||||
if (!note) throw new Error("Failed to unlock note.");
|
||||
if (state.biometricUnlock && !state.isBiometryEnrolled) {
|
||||
if (biometricUnlock && !isBiometryEnrolled) {
|
||||
await enrollFingerprint(passwordRef.current || "");
|
||||
}
|
||||
|
||||
if (state.goToEditor) {
|
||||
const requestType = requestTypeRef.current;
|
||||
|
||||
if (requestType === VaultRequestType.GoToEditor) {
|
||||
openInEditor(note);
|
||||
} else if (state.share) {
|
||||
} else if (requestType === VaultRequestType.ShareNote) {
|
||||
await shareNote(note);
|
||||
} else if (state.deleteNote) {
|
||||
} else if (requestType === VaultRequestType.DeleteNote) {
|
||||
await deleteNote();
|
||||
} else if (state.copyNote) {
|
||||
} else if (requestType === VaultRequestType.CopyNote) {
|
||||
await copyNote(note);
|
||||
} else if (state.customAction && state.onUnlock) {
|
||||
} else if (
|
||||
requestType === VaultRequestType.CustomAction &&
|
||||
onUnlockRef.current
|
||||
) {
|
||||
const password = passwordRef.current;
|
||||
const unlock = onUnlockRef.current;
|
||||
close();
|
||||
await sleep(300);
|
||||
state.onUnlock(note, password);
|
||||
await sleep(500);
|
||||
unlock(note, password);
|
||||
}
|
||||
} catch (e) {
|
||||
takeErrorAction();
|
||||
}
|
||||
}, [
|
||||
state.note,
|
||||
state.biometricUnlock,
|
||||
state.isBiometryEnrolled,
|
||||
state.goToEditor,
|
||||
state.share,
|
||||
state.deleteNote,
|
||||
state.copyNote,
|
||||
state.customAction,
|
||||
state.onUnlock,
|
||||
biometricUnlock,
|
||||
isBiometryEnrolled,
|
||||
openInEditor,
|
||||
shareNote,
|
||||
deleteNote,
|
||||
@@ -495,20 +457,20 @@ export const VaultDialog: React.FC = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (state.permanant) {
|
||||
if (requestTypeRef.current === VaultRequestType.PermanentUnlock) {
|
||||
permanantUnlock();
|
||||
} else {
|
||||
await openNote();
|
||||
}
|
||||
}, [state.permanant, permanantUnlock, openNote]);
|
||||
}, [permanantUnlock, openNote]);
|
||||
|
||||
const enrollFingerprint = useCallback(
|
||||
async (password: string) => {
|
||||
setState((prev) => ({ ...prev, loading: true }));
|
||||
setLoading(true);
|
||||
try {
|
||||
await db.vault.unlock(password);
|
||||
await BiometricService.storeCredentials(password);
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
setLoading(false);
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: strings.biometricUnlockEnabled(),
|
||||
@@ -523,7 +485,7 @@ export const VaultDialog: React.FC = () => {
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[close]
|
||||
@@ -532,13 +494,13 @@ export const VaultDialog: React.FC = () => {
|
||||
const createVault = useCallback(async () => {
|
||||
await db.vault.create(passwordRef.current || "");
|
||||
|
||||
if (state.biometricUnlock) {
|
||||
if (biometricUnlock) {
|
||||
await enrollFingerprint(passwordRef.current || "");
|
||||
}
|
||||
if (state.note?.id) {
|
||||
await db.vault.add(state.note.id);
|
||||
eSendEvent(eUpdateNoteInEditor, state.note, true);
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
if (noteRef.current?.id) {
|
||||
await db.vault.add(noteRef.current.id);
|
||||
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.noteLocked(),
|
||||
type: "success",
|
||||
@@ -554,7 +516,7 @@ export const VaultDialog: React.FC = () => {
|
||||
close();
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
}, [state.biometricUnlock, state.note, enrollFingerprint, close]);
|
||||
}, [biometricUnlock, enrollFingerprint, close]);
|
||||
|
||||
const revokeFingerprintAccess = useCallback(async () => {
|
||||
try {
|
||||
@@ -578,8 +540,8 @@ export const VaultDialog: React.FC = () => {
|
||||
async (title?: string, description?: string) => {
|
||||
try {
|
||||
const credentials = await BiometricService.getCredentials(
|
||||
title || state.title,
|
||||
description || state.description || ""
|
||||
title || titleRef.current,
|
||||
description || descriptionRef.current || ""
|
||||
);
|
||||
|
||||
if (!credentials) throw new Error("Failed to get user credentials");
|
||||
@@ -590,22 +552,25 @@ export const VaultDialog: React.FC = () => {
|
||||
} else {
|
||||
eSendEvent(eCloseActionSheet);
|
||||
await sleep(300);
|
||||
setState((prev) => ({ ...prev, visible: true }));
|
||||
setVisible(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
[state.title, state.description]
|
||||
[]
|
||||
);
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
if (state.revokeFingerprintAccess) {
|
||||
const requestType = requestTypeRef.current;
|
||||
|
||||
if (requestType === VaultRequestType.RevokeFingerprint) {
|
||||
await revokeFingerprintAccess();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (state.loading) return;
|
||||
|
||||
if (loading) return;
|
||||
|
||||
if (!passwordRef.current) {
|
||||
ToastManager.show({
|
||||
@@ -616,26 +581,26 @@ export const VaultDialog: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.novault) {
|
||||
if (requestType === VaultRequestType.CreateVault) {
|
||||
if (passwordRef.current !== confirmPasswordRef.current) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordNotMatched(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setState((prev) => ({ ...prev, passwordsDontMatch: true }));
|
||||
setPasswordsDontMatch(true);
|
||||
return;
|
||||
}
|
||||
|
||||
createVault();
|
||||
} else if (state.changePassword) {
|
||||
setState((prev) => ({ ...prev, loading: true }));
|
||||
} else if (requestType === VaultRequestType.ChangePassword) {
|
||||
setLoading(true);
|
||||
|
||||
db.vault
|
||||
.changePassword(passwordRef.current, newPasswordRef.current || "")
|
||||
.then(() => {
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
if (state.biometricUnlock) {
|
||||
setLoading(false);
|
||||
if (biometricUnlock) {
|
||||
enrollFingerprint(newPasswordRef.current || "");
|
||||
}
|
||||
ToastManager.show({
|
||||
@@ -646,7 +611,7 @@ export const VaultDialog: React.FC = () => {
|
||||
close();
|
||||
})
|
||||
.catch((e) => {
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
setLoading(false);
|
||||
if (e.message === VAULT_ERRORS.wrongPassword) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
@@ -657,50 +622,62 @@ export const VaultDialog: React.FC = () => {
|
||||
ToastManager.error(e);
|
||||
}
|
||||
});
|
||||
} else if (state.locked) {
|
||||
} else if (requestType === VaultRequestType.LockNote) {
|
||||
if (!passwordRef.current || passwordRef.current.trim() === "") {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setState((prev) => ({ ...prev, wrongPassword: true }));
|
||||
setWrongPassword(true);
|
||||
return;
|
||||
}
|
||||
if (state.noteLocked) {
|
||||
db.vault
|
||||
.unlock(passwordRef.current)
|
||||
.then(async (unlocked) => {
|
||||
if (unlocked) {
|
||||
setWrongPassword(false);
|
||||
await lockNote();
|
||||
} else {
|
||||
takeErrorAction();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
takeErrorAction();
|
||||
});
|
||||
} else if (
|
||||
requestType === VaultRequestType.UnlockNote ||
|
||||
requestType === VaultRequestType.PermanentUnlock ||
|
||||
requestType === VaultRequestType.GoToEditor ||
|
||||
requestType === VaultRequestType.ShareNote ||
|
||||
requestType === VaultRequestType.CopyNote ||
|
||||
requestType === VaultRequestType.DeleteNote ||
|
||||
requestType === VaultRequestType.CustomAction
|
||||
) {
|
||||
if (!passwordRef.current || passwordRef.current.trim() === "") {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setWrongPassword(true);
|
||||
return;
|
||||
}
|
||||
if (noteLockedRef.current) {
|
||||
await unlockNote();
|
||||
} else {
|
||||
db.vault
|
||||
.unlock(passwordRef.current)
|
||||
.then(async () => {
|
||||
setState((prev) => ({ ...prev, wrongPassword: false }));
|
||||
await lockNote();
|
||||
})
|
||||
.catch((e) => {
|
||||
takeErrorAction();
|
||||
});
|
||||
console.log("Error: Note should be locked for this operation");
|
||||
}
|
||||
} else if (state.fingerprintAccess) {
|
||||
} else if (requestType === VaultRequestType.EnableFingerprint) {
|
||||
enrollFingerprint(passwordRef.current);
|
||||
} else if (state.clearVault) {
|
||||
} else if (requestType === VaultRequestType.ClearVault) {
|
||||
await clearVault();
|
||||
} else if (state.deleteVault) {
|
||||
} else if (requestType === VaultRequestType.DeleteVault) {
|
||||
await deleteVault();
|
||||
} else if (state.customAction) {
|
||||
await unlockNote();
|
||||
}
|
||||
}, [
|
||||
state.revokeFingerprintAccess,
|
||||
state.loading,
|
||||
state.novault,
|
||||
state.changePassword,
|
||||
state.locked,
|
||||
state.noteLocked,
|
||||
state.fingerprintAccess,
|
||||
state.clearVault,
|
||||
state.deleteVault,
|
||||
state.customAction,
|
||||
state.biometricUnlock,
|
||||
loading,
|
||||
biometricUnlock,
|
||||
revokeFingerprintAccess,
|
||||
close,
|
||||
createVault,
|
||||
@@ -722,23 +699,21 @@ export const VaultDialog: React.FC = () => {
|
||||
};
|
||||
}, [open, close]);
|
||||
|
||||
if (!state.visible) return null;
|
||||
if (!visible) return null;
|
||||
|
||||
const {
|
||||
note,
|
||||
novault,
|
||||
deleteNote: shouldDeleteNote,
|
||||
share,
|
||||
goToEditor,
|
||||
fingerprintAccess,
|
||||
changePassword,
|
||||
loading,
|
||||
deleteVault: shouldDeleteVault,
|
||||
clearVault: shouldClearVault,
|
||||
customAction,
|
||||
customActionTitle,
|
||||
customActionParagraph
|
||||
} = state;
|
||||
const requestType = requestTypeRef.current;
|
||||
const isCreateVault = requestType === VaultRequestType.CreateVault;
|
||||
const isChangePassword = requestType === VaultRequestType.ChangePassword;
|
||||
const isClearVault = requestType === VaultRequestType.ClearVault;
|
||||
const isDeleteVault = requestType === VaultRequestType.DeleteVault;
|
||||
const isRevokeFingerprint =
|
||||
requestType === VaultRequestType.RevokeFingerprint;
|
||||
const isEnableFingerprint =
|
||||
requestType === VaultRequestType.EnableFingerprint;
|
||||
const isCustomAction = requestType === VaultRequestType.CustomAction;
|
||||
const isDeleteNote = requestType === VaultRequestType.DeleteNote;
|
||||
const isShareNote = requestType === VaultRequestType.ShareNote;
|
||||
const isGoToEditor = requestType === VaultRequestType.GoToEditor;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
@@ -760,8 +735,10 @@ export const VaultDialog: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={state.title}
|
||||
paragraph={customActionParagraph || ""}
|
||||
title={titleRef.current}
|
||||
paragraph={
|
||||
paragraphRef.current || customActionParagraphRef.current || ""
|
||||
}
|
||||
icon="shield"
|
||||
padding={12}
|
||||
/>
|
||||
@@ -772,12 +749,12 @@ export const VaultDialog: React.FC = () => {
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{(novault ||
|
||||
changePassword ||
|
||||
shouldClearVault ||
|
||||
shouldDeleteVault ||
|
||||
customAction) &&
|
||||
!state.revokeFingerprintAccess ? (
|
||||
{(isChangePassword ||
|
||||
isClearVault ||
|
||||
!isCreateVault ||
|
||||
isDeleteVault ||
|
||||
isCustomAction) &&
|
||||
!isRevokeFingerprint ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passInputRef}
|
||||
@@ -788,37 +765,40 @@ export const VaultDialog: React.FC = () => {
|
||||
passwordRef.current = value;
|
||||
}}
|
||||
marginBottom={
|
||||
!state.biometricUnlock ||
|
||||
!state.isBiometryEnrolled ||
|
||||
!novault ||
|
||||
changePassword ||
|
||||
customAction
|
||||
!biometricUnlock ||
|
||||
!isBiometryEnrolled ||
|
||||
isCreateVault ||
|
||||
isChangePassword ||
|
||||
isCustomAction
|
||||
? 0
|
||||
: 10
|
||||
}
|
||||
onSubmit={() => {
|
||||
if (changePassword) {
|
||||
if (isChangePassword) {
|
||||
confirmPassRef.current?.focus();
|
||||
} else {
|
||||
onPress();
|
||||
}
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={changePassword ? strings.next() : state.title}
|
||||
returnKeyType={changePassword ? "next" : "done"}
|
||||
returnKeyLabel={
|
||||
isChangePassword ? strings.next() : titleRef.current
|
||||
}
|
||||
returnKeyType={isChangePassword ? "next" : "done"}
|
||||
secureTextEntry
|
||||
placeholder={
|
||||
changePassword
|
||||
isChangePassword
|
||||
? strings.currentPassword()
|
||||
: strings.password()
|
||||
}
|
||||
/>
|
||||
|
||||
{!state.biometricUnlock ||
|
||||
!state.isBiometryEnrolled ||
|
||||
!novault ||
|
||||
changePassword ||
|
||||
customAction ? null : (
|
||||
{!biometricUnlock ||
|
||||
!isBiometryEnrolled ||
|
||||
!isBiometryAvailable ||
|
||||
isCreateVault ||
|
||||
isChangePassword ||
|
||||
isCustomAction ? null : (
|
||||
<Button
|
||||
onPress={() =>
|
||||
onPressFingerprintAuth(strings.unlockNote(), "")
|
||||
@@ -832,16 +812,11 @@ export const VaultDialog: React.FC = () => {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{shouldDeleteVault && (
|
||||
{isDeleteVault && (
|
||||
<Button
|
||||
onPress={() =>
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
deleteAll: !prev.deleteAll
|
||||
}))
|
||||
}
|
||||
onPress={() => setDeleteAll(!deleteAll)}
|
||||
icon={
|
||||
state.deleteAll
|
||||
deleteAll
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
@@ -854,7 +829,7 @@ export const VaultDialog: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{changePassword ? (
|
||||
{isChangePassword ? (
|
||||
<>
|
||||
<Seperator half />
|
||||
<Input
|
||||
@@ -877,7 +852,7 @@ export const VaultDialog: React.FC = () => {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!novault ? (
|
||||
{isCreateVault ? (
|
||||
<View>
|
||||
<Input
|
||||
fwdRef={passInputRef}
|
||||
@@ -912,12 +887,9 @@ export const VaultDialog: React.FC = () => {
|
||||
onChangeText={(value) => {
|
||||
confirmPasswordRef.current = value;
|
||||
if (value !== passwordRef.current) {
|
||||
setState((prev) => ({ ...prev, passwordsDontMatch: true }));
|
||||
setPasswordsDontMatch(true);
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
passwordsDontMatch: false
|
||||
}));
|
||||
setPasswordsDontMatch(false);
|
||||
}
|
||||
}}
|
||||
onSubmit={() => {
|
||||
@@ -928,22 +900,23 @@ export const VaultDialog: React.FC = () => {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{state.biometricUnlock && !state.isBiometryEnrolled && novault ? (
|
||||
{biometricUnlock && !isBiometryEnrolled && !isCreateVault ? (
|
||||
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{state.isBiometryAvailable &&
|
||||
!state.fingerprintAccess &&
|
||||
!shouldClearVault &&
|
||||
!shouldDeleteVault &&
|
||||
!customAction &&
|
||||
((!state.biometricUnlock && !changePassword) || !novault) ? (
|
||||
{!biometricUnlock &&
|
||||
!isBiometryEnrolled &&
|
||||
isBiometryAvailable &&
|
||||
(requestType === VaultRequestType.CopyNote ||
|
||||
requestType === VaultRequestType.DeleteNote ||
|
||||
requestType === VaultRequestType.ShareNote ||
|
||||
requestType === VaultRequestType.CustomAction ||
|
||||
requestType === VaultRequestType.GoToEditor ||
|
||||
requestType === VaultRequestType.PermanentUnlock ||
|
||||
requestType === VaultRequestType.LockNote) ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
biometricUnlock: !prev.biometricUnlock
|
||||
}));
|
||||
setBiometricUnlock(!biometricUnlock);
|
||||
}}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
@@ -952,11 +925,9 @@ export const VaultDialog: React.FC = () => {
|
||||
width="100%"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
iconColor={
|
||||
state.biometricUnlock
|
||||
? colors.selected.accent
|
||||
: colors.primary.icon
|
||||
biometricUnlock ? colors.selected.accent : colors.primary.icon
|
||||
}
|
||||
type={state.biometricUnlock ? "transparent" : "plain"}
|
||||
type={biometricUnlock ? "transparent" : "plain"}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -965,34 +936,8 @@ export const VaultDialog: React.FC = () => {
|
||||
onPressNegative={close}
|
||||
onPressPositive={onPress}
|
||||
loading={loading}
|
||||
positiveType={
|
||||
shouldDeleteVault || shouldClearVault ? "errorShade" : "transparent"
|
||||
}
|
||||
positiveTitle={
|
||||
shouldDeleteVault
|
||||
? strings.delete()
|
||||
: shouldClearVault
|
||||
? strings.clear()
|
||||
: fingerprintAccess
|
||||
? strings.enable()
|
||||
: state.revokeFingerprintAccess
|
||||
? strings.revoke()
|
||||
: changePassword
|
||||
? strings.change()
|
||||
: customAction && customActionTitle
|
||||
? customActionTitle
|
||||
: state.noteLocked
|
||||
? shouldDeleteNote
|
||||
? strings.delete()
|
||||
: share
|
||||
? strings.share()
|
||||
: goToEditor
|
||||
? strings.open()
|
||||
: strings.unlock()
|
||||
: !note?.id
|
||||
? strings.create()
|
||||
: strings.lock()
|
||||
}
|
||||
positiveType={positiveButtonTypeRef.current}
|
||||
positiveTitle={buttonTitleRef.current || strings.unlock()}
|
||||
/>
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
|
||||
@@ -252,14 +252,16 @@ const NoteItem = ({
|
||||
{reminder ? (
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
disabled
|
||||
color={color?.colorCode}
|
||||
textStyle={{
|
||||
fontSize: AppFontSize.xxxs
|
||||
fontSize: AppFontSize.xxs
|
||||
}}
|
||||
iconSize={AppFontSize.xxxs}
|
||||
short
|
||||
iconSize={AppFontSize.xxs}
|
||||
style={{
|
||||
height: "auto"
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -32,9 +32,11 @@ import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
|
||||
import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import { RouteParams } from "../../../stores/use-navigation-store";
|
||||
import NotePreview from "../../note-history/preview";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import { RouteParams } from "../../../stores/use-navigation-store";
|
||||
|
||||
export const openNote = async (
|
||||
item: Note,
|
||||
@@ -63,11 +65,19 @@ export const openNote = async (
|
||||
component: <NotePreview note={item} content={content} />
|
||||
});
|
||||
} else {
|
||||
if (!useTabStore.getState().hasTabForNote(note.id!)) {
|
||||
editorController.current.commands.setLoading(
|
||||
true,
|
||||
useTabStore.getState().currentTab
|
||||
);
|
||||
}
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
setTimeout(() => {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
}, 32);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,10 +77,10 @@ export default function List(props: ListProps) {
|
||||
props.renderedInRoute === "Notes"
|
||||
? "home"
|
||||
: props.renderedInRoute === "Favorites"
|
||||
? "favorites"
|
||||
: props.renderedInRoute === "Trash" || props.dataType === "trash"
|
||||
? "trash"
|
||||
: `${props.dataType}s`;
|
||||
? "favorites"
|
||||
: props.renderedInRoute === "Trash" || props.dataType === "trash"
|
||||
? "trash"
|
||||
: `${props.dataType}s`;
|
||||
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
openVault
|
||||
openVault,
|
||||
VaultRequestType
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Sync from "../../services/sync";
|
||||
@@ -59,6 +60,7 @@ import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Cipher } from "@notesnook/crypto";
|
||||
|
||||
const MergeConflicts = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -83,12 +85,21 @@ const MergeConflicts = () => {
|
||||
dateEdited: contentToSave.dateEdited
|
||||
});
|
||||
|
||||
const noteLocked = await db.vaults.itemExists(note);
|
||||
const noteContent = await db.content.findByNoteId(note.id);
|
||||
|
||||
if (noteLocked) {
|
||||
await db.vault.save({
|
||||
...contentToSave,
|
||||
sessionId: `${Date.now()}`
|
||||
if (noteContent?.locked) {
|
||||
const selectedContent = contentToSave.conflicted
|
||||
? noteContent
|
||||
: noteContent.conflicted;
|
||||
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
dateResolved: noteContent?.conflicted?.dateModified || Date.now(),
|
||||
sessionId: `${Date.now()}`,
|
||||
data: selectedContent?.data as Cipher<"base64">,
|
||||
type: selectedContent?.type,
|
||||
locked: true,
|
||||
conflicted: undefined
|
||||
});
|
||||
} else {
|
||||
await db.content.add({
|
||||
@@ -128,14 +139,15 @@ const MergeConflicts = () => {
|
||||
let noteContent: UnencryptedContentItem;
|
||||
if (isLocked) {
|
||||
openVault({
|
||||
requestType: VaultRequestType.CustomAction,
|
||||
item: item,
|
||||
novault: true,
|
||||
customActionTitle: "Unlock note",
|
||||
customActionParagraph: "Unlock note to merge conflicts",
|
||||
title: strings.unlockNote(),
|
||||
customActionTitle: strings.unlockNote(),
|
||||
customActionParagraph: strings.unlockNoteToMergeConflicts(),
|
||||
buttonTitle: strings.unlock(),
|
||||
onUnlock: async (item, password) => {
|
||||
if (!item || !password) return;
|
||||
const currentContent = await db.content.get(item.contentId!);
|
||||
|
||||
try {
|
||||
noteContent = {
|
||||
...(await db.content.get(item.contentId!)),
|
||||
|
||||
@@ -76,6 +76,7 @@ import { IconButton } from "../ui/icon-button";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
const Steps = {
|
||||
select: 1,
|
||||
@@ -139,12 +140,12 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
}, [isFocused, step]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = EV.subscribe(
|
||||
const sub = db.eventManager.subscribe(
|
||||
EVENTS.userSubscriptionUpdated,
|
||||
(sub: User["subscription"]) => {
|
||||
if (sub.plan === SubscriptionPlan.FREE) return;
|
||||
if (routeParams.context === "signup") {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
@@ -182,8 +183,9 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
>
|
||||
<IconButton
|
||||
name="close"
|
||||
color={colors.primary.icon}
|
||||
onPress={() => {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -196,7 +198,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
return;
|
||||
}
|
||||
if (routeParams.context === "signup") {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
@@ -654,7 +656,7 @@ After trying all the privacy security oriented note taking apps, for the price a
|
||||
type="accent"
|
||||
onPress={() => {
|
||||
if (routeParams.context === "signup") {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
@@ -1006,8 +1008,8 @@ const PricingPlanCard = ({
|
||||
: "monthly"
|
||||
}`
|
||||
: pricingPlans.isGithubRelease
|
||||
? (WebPlan?.period as string)
|
||||
: (product?.productId as string)
|
||||
? (WebPlan?.period as string)
|
||||
: (product?.productId as string)
|
||||
);
|
||||
setStep(Steps.buy);
|
||||
}}
|
||||
|
||||
@@ -33,7 +33,6 @@ import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Dialog } from "../dialog";
|
||||
|
||||
const TOP_BAR_ITEMS: ActionId[] = [
|
||||
"pin",
|
||||
@@ -174,15 +173,15 @@ export const Items = ({
|
||||
DDS.isTab
|
||||
? AppFontSize.xxl
|
||||
: shouldShrink
|
||||
? AppFontSize.xxl
|
||||
: AppFontSize.lg
|
||||
? AppFontSize.xxl
|
||||
: AppFontSize.lg
|
||||
}
|
||||
color={
|
||||
item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id.match(/(delete|trash)/g)
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
@@ -213,8 +212,8 @@ export const Items = ({
|
||||
text: item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id === "delete" || item.id === "trash"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
testID={"icon-" + item.id}
|
||||
onPress={item.onPress}
|
||||
@@ -278,8 +277,8 @@ export const Items = ({
|
||||
item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id === "delete" || item.id === "trash"
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -327,8 +326,9 @@ export const Items = ({
|
||||
|
||||
const getTopBarItemChunksOfFour = () => {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < topBarItems.length; i += 5) {
|
||||
chunks.push(topBarItems.slice(i, i + 5));
|
||||
const itemCount = shouldShrink ? 4 : 5;
|
||||
for (let i = 0; i < topBarItems.length; i += itemCount) {
|
||||
chunks.push(topBarItems.slice(i, i + itemCount));
|
||||
}
|
||||
return chunks;
|
||||
};
|
||||
@@ -375,7 +375,8 @@ export const Items = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: 5
|
||||
gap: 5,
|
||||
width: width
|
||||
}}
|
||||
>
|
||||
{item.map(renderTopBarItem)}
|
||||
|
||||
@@ -356,7 +356,7 @@ const ProductItem = (props: {
|
||||
(product as RNIap.Subscription)?.productId
|
||||
) ||
|
||||
props.pricingPlans.user?.subscription?.productId ===
|
||||
(product as Plan).id);
|
||||
(product as Plan)?.id);
|
||||
|
||||
useEffect(() => {
|
||||
props.pricingPlans
|
||||
|
||||
@@ -102,9 +102,17 @@ const TabItemComponent = (props: {
|
||||
{props.tab.session?.noteLocked ? (
|
||||
<>
|
||||
{props.tab.session?.locked ? (
|
||||
<Icon size={AppFontSize.md} name="lock" />
|
||||
<Icon
|
||||
size={AppFontSize.md}
|
||||
name="lock"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
) : (
|
||||
<Icon size={AppFontSize.md} name="lock-open-outline" />
|
||||
<Icon
|
||||
size={AppFontSize.md}
|
||||
name="lock-open-outline"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -118,8 +118,8 @@ const ListBlockItem = ({
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: !item.content || item.content.trim() === ""
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
|
||||
@@ -169,7 +169,7 @@ class RecoveryKeySheet extends React.Component {
|
||||
};
|
||||
|
||||
onOpen = async () => {
|
||||
let k = await db.user.getEncryptionKey();
|
||||
let k = await db.user.getMasterKey();
|
||||
this.user = await db.user.getUser();
|
||||
if (k) {
|
||||
this.setState({
|
||||
|
||||
@@ -104,8 +104,8 @@ const ListBlockItem = ({
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: !item.content || item.content.trim() === ""
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
|
||||
@@ -114,8 +114,8 @@ export const UserSheet = () => {
|
||||
progress ? `(${progress.current})` : ""
|
||||
}`
|
||||
: lastSyncStatus === SyncStatus.Failed
|
||||
? strings.syncFailed()
|
||||
: strings.synced()}{" "}
|
||||
? strings.syncFailed()
|
||||
: strings.synced()}{" "}
|
||||
{!syncing ? (
|
||||
<TimeSince
|
||||
style={{
|
||||
@@ -139,8 +139,8 @@ export const UserSheet = () => {
|
||||
!user || lastSyncStatus === SyncStatus.Failed
|
||||
? colors.error.icon
|
||||
: isOffline
|
||||
? colors.static.orange
|
||||
: colors.success.icon
|
||||
? colors.static.orange
|
||||
: colors.success.icon
|
||||
}
|
||||
/>
|
||||
</Paragraph>
|
||||
|
||||
@@ -136,8 +136,8 @@ const SimpleTabView = ({
|
||||
{loadedKeysRef.current.has(route.key)
|
||||
? getSceneForRoute(route)
|
||||
: navigationState.index === routeIndex
|
||||
? getSceneForRoute(route)
|
||||
: null}
|
||||
? getSceneForRoute(route)
|
||||
: null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -435,8 +435,9 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
color={colors.primary.icon}
|
||||
onPress={async () => {
|
||||
if (props.navigationState.index === 1) {
|
||||
const notebooksFeature =
|
||||
await isFeatureAvailable("notebooks");
|
||||
const notebooksFeature = await isFeatureAvailable(
|
||||
"notebooks"
|
||||
);
|
||||
if (!notebooksFeature.isAllowed) {
|
||||
PaywallSheet.present(notebooksFeature);
|
||||
return;
|
||||
|
||||
@@ -151,8 +151,8 @@ export function MenuItem({
|
||||
item.icon === "crown"
|
||||
? colors.static.yellow
|
||||
: isFocused
|
||||
? colors.selected.icon
|
||||
: colors.secondary.icon
|
||||
? colors.selected.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
size={AppFontSize.md}
|
||||
/>
|
||||
|
||||
@@ -98,8 +98,8 @@ export const NotebookItem = ({
|
||||
item.depth === 0
|
||||
? undefined
|
||||
: item.depth < 6
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
width: "100%",
|
||||
marginTop: 2
|
||||
}}
|
||||
@@ -187,8 +187,8 @@ export const NotebookItem = ({
|
||||
!item.hasChildren || disableExpand
|
||||
? "book-outline"
|
||||
: expanded
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -241,6 +241,7 @@ export const NotebookItem = ({
|
||||
name="plus"
|
||||
size={AppFontSize.md}
|
||||
testID={`add-notebook-${index}`}
|
||||
color={colors.primary.icon}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
|
||||
@@ -303,7 +303,7 @@ export const SideMenuTags = () => {
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}, 500);
|
||||
}, 100);
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
@@ -158,22 +158,22 @@ export const Toast = ({ context = "global" }) => {
|
||||
toastOptions.icon
|
||||
? toastOptions.icon
|
||||
: toastOptions.type === "success"
|
||||
? "check"
|
||||
: toastOptions.type === "info"
|
||||
? "information"
|
||||
: "close"
|
||||
? "check"
|
||||
: toastOptions.type === "info"
|
||||
? "information"
|
||||
: "close"
|
||||
}
|
||||
size={isFullToastMessage ? AppFontSize.xxxl : AppFontSize.xl}
|
||||
color={
|
||||
toastOptions?.icon
|
||||
? toastOptions?.icon
|
||||
: toastOptions.type === "error"
|
||||
? colors.error.icon
|
||||
: toastOptions.type === "info"
|
||||
? isDark
|
||||
? colors.static.white
|
||||
: colors.static.black
|
||||
: colors.success.icon
|
||||
? colors.error.icon
|
||||
: toastOptions.type === "info"
|
||||
? isDark
|
||||
? colors.static.white
|
||||
: colors.static.black
|
||||
: colors.success.icon
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ColorValue, TextProps } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import EvilIcon from "react-native-vector-icons/EvilIcons";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
|
||||
export interface IconProps extends TextProps {
|
||||
@@ -43,11 +44,22 @@ export interface IconProps extends TextProps {
|
||||
*
|
||||
*/
|
||||
color?: ColorValue | number | undefined;
|
||||
|
||||
iconFamily?: "evilicons" | "material";
|
||||
}
|
||||
|
||||
export default function AppIcon(props: IconProps) {
|
||||
export default function AppIcon({
|
||||
iconFamily = "material",
|
||||
...props
|
||||
}: IconProps) {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
return iconFamily === "evilicons" ? (
|
||||
<EvilIcon
|
||||
size={AppFontSize.md}
|
||||
color={colors.primary.icon}
|
||||
{...(props as any)}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
size={AppFontSize.md}
|
||||
color={colors.primary.icon}
|
||||
|
||||
@@ -107,7 +107,7 @@ export const IconButton = ({
|
||||
color={
|
||||
restProps.disabled
|
||||
? RGB_Linear_Shade(-0.05, hexToRGBA(colors.secondary.background))
|
||||
: colors.static[color as never] || color
|
||||
: colors.static[color as never] || color || colors.primary.icon
|
||||
}
|
||||
size={size}
|
||||
/>
|
||||
|
||||
@@ -121,8 +121,8 @@ const Input = ({
|
||||
const color = error
|
||||
? colors.error.border
|
||||
: focus
|
||||
? customColor || colors.selected.border
|
||||
: colors.primary.border;
|
||||
? customColor || colors.selected.border
|
||||
: colors.primary.border;
|
||||
|
||||
const validate = async (value: string) => {
|
||||
if (!validationType) return;
|
||||
|
||||
@@ -268,8 +268,8 @@ export const Pressable = ({
|
||||
const opacity = customOpacity
|
||||
? customOpacity
|
||||
: type === "accent"
|
||||
? 1
|
||||
: colorOpacity;
|
||||
? 1
|
||||
: colorOpacity;
|
||||
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 8;
|
||||
|
||||
@@ -41,7 +41,9 @@ export const ReminderTime = ({
|
||||
} & ButtonProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const reminder = props.reminder;
|
||||
const time = !reminder ? undefined : getFormattedReminderTime(reminder);
|
||||
const time = !reminder
|
||||
? undefined
|
||||
: getFormattedReminderTime(reminder, props.short || false);
|
||||
const isTodayOrTomorrow =
|
||||
(time?.includes("Today") || time?.includes("Tomorrow")) &&
|
||||
!time?.includes("Last");
|
||||
|
||||
@@ -159,7 +159,7 @@ const SheetWrapper = ({
|
||||
{bottomPadding ? (
|
||||
<View
|
||||
style={{
|
||||
height: bottomInsets
|
||||
height: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import {
|
||||
Color,
|
||||
createInternalLink,
|
||||
isEncryptedContent,
|
||||
Item,
|
||||
ItemReference,
|
||||
Note,
|
||||
@@ -54,7 +55,8 @@ import {
|
||||
eSubscribeEvent,
|
||||
openVault,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
ToastManager,
|
||||
VaultRequestType
|
||||
} from "../services/event-manager";
|
||||
import Navigation from "../services/navigation";
|
||||
import Notifications from "../services/notifications";
|
||||
@@ -220,7 +222,15 @@ export const useActions = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (item.type === "note") {
|
||||
db.vaults.itemExists(item).then((locked) => setLocked(locked));
|
||||
db.vaults.itemExists(item).then(async (locked) => {
|
||||
setLocked(locked);
|
||||
if (!locked) {
|
||||
const content = await db.content.findByNoteId(item.id);
|
||||
if (content && isEncryptedContent(content)) {
|
||||
setLocked(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
@@ -408,12 +418,12 @@ export const useActions = ({
|
||||
|
||||
if (item.type === "note" && (await db.vaults.itemExists(item))) {
|
||||
openVault({
|
||||
deleteNote: true,
|
||||
novault: true,
|
||||
locked: true,
|
||||
requestType: VaultRequestType.DeleteNote,
|
||||
item: item,
|
||||
title: strings.deleteNote(),
|
||||
description: strings.unlockToDelete()
|
||||
description: strings.unlockToDelete(),
|
||||
buttonTitle: strings.delete(),
|
||||
positiveButtonType: "errorShade"
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
@@ -867,11 +877,10 @@ export const useActions = ({
|
||||
close();
|
||||
await sleep(300);
|
||||
openVault({
|
||||
requestType: VaultRequestType.ShareNote,
|
||||
item: item,
|
||||
novault: true,
|
||||
locked: true,
|
||||
share: true,
|
||||
title: strings.shareNote()
|
||||
title: strings.shareNote(),
|
||||
buttonTitle: strings.share()
|
||||
});
|
||||
} else {
|
||||
processingId.current = "shareNote";
|
||||
@@ -896,11 +905,10 @@ export const useActions = ({
|
||||
close();
|
||||
await sleep(300);
|
||||
openVault({
|
||||
requestType: VaultRequestType.PermanentUnlock,
|
||||
item: item,
|
||||
novault: true,
|
||||
locked: true,
|
||||
permanant: true,
|
||||
title: strings.unlockNote()
|
||||
title: strings.unlockNote(),
|
||||
buttonTitle: strings.unlock()
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -918,17 +926,18 @@ export const useActions = ({
|
||||
switch ((e as Error).message) {
|
||||
case VAULT_ERRORS.noVault:
|
||||
openVault({
|
||||
requestType: VaultRequestType.CreateVault,
|
||||
item: item,
|
||||
novault: false,
|
||||
title: strings.createVault()
|
||||
title: strings.createVault(),
|
||||
buttonTitle: strings.lock()
|
||||
});
|
||||
break;
|
||||
case VAULT_ERRORS.vaultLocked:
|
||||
openVault({
|
||||
requestType: VaultRequestType.LockNote,
|
||||
item: item,
|
||||
novault: true,
|
||||
locked: true,
|
||||
title: strings.lockNote()
|
||||
title: strings.lockNote(),
|
||||
buttonTitle: strings.lock()
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -949,11 +958,10 @@ export const useActions = ({
|
||||
close();
|
||||
await sleep(300);
|
||||
openVault({
|
||||
copyNote: true,
|
||||
novault: true,
|
||||
locked: true,
|
||||
requestType: VaultRequestType.CopyNote,
|
||||
item: item as Note,
|
||||
title: strings.copyNote()
|
||||
title: strings.copyNote(),
|
||||
buttonTitle: strings.copy()
|
||||
});
|
||||
} else {
|
||||
processingId.current = "copyContent";
|
||||
|
||||
@@ -94,6 +94,7 @@ import { SyncStatus, useUserStore } from "../stores/use-user-store";
|
||||
import { updateStatusBarColor } from "../utils/colors";
|
||||
import { BETA } from "../utils/constants";
|
||||
import {
|
||||
eAfterSync,
|
||||
eCloseSheet,
|
||||
eEditorReset,
|
||||
eLoginSessionExpired,
|
||||
@@ -332,6 +333,7 @@ const onLogout = async (reason: string) => {
|
||||
SettingsService.resetSettings();
|
||||
useUserStore.getState().setUser(null);
|
||||
useUserStore.getState().setSyncing(false);
|
||||
eSendEvent(eAfterSync);
|
||||
};
|
||||
|
||||
async function checkForShareExtensionLaunchedInBackground() {
|
||||
@@ -727,24 +729,39 @@ export const useAppEvents = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const subscriptions = [
|
||||
EV.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
|
||||
EV.subscribe(EVENTS.syncAborted, onSyncAborted),
|
||||
EV.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
|
||||
db.eventManager.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
|
||||
db.eventManager.subscribe(EVENTS.syncAborted, onSyncAborted),
|
||||
db.eventManager.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
|
||||
db.eventManager.subscribe(EVENTS.userLoggedOut, onLogout),
|
||||
db.eventManager.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
|
||||
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.userSessionExpired,
|
||||
onUserSessionExpired
|
||||
),
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.userSubscriptionUpdated,
|
||||
onUserSubscriptionStatusChanged
|
||||
),
|
||||
EV.subscribe(EVENTS.fileDownload, onDownloadingAttachmentProgress),
|
||||
EV.subscribe(EVENTS.fileUpload, onUploadingAttachmentProgress),
|
||||
EV.subscribe(EVENTS.fileDownloaded, onDownloadedAttachmentProgress),
|
||||
EV.subscribe(EVENTS.fileUploaded, onUploadedAttachmentProgress),
|
||||
EV.subscribe(EVENTS.downloadCanceled, (data) => {
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.fileDownload,
|
||||
onDownloadingAttachmentProgress
|
||||
),
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.fileUpload,
|
||||
onUploadingAttachmentProgress
|
||||
),
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.fileDownloaded,
|
||||
onDownloadedAttachmentProgress
|
||||
),
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.fileUploaded,
|
||||
onUploadedAttachmentProgress
|
||||
),
|
||||
db.eventManager.subscribe(EVENTS.downloadCanceled, (data) => {
|
||||
useAttachmentStore.getState().setDownloading(data);
|
||||
}),
|
||||
EV.subscribe(EVENTS.uploadCanceled, (data) => {
|
||||
db.eventManager.subscribe(EVENTS.uploadCanceled, (data) => {
|
||||
useAttachmentStore.getState().setUploading(data);
|
||||
}),
|
||||
EV.subscribe(EVENTS.migrationStarted, (name) => {
|
||||
@@ -770,7 +787,7 @@ export const useAppEvents = () => {
|
||||
return;
|
||||
endProgress();
|
||||
}),
|
||||
EV.subscribe(EVENTS.vaultLocked, async () => {
|
||||
db.eventManager.subscribe(EVENTS.vaultLocked, async () => {
|
||||
// Lock all notes in all tabs...
|
||||
for (const tab of useTabStore.getState().tabs) {
|
||||
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
|
||||
@@ -802,7 +819,6 @@ export const useAppEvents = () => {
|
||||
return () => {
|
||||
emitterSubscriptions.forEach((sub) => sub?.remove?.());
|
||||
subscriptions.forEach((sub) => sub?.unsubscribe?.());
|
||||
EV.unsubscribeAll();
|
||||
};
|
||||
}, [onSyncComplete, onUserUpdated]);
|
||||
|
||||
|
||||
@@ -60,23 +60,17 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
|
||||
): [ItemTypeKey[T] | undefined, () => void] => {
|
||||
const [item, setItem] = useState<ItemTypeKey[T]>();
|
||||
const itemIdRef = useRef<string>(undefined);
|
||||
const prevIdOrIndexRef = useRef<string | number>(undefined);
|
||||
|
||||
if (prevIdOrIndexRef.current !== idOrIndex) {
|
||||
itemIdRef.current = undefined;
|
||||
prevIdOrIndexRef.current = idOrIndex;
|
||||
}
|
||||
const itemsRef = useRef(item);
|
||||
itemsRef.current = item;
|
||||
|
||||
useEffect(() => {
|
||||
const onUpdateItem = async (itemId?: string) => {
|
||||
if (typeof itemId === "string" && itemId !== itemIdRef.current) return;
|
||||
if (!isValidIdOrIndex(idOrIndex)) return;
|
||||
|
||||
let item: ItemTypeKey[T] | undefined = undefined;
|
||||
if (items && typeof idOrIndex === "number") {
|
||||
const item = (await items.item(idOrIndex))?.item;
|
||||
setItem(item);
|
||||
itemIdRef.current = item?.id;
|
||||
onItemUpdated?.(item);
|
||||
item = (await items.item(idOrIndex))?.item;
|
||||
} else {
|
||||
if (!(db as any)[type + "s"][type]) {
|
||||
console.warn(
|
||||
@@ -84,24 +78,30 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
|
||||
`db.${type}s.${type}(id: string)`
|
||||
);
|
||||
} else {
|
||||
const item = await (db as any)[type + "s"]?.[type]?.(
|
||||
idOrIndex as string
|
||||
);
|
||||
setItem(item);
|
||||
itemIdRef.current = item.id;
|
||||
onItemUpdated?.(item);
|
||||
item = await (db as any)[type + "s"]?.[type]?.(idOrIndex as string);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
itemsRef.current === item ||
|
||||
itemsRef.current?.dateModified === item?.dateModified
|
||||
)
|
||||
return;
|
||||
setItem(item);
|
||||
itemIdRef.current = item?.id;
|
||||
onItemUpdated?.(item);
|
||||
};
|
||||
let unsub: (() => void) | undefined;
|
||||
if (
|
||||
useSettingStore.getState().isAppLoading &&
|
||||
//@ts-ignore
|
||||
!globalThis["IS_SHARE_EXTENSION"]
|
||||
) {
|
||||
useSettingStore.subscribe((state) => {
|
||||
unsub = useSettingStore.subscribe((state) => {
|
||||
if (!state.isAppLoading) {
|
||||
onUpdateItem();
|
||||
unsub?.();
|
||||
unsub = undefined;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -109,9 +109,10 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
|
||||
}
|
||||
eSubscribeEvent(eDBItemUpdate, onUpdateItem);
|
||||
return () => {
|
||||
unsub?.();
|
||||
eUnSubscribeEvent(eDBItemUpdate, onUpdateItem);
|
||||
};
|
||||
}, [idOrIndex, type, items, onItemUpdated]);
|
||||
}, [idOrIndex, type, items]);
|
||||
|
||||
return [
|
||||
isValidIdOrIndex(idOrIndex) ? (item as ItemTypeKey[T]) : undefined,
|
||||
@@ -134,8 +135,9 @@ export const useNoteLocked = (noteId: string | undefined) => {
|
||||
//@ts-ignore
|
||||
!globalThis["IS_SHARE_EXTENSION"]
|
||||
) {
|
||||
useSettingStore.subscribe((state) => {
|
||||
let unsub = useSettingStore.subscribe((state) => {
|
||||
if (!state.isAppLoading) {
|
||||
unsub();
|
||||
db.vaults
|
||||
.itemExists({
|
||||
type: "note",
|
||||
@@ -181,25 +183,27 @@ export const useTotalNotes = (type: "notebook" | "tag" | "color") => {
|
||||
const [totalNotesById, setTotalNotesById] = useState<{
|
||||
[id: string]: number;
|
||||
}>({});
|
||||
const totalNotesRef = useRef(0);
|
||||
|
||||
const getTotalNotes = React.useCallback(
|
||||
(ids: string[]) => {
|
||||
if (!ids || !ids.length || !type) return;
|
||||
db.relations
|
||||
.from({ type: type, ids: ids as string[] }, ["note"])
|
||||
.get()
|
||||
.then((relations) => {
|
||||
const totalNotesById: any = {};
|
||||
for (const id of ids) {
|
||||
totalNotesById[id] = relations.filter(
|
||||
(relation) => relation.fromId === id && relation.toType === "note"
|
||||
)?.length;
|
||||
}
|
||||
setTotalNotesById(totalNotesById);
|
||||
});
|
||||
},
|
||||
[type]
|
||||
);
|
||||
const getTotalNotes = React.useCallback((ids: string[]) => {
|
||||
if (!ids || !ids.length || !type) return;
|
||||
db.relations
|
||||
.from({ type: type, ids: ids as string[] }, ["note"])
|
||||
.get()
|
||||
.then((relations) => {
|
||||
const totalNotesById: any = {};
|
||||
|
||||
for (const id of ids) {
|
||||
totalNotesById[id] = relations.filter(
|
||||
(relation) => relation.fromId === id && relation.toType === "note"
|
||||
)?.length;
|
||||
}
|
||||
|
||||
if (totalNotesById === totalNotesRef.current) return;
|
||||
totalNotesRef.current = totalNotesById;
|
||||
setTotalNotesById(totalNotesById);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
totalNotes: (id: string) => {
|
||||
|
||||
@@ -276,8 +276,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
(product as Plan).period === "yearly"
|
||||
? (product as Plan).price.gross
|
||||
: (product as Plan).period === "5-year"
|
||||
? (product as Plan).price.gross
|
||||
: (product as Plan).price.gross
|
||||
? (product as Plan).price.gross
|
||||
: (product as Plan).price.gross
|
||||
}`;
|
||||
}
|
||||
|
||||
@@ -432,8 +432,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
return period.endsWith("W")
|
||||
? "week"
|
||||
: period.endsWith("M")
|
||||
? "month"
|
||||
: "year";
|
||||
? "month"
|
||||
: "year";
|
||||
} else {
|
||||
const unit = (product as RNIap.SubscriptionIOS)
|
||||
?.subscriptionPeriodUnitIOS;
|
||||
@@ -475,8 +475,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
type: phase.billingPeriod.endsWith("W")
|
||||
? "week"
|
||||
: phase.billingPeriod.endsWith("M")
|
||||
? "month"
|
||||
: "year"
|
||||
? "month"
|
||||
: "year"
|
||||
};
|
||||
} else {
|
||||
const productIos = product as RNIap.SubscriptionIOS;
|
||||
@@ -607,8 +607,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
(product as Plan).period === "yearly"
|
||||
? ((product as Plan).price.gross / 12).toFixed(2)
|
||||
: (product as Plan).period === "5-year"
|
||||
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
|
||||
: (product as Plan).price.gross
|
||||
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
|
||||
: (product as Plan).price.gross
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ export type SyncProgressEventType = {
|
||||
|
||||
const useSyncProgress = () => {
|
||||
const [progress, setProgress] = useState<SyncProgressEventType>();
|
||||
const EV = db.eventManager;
|
||||
|
||||
const onProgress = useCallback(
|
||||
({ type, current, total }: SyncProgressEventType) => {
|
||||
@@ -42,13 +41,13 @@ const useSyncProgress = () => {
|
||||
setProgress(undefined);
|
||||
};
|
||||
useEffect(() => {
|
||||
EV?.subscribe(EVENTS.syncProgress, onProgress);
|
||||
EV?.subscribe(EVENTS.syncCompleted, onSyncComplete);
|
||||
db.eventManager.subscribe(EVENTS.syncProgress, onProgress);
|
||||
db.eventManager.subscribe(EVENTS.syncCompleted, onSyncComplete);
|
||||
return () => {
|
||||
EV?.unsubscribe(EVENTS.syncProgress, onProgress);
|
||||
EV?.unsubscribe(EVENTS.syncCompleted, onSyncComplete);
|
||||
db.eventManager.unsubscribe(EVENTS.syncProgress, onProgress);
|
||||
db.eventManager.unsubscribe(EVENTS.syncCompleted, onSyncComplete);
|
||||
};
|
||||
}, [EV, onProgress]);
|
||||
}, [onProgress]);
|
||||
|
||||
return {
|
||||
progress
|
||||
|
||||
@@ -243,8 +243,8 @@ export const FluidPanelsView = React.memo(
|
||||
const nextDeviceMode = DDS.isLargeTablet()
|
||||
? "tablet"
|
||||
: DDS.isSmallTab
|
||||
? "smallTablet"
|
||||
: "mobile";
|
||||
? "smallTablet"
|
||||
: "mobile";
|
||||
setDeviceMode(nextDeviceMode, size);
|
||||
},
|
||||
[orientation, setDeviceMode, setDimensions]
|
||||
|
||||
@@ -39,7 +39,7 @@ import Input from "../../components/ui/input";
|
||||
import { ReminderTime } from "../../components/ui/reminder-time";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation, { NavigationProps } from "../../services/navigation";
|
||||
import Notifications from "../../services/notifications";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -47,9 +47,18 @@ import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import {
|
||||
getFormattedDate,
|
||||
useIsFeatureAvailable,
|
||||
usePromise
|
||||
} from "@notesnook/common";
|
||||
import PaywallSheet from "../../components/sheets/paywall";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import { TimeSince } from "../../components/ui/time-since";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import { eOnLoadNote } from "../../utils/events";
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
|
||||
const ReminderModes =
|
||||
Platform.OS === "ios"
|
||||
@@ -114,6 +123,15 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
const titleRef = useRef<TextInput>(null);
|
||||
const descriptionRef = useRef<TextInput>(null);
|
||||
const timer = useRef<NodeJS.Timeout>(undefined);
|
||||
const referencedNotes = usePromise(
|
||||
() =>
|
||||
reminder?.id
|
||||
? db.relations
|
||||
.to({ id: reminder.id, type: "reminder" }, "note")
|
||||
.resolve()
|
||||
: null,
|
||||
[reminder?.id]
|
||||
);
|
||||
|
||||
const showDatePicker = () => {
|
||||
setDatePickerVisibility(true);
|
||||
@@ -237,6 +255,9 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
marginBottom: DDS.isTab ? 25 : undefined,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
@@ -271,15 +292,11 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
height={80}
|
||||
wrapperStyle={{
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
flexDirection: "row"
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
@@ -335,8 +352,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
borderRadius: defaultBorderRadius
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -384,45 +400,17 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
recurringMode === RecurringModes.Year
|
||||
? null
|
||||
: recurringMode === RecurringModes.Week
|
||||
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
|
||||
(item) => (
|
||||
<Button
|
||||
key={strings.weekDayNamesShort[
|
||||
item as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
title={strings.weekDayNamesShort[
|
||||
item as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
type={
|
||||
selectedDays.indexOf(item) > -1
|
||||
? "selected"
|
||||
: "plain"
|
||||
}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
setSelectedDays((days) => {
|
||||
if (days.indexOf(item) > -1) {
|
||||
days.splice(days.indexOf(item), 1);
|
||||
return [...days];
|
||||
}
|
||||
days.push(item);
|
||||
return [...days];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)
|
||||
: MonthDays.map((item, index) => (
|
||||
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
|
||||
(item) => (
|
||||
<Button
|
||||
key={index + "monthday"}
|
||||
title={index + 1 + ""}
|
||||
key={strings.weekDayNamesShort[
|
||||
item as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
title={strings.weekDayNamesShort[
|
||||
item as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
type={
|
||||
selectedDays.indexOf(index + 1) > -1
|
||||
selectedDays.indexOf(item) > -1
|
||||
? "selected"
|
||||
: "plain"
|
||||
}
|
||||
@@ -434,16 +422,44 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
}}
|
||||
onPress={() => {
|
||||
setSelectedDays((days) => {
|
||||
if (days.indexOf(index + 1) > -1) {
|
||||
days.splice(days.indexOf(index + 1), 1);
|
||||
if (days.indexOf(item) > -1) {
|
||||
days.splice(days.indexOf(item), 1);
|
||||
return [...days];
|
||||
}
|
||||
days.push(index + 1);
|
||||
days.push(item);
|
||||
return [...days];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
)
|
||||
)
|
||||
: MonthDays.map((item, index) => (
|
||||
<Button
|
||||
key={index + "monthday"}
|
||||
title={index + 1 + ""}
|
||||
type={
|
||||
selectedDays.indexOf(index + 1) > -1
|
||||
? "selected"
|
||||
: "plain"
|
||||
}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
setSelectedDays((days) => {
|
||||
if (days.indexOf(index + 1) > -1) {
|
||||
days.splice(days.indexOf(index + 1), 1);
|
||||
return [...days];
|
||||
}
|
||||
days.push(index + 1);
|
||||
return [...days];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -454,7 +470,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
@@ -513,10 +528,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
flexDirection: "row",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
>
|
||||
<>
|
||||
@@ -529,45 +542,33 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
dayjs(date).format("hh:mm A")
|
||||
)
|
||||
: recurringMode === RecurringModes.Year
|
||||
? strings.reminderRepeatStrings.year(
|
||||
dayjs(date).format("dddd, MMMM D, h:mm A")
|
||||
)
|
||||
: selectedDays.length === 7 &&
|
||||
recurringMode === RecurringModes.Week
|
||||
? strings.reminderRepeatStrings.week.daily(
|
||||
dayjs(date).format("hh:mm A")
|
||||
)
|
||||
: selectedDays.length === 0
|
||||
? strings.reminderRepeatStrings[
|
||||
recurringMode as "week" | "month"
|
||||
].selectDays()
|
||||
: strings.reminderRepeatStrings.repeats(
|
||||
repeatFrequency,
|
||||
recurringMode as string,
|
||||
getSelectedDaysText(selectedDays),
|
||||
dayjs(date).format("hh:mm A")
|
||||
)}
|
||||
? strings.reminderRepeatStrings.year(
|
||||
dayjs(date).format("dddd, MMMM D, h:mm A")
|
||||
)
|
||||
: selectedDays.length === 7 &&
|
||||
recurringMode === RecurringModes.Week
|
||||
? strings.reminderRepeatStrings.week.daily(
|
||||
dayjs(date).format("hh:mm A")
|
||||
)
|
||||
: selectedDays.length === 0
|
||||
? strings.reminderRepeatStrings[
|
||||
recurringMode as "week" | "month"
|
||||
].selectDays()
|
||||
: strings.reminderRepeatStrings.repeats(
|
||||
repeatFrequency,
|
||||
recurringMode as string,
|
||||
getSelectedDaysText(selectedDays),
|
||||
dayjs(date).format("hh:mm A")
|
||||
)}
|
||||
</Paragraph>
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
|
||||
{reminderMode === ReminderModes.Permanent ? null : (
|
||||
<ScrollView
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
height: 50
|
||||
}}
|
||||
horizontal
|
||||
@@ -586,8 +587,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
mode === "Silent"
|
||||
? "minus-circle"
|
||||
: mode === "Vibrate"
|
||||
? "vibrate"
|
||||
: "volume-high"
|
||||
? "vibrate"
|
||||
: "volume-high"
|
||||
}
|
||||
fontSize={AppFontSize.xs}
|
||||
height={35}
|
||||
@@ -612,6 +613,60 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
|
||||
{referencedNotes &&
|
||||
referencedNotes.status === "fulfilled" &&
|
||||
referencedNotes.value !== null &&
|
||||
referencedNotes.value?.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.md}>{strings.referencedIn()}</Heading>
|
||||
{referencedNotes.value.map((item) => (
|
||||
<Pressable
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onPress={() => {
|
||||
Navigation.navigate("FluidPanelsView");
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: item
|
||||
});
|
||||
}}
|
||||
type="secondary"
|
||||
>
|
||||
<Paragraph>{item.title}</Paragraph>
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: AppFontSize.xxs,
|
||||
color: colors.secondary.paragraph,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={item.dateEdited}
|
||||
updateFrequency={
|
||||
Date.now() - item.dateEdited < 60000 ? 2000 : 60000
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</KeyboardViewIOS>
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -25,7 +25,8 @@ import React, {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { Platform, ViewStyle } from "react-native";
|
||||
import WebView from "react-native-webview";
|
||||
@@ -109,17 +110,20 @@ const Editor = React.memo(
|
||||
noToolbar,
|
||||
noHeader
|
||||
});
|
||||
const renderKey = useRef(randId("editor-id") + editorId);
|
||||
const [renderKey, setRenderKey] = useState(
|
||||
randId("editor-id") + editorId
|
||||
);
|
||||
useImperativeHandle(ref, () => ({
|
||||
get: () => editor
|
||||
}));
|
||||
useLockedNoteHandler();
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current = randId("editor-id") + editorId;
|
||||
setRenderKey(randId("editor-id") + editorId);
|
||||
editor.state.current.ready = false;
|
||||
editor.state.current.initialLoadCalled = false;
|
||||
editor.setLoading(true);
|
||||
}, [editor, editorId]);
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = [eSubscribeEvent(eEditorReset, onError)];
|
||||
@@ -144,7 +148,7 @@ const Editor = React.memo(
|
||||
<WebView
|
||||
testID={notesnook.editor.id}
|
||||
ref={editor.ref}
|
||||
key={renderKey.current}
|
||||
key={renderKey}
|
||||
onRenderProcessGone={onError}
|
||||
nestedScrollEnabled
|
||||
onError={onError}
|
||||
@@ -179,6 +183,7 @@ const Editor = React.memo(
|
||||
hideKeyboardAccessoryView={false}
|
||||
allowsFullscreenVideo={true}
|
||||
allowFileAccessFromFileURLs={true}
|
||||
allowsInlineMediaPlayback
|
||||
allowUniversalAccessFromFileURLs={true}
|
||||
originWhitelist={["*"]}
|
||||
source={{
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
KeepLocalCopyResponse,
|
||||
pick as pickFile
|
||||
} from "@react-native-documents/picker";
|
||||
import { basename } from "pathe";
|
||||
import { basename, dirname } from "pathe";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
|
||||
@@ -119,41 +119,30 @@ const file = async (fileOptions: PickerOptions) => {
|
||||
throw new Error("Failed to attach file");
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.unlink(uri);
|
||||
RNFetchBlob.fs.unlink(dirname(fileCopyUri.localUri)).catch((e) => {
|
||||
console.log(e, "error");
|
||||
});
|
||||
|
||||
if (
|
||||
fileOptions.tabId !== undefined &&
|
||||
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
|
||||
fileOptions.noteId
|
||||
) {
|
||||
if (isImage(file.type || "application/octet-stream")) {
|
||||
editorController.current?.commands.insertImage(
|
||||
{
|
||||
hash: hash,
|
||||
filename: fileName,
|
||||
mime: file.type || "application/octet-stream",
|
||||
size: file.size || 0,
|
||||
dataurl: (await db.attachments.read(hash, "base64")) as string,
|
||||
type: "image"
|
||||
},
|
||||
fileOptions.tabId
|
||||
);
|
||||
} else {
|
||||
editorController.current?.commands.insertAttachment(
|
||||
{
|
||||
hash: hash,
|
||||
filename: fileName,
|
||||
mime: file.type || "application/octet-stream",
|
||||
size: file.size || 0,
|
||||
type: "file"
|
||||
},
|
||||
fileOptions.tabId
|
||||
);
|
||||
}
|
||||
editorController.current?.commands.insertAttachment(
|
||||
{
|
||||
hash: hash,
|
||||
filename: fileName,
|
||||
mime: file.type || "application/octet-stream",
|
||||
size: file.size || 0,
|
||||
type: "file"
|
||||
},
|
||||
fileOptions.tabId
|
||||
);
|
||||
} else {
|
||||
throw new Error("Failed to attach file, no tabId is set");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("ERROR OCCURED HERE.", e);
|
||||
ToastManager.show({
|
||||
heading: (e as Error).message,
|
||||
type: "error",
|
||||
@@ -264,7 +253,6 @@ const handleImageResponse = async (
|
||||
for (const image of response) {
|
||||
const isPng = /(png)/g.test(image.mime);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(image.mime);
|
||||
|
||||
if (compress && (isPng || isJpeg)) {
|
||||
image.path = await compressToFile(
|
||||
Platform.OS === "ios" ? "file://" + image.path : image.path,
|
||||
@@ -304,7 +292,7 @@ const handleImageResponse = async (
|
||||
|
||||
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
|
||||
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
RNFetchBlob.fs.unlink(uri).catch((e) => {});
|
||||
|
||||
if (
|
||||
options.tabId !== undefined &&
|
||||
@@ -389,9 +377,7 @@ export async function attachFile(
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
if (Platform.OS === "ios") {
|
||||
await RNFetchBlob.fs.unlink(uri);
|
||||
}
|
||||
RNFetchBlob.fs.unlink(uri).catch((e) => {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,8 +471,9 @@ export const useEditorEvents = (
|
||||
relationType: "from",
|
||||
title: strings.dataTypesPluralCamelCase.reminder(),
|
||||
onAdd: async () => {
|
||||
const reminderFeature =
|
||||
await isFeatureAvailable("activeReminders");
|
||||
const reminderFeature = await isFeatureAvailable(
|
||||
"activeReminders"
|
||||
);
|
||||
if (!reminderFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
@@ -534,15 +535,24 @@ export const useEditorEvents = (
|
||||
}
|
||||
|
||||
case EditorEvents.getAttachmentData: {
|
||||
const attachment = (editorMessage.value as any)
|
||||
?.attachment as Attachment;
|
||||
const data = (editorMessage.value as any)?.attachment as Attachment;
|
||||
|
||||
const attachment = await db.attachments.attachment(data.hash);
|
||||
|
||||
if (!attachment) {
|
||||
editor.postMessage(NativeEvents.resolve, {
|
||||
resolverId: editorMessage.resolverId,
|
||||
data: undefined
|
||||
});
|
||||
break;
|
||||
}
|
||||
DatabaseLogger.log(
|
||||
`Getting attachment data: ${attachment?.hash} ${attachment?.type}`
|
||||
`Getting attachment data: ${attachment.mimeType} ${attachment.hash} ${data.type}`
|
||||
);
|
||||
downloadAttachment(attachment.hash, true, {
|
||||
base64: attachment.type === "image",
|
||||
text: attachment.type === "web-clip",
|
||||
base64:
|
||||
data.type === "image" || attachment.mimeType?.startsWith("audio"),
|
||||
text: data.type === "web-clip",
|
||||
silent: true,
|
||||
groupId: editor.note.current?.id,
|
||||
cache: true
|
||||
@@ -550,7 +560,7 @@ export const useEditorEvents = (
|
||||
.then((data: any) => {
|
||||
console.log(
|
||||
"Got attachment data:",
|
||||
!!data,
|
||||
data,
|
||||
editorMessage.resolverId
|
||||
);
|
||||
editor.postMessage(NativeEvents.resolve, {
|
||||
|
||||
@@ -487,6 +487,17 @@ export const useEditor = (
|
||||
};
|
||||
} else if (note.contentId) {
|
||||
const rawContent = await db.content?.get(note.contentId);
|
||||
|
||||
if (rawContent && isEncryptedContent(rawContent)) {
|
||||
useTabStore.getState().updateTab(useTabStore.getState().currentTab, {
|
||||
session: {
|
||||
noteLocked: true,
|
||||
locked: true
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawContent && !isDeleted(rawContent) && !rawContent.locked) {
|
||||
currentContents.current[note.id] = {
|
||||
data: rawContent.data,
|
||||
@@ -684,8 +695,6 @@ export const useEditor = (
|
||||
lastContentChangeTime.current[item.id] = item.dateEdited;
|
||||
currentNotes.current[item.id] = item;
|
||||
|
||||
if (!currentNotes.current[item.id]) return;
|
||||
|
||||
editorSessionHistory.newSession(item.id);
|
||||
|
||||
await commands.setStatus(
|
||||
@@ -1034,13 +1043,6 @@ export const useEditor = (
|
||||
state.current.isRestoringState = false;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOnLoadNote + editorId, loadNote);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOnLoadNote + editorId, loadNote);
|
||||
};
|
||||
}, [editorId, loadNote, restoreEditorState, isDefaultEditor]);
|
||||
|
||||
const onContentChanged = (noteId?: string) => {
|
||||
if (noteId) {
|
||||
lastContentChangeTime.current[noteId] = Date.now();
|
||||
@@ -1104,6 +1106,8 @@ export const useEditor = (
|
||||
if (noteId) {
|
||||
const note = await db.notes?.note(noteId);
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
loadNoteMutex.cancel();
|
||||
loadNoteMutex.release();
|
||||
if (note) {
|
||||
loadNote({
|
||||
item: note
|
||||
@@ -1111,6 +1115,8 @@ export const useEditor = (
|
||||
}
|
||||
} else {
|
||||
noteId = useTabStore.getState().getCurrentNoteId() || null;
|
||||
loadNoteMutex.cancel();
|
||||
loadNoteMutex.release();
|
||||
if (!noteId) {
|
||||
loadNote({ newNote: true });
|
||||
if (fluidTabsRef.current?.page() === "editor") {
|
||||
|
||||
@@ -237,8 +237,9 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
|
||||
button={{
|
||||
icon: "plus",
|
||||
onPress: async () => {
|
||||
const notebooksFeature =
|
||||
await isFeatureAvailable("notebooks");
|
||||
const notebooksFeature = await isFeatureAvailable(
|
||||
"notebooks"
|
||||
);
|
||||
if (!notebooksFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: notebooksFeature.error,
|
||||
@@ -387,8 +388,8 @@ const NotebookItemWrapper = React.memo(
|
||||
!selected
|
||||
? "selected"
|
||||
: !state.initialState[item.notebook.id]
|
||||
? undefined
|
||||
: "deselected"
|
||||
? undefined
|
||||
: "deselected"
|
||||
);
|
||||
}}
|
||||
canDisableSelectionMode={false}
|
||||
|
||||
@@ -17,24 +17,22 @@ 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 { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { sanitizeTag } from "@notesnook/core";
|
||||
import { Tag } from "@notesnook/core";
|
||||
import { LegendList } from "@legendapp/list";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { Tag, VirtualizedGrouping, sanitizeTag } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { TextInput, View, useWindowDimensions } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { Header } from "../../components/header";
|
||||
import Input from "../../components/ui/input";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation, { NavigationProps } from "../../services/navigation";
|
||||
import {
|
||||
@@ -43,17 +41,8 @@ import {
|
||||
} from "../../stores/item-selection-store";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { useTagStore } from "../../stores/use-tag-store";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
|
||||
import Input from "../../components/ui/input";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Header } from "../../components/header";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { LegendList } from "@legendapp/list";
|
||||
|
||||
async function updateInitialSelectionState(items: string[]) {
|
||||
const relations = await db.relations
|
||||
@@ -92,13 +81,12 @@ const useTagItemSelection = createItemSelectionStore(true);
|
||||
const ManageTags = (props: NavigationProps<"ManageTags">) => {
|
||||
const { colors } = useThemeColors();
|
||||
const ids = props.route.params.ids || [];
|
||||
const [tags, setTags] = useState<VirtualizedGrouping<Tag>>();
|
||||
const [tags, setTags] = useState<Tag[]>();
|
||||
const [query, setQuery] = useState<string>();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [focus, setFocus] = useState(false);
|
||||
useNavigationFocus(props.navigation, { focusOnInit: true });
|
||||
const timerRef = useRef<NodeJS.Timeout>(undefined);
|
||||
const [queryExists, setQueryExists] = useState(false);
|
||||
const dimensions = useWindowDimensions();
|
||||
const refreshSelection = useCallback(() => {
|
||||
updateInitialSelectionState(ids).then((selection) => {
|
||||
useTagItemSelection.setState({
|
||||
@@ -109,18 +97,43 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ids, tags]);
|
||||
|
||||
const sortAndSetTags = useCallback(
|
||||
async (items: VirtualizedGrouping<Tag>) => {
|
||||
const tags = [];
|
||||
const noteTags = [];
|
||||
const assignedTags =
|
||||
ids.length > 1
|
||||
? []
|
||||
: await db.relations
|
||||
.to({ type: "note", id: ids[0] }, "tag")
|
||||
.resolve();
|
||||
|
||||
for (let i = 0; i < items.placeholders.length; i++) {
|
||||
const item = (await items.item(i)).item;
|
||||
if (item) {
|
||||
if (assignedTags.find((tag) => tag.id === item.id)) {
|
||||
noteTags.push(item);
|
||||
} else {
|
||||
tags.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
tags.splice(0, 0, ...noteTags);
|
||||
setTags(tags);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const refreshTags = useCallback(() => {
|
||||
if (query && query.trim() !== "") {
|
||||
db.lookup
|
||||
.tags(query)
|
||||
.sorted(db.settings.getGroupOptions("tags"))
|
||||
.then((items) => {
|
||||
setTags(items);
|
||||
});
|
||||
.then(sortAndSetTags);
|
||||
} else {
|
||||
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
|
||||
setTags(items);
|
||||
});
|
||||
db.tags.all
|
||||
.sorted(db.settings.getGroupOptions("tags"))
|
||||
.then(sortAndSetTags);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
@@ -241,14 +254,10 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
|
||||
);
|
||||
|
||||
const renderTag = useCallback(
|
||||
({ index }: { item: boolean; index: number }) => (
|
||||
<TagItem
|
||||
tags={tags as VirtualizedGrouping<Tag>}
|
||||
id={index}
|
||||
onPress={onPress}
|
||||
/>
|
||||
({ index, item }: { item: Tag; index: number }) => (
|
||||
<TagItem tag={item} onPress={onPress} />
|
||||
),
|
||||
[onPress, tags]
|
||||
[onPress]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -280,14 +289,11 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(sanitizeTag(v));
|
||||
checkQueryExists(sanitizeTag(v));
|
||||
}}
|
||||
onFocusInput={() => {
|
||||
setFocus(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocus(false);
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setQuery(sanitizeTag(v));
|
||||
checkQueryExists(sanitizeTag(v));
|
||||
}, 300);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
@@ -324,7 +330,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
|
||||
}}
|
||||
>
|
||||
<LegendList
|
||||
data={tags?.placeholders || []}
|
||||
data={tags || []}
|
||||
extraData={tags}
|
||||
keyboardShouldPersistTaps
|
||||
keyboardDismissMode="interactive"
|
||||
@@ -367,16 +373,13 @@ ManageTags.present = (ids?: string[]) => {
|
||||
export default ManageTags;
|
||||
|
||||
const TagItem = ({
|
||||
id,
|
||||
tags,
|
||||
onPress
|
||||
onPress,
|
||||
tag
|
||||
}: {
|
||||
id: string | number;
|
||||
tags: VirtualizedGrouping<Tag>;
|
||||
tag: Tag;
|
||||
onPress: (id: string) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [tag] = useDBItem(id, "tag", tags);
|
||||
const selection = useTagItemSelection((state) =>
|
||||
tag?.id ? state.selection[tag?.id] : false
|
||||
);
|
||||
@@ -414,15 +417,15 @@ const TagItem = ({
|
||||
selection === "selected"
|
||||
? "check-circle-outline"
|
||||
: selection === "intermediate"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
selection === "selected"
|
||||
? "check-circle-outline"
|
||||
: selection === "intermediate"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -242,8 +242,9 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
|
||||
button={{
|
||||
icon: "plus",
|
||||
onPress: async () => {
|
||||
const notebooksFeature =
|
||||
await isFeatureAvailable("notebooks");
|
||||
const notebooksFeature = await isFeatureAvailable(
|
||||
"notebooks"
|
||||
);
|
||||
if (!notebooksFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: notebooksFeature.error,
|
||||
|
||||
@@ -99,7 +99,7 @@ export const NotePreviewConfigure = () => {
|
||||
useEffect(() => {
|
||||
useSettingStore.getState().setDeviceMode("mobile");
|
||||
if (loading) return;
|
||||
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
|
||||
db.notes.all.sorted(db.settings.getGroupOptions("home")).then((notes) => {
|
||||
setItems(notes);
|
||||
});
|
||||
}, [loading]);
|
||||
@@ -141,7 +141,7 @@ export const NotePreviewConfigure = () => {
|
||||
bounceRef.current = setTimeout(() => {
|
||||
if (!value) {
|
||||
db.notes.all
|
||||
.sorted(db.settings.getGroupOptions("notes"))
|
||||
.sorted(db.settings.getGroupOptions("home"))
|
||||
.then((notes) => {
|
||||
setItems(notes);
|
||||
});
|
||||
@@ -149,7 +149,7 @@ export const NotePreviewConfigure = () => {
|
||||
}
|
||||
db.lookup
|
||||
.notes(value)
|
||||
.sorted()
|
||||
.sorted(db.settings.getGroupOptions("home"))
|
||||
.then((notes) => {
|
||||
setItems(notes);
|
||||
});
|
||||
|
||||
@@ -322,8 +322,8 @@ export const MFASetup = ({
|
||||
method?.id === "email"
|
||||
? user?.email
|
||||
: method?.id === "app"
|
||||
? authenticatorDetails?.sharedKey || ""
|
||||
: undefined
|
||||
? authenticatorDetails?.sharedKey || ""
|
||||
: undefined
|
||||
}
|
||||
multiline={method.id === "app"}
|
||||
onChangeText={(value) => {
|
||||
@@ -356,12 +356,12 @@ export const MFASetup = ({
|
||||
sending
|
||||
? null
|
||||
: method.id === "app"
|
||||
? strings.copy()
|
||||
: `${
|
||||
seconds
|
||||
? strings.resendCode(seconds as number)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
? strings.copy()
|
||||
: `${
|
||||
seconds
|
||||
? strings.resendCode(seconds as number)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -75,15 +75,15 @@ export default function DebugLogs() {
|
||||
item.level === LogLevel.Error || item.level === LogLevel.Fatal
|
||||
? hexToRGBA(colors.error.paragraph, 0.2)
|
||||
: item.level === LogLevel.Warn
|
||||
? hexToRGBA(colors.static.orange, 0.2)
|
||||
: "transparent";
|
||||
? hexToRGBA(colors.static.orange, 0.2)
|
||||
: "transparent";
|
||||
|
||||
const color =
|
||||
item.level === LogLevel.Error || item.level === LogLevel.Fatal
|
||||
? colors.error.paragraph
|
||||
: item.level === LogLevel.Warn
|
||||
? colors.static.black
|
||||
: colors.primary.paragraph;
|
||||
? colors.static.black
|
||||
: colors.primary.paragraph;
|
||||
|
||||
return !item ? null : (
|
||||
<TouchableOpacity
|
||||
|
||||
@@ -61,6 +61,7 @@ const Home = ({
|
||||
/>
|
||||
<DelayLayout type="settings">
|
||||
<LegendList
|
||||
testID="settings-list"
|
||||
data={settingsGroups}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
|
||||
@@ -123,8 +123,8 @@ export const TrashIntervalPicker = createSettingsPicker({
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 1
|
||||
? strings.reminderRecurringMode.day()
|
||||
: strings.days(item);
|
||||
? strings.reminderRecurringMode.day()
|
||||
: strings.days(item);
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 1, 7, 30, 365],
|
||||
@@ -276,10 +276,10 @@ export const ApplockTimerPicker = createSettingsPicker({
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 0 || item === undefined
|
||||
? strings.immediately()
|
||||
: item === 1
|
||||
? strings.minutes(1)
|
||||
: strings.minutes(item);
|
||||
? strings.immediately()
|
||||
: item === 1
|
||||
? strings.minutes(1)
|
||||
: strings.minutes(item);
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 0, 1, 5, 15, 30],
|
||||
|
||||
@@ -570,13 +570,17 @@ const BackupItem = ({
|
||||
paragraph: strings.restoreBackupConfirm(),
|
||||
positiveText: strings.restore(),
|
||||
negativeText: strings.cancel(),
|
||||
context: "global",
|
||||
positivePress: async () => {
|
||||
restoreBackup({
|
||||
uri:
|
||||
Platform.OS === "android"
|
||||
? (item as ScopedStorage.FileType).uri
|
||||
: (item as ReactNativeBlobUtilStat).path
|
||||
});
|
||||
setTimeout(() => {
|
||||
restoreBackup({
|
||||
uri:
|
||||
Platform.OS === "android"
|
||||
? (item as ScopedStorage.FileType).uri
|
||||
: (item as ReactNativeBlobUtilStat).path
|
||||
});
|
||||
}, 500);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from "@react-navigation/native";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
|
||||
//@ts-ignore
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
@@ -218,14 +217,15 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
}}
|
||||
>
|
||||
{!!item.icon && (
|
||||
<Icon
|
||||
<AppIcon
|
||||
color={
|
||||
item.type === "danger"
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
iconFamily={item.iconFamily}
|
||||
name={item.icon}
|
||||
size={30}
|
||||
size={item.iconSize || 30}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -53,7 +53,8 @@ import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
openVault,
|
||||
presentSheet
|
||||
presentSheet,
|
||||
VaultRequestType
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Notifications from "../../services/notifications";
|
||||
@@ -62,7 +63,11 @@ import SettingsService from "../../services/settings";
|
||||
import Sync from "../../services/sync";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import {
|
||||
eAfterSync,
|
||||
eCloseSheet,
|
||||
eOpenRecoveryKeyDialog
|
||||
} from "../../utils/events";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { MFARecoveryCodes, MFASheet } from "./2fa";
|
||||
@@ -72,8 +77,50 @@ import { logoutUser } from "./logout";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import { resetTabStore } from "../editor/tiptap/use-tab-store";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { refreshAllStores } from "../../stores/create-db-collection-store";
|
||||
|
||||
export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "account-local",
|
||||
name: strings.account(),
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (current) => !!current,
|
||||
sections: [
|
||||
{
|
||||
id: "delete-data",
|
||||
name: strings.deleteData(),
|
||||
icon: "delete",
|
||||
description: strings.deleteAccountDesc(),
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: strings.deleteData(),
|
||||
paragraph: strings.irreverisibleAction(),
|
||||
positiveType: "errorShade",
|
||||
positiveText: "Delete data",
|
||||
positivePress: async () => {
|
||||
await PremiumService.setPremiumStatus();
|
||||
await BiometricService.resetCredentials();
|
||||
MMKV.clearStore();
|
||||
resetTabStore();
|
||||
clearAllStores();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
SettingsService.resetSettings();
|
||||
db.reset();
|
||||
|
||||
setImmediate(() => {
|
||||
refreshAllStores();
|
||||
eSendEvent(eAfterSync);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "account",
|
||||
name: strings.account(),
|
||||
@@ -159,15 +206,15 @@ export const settingsGroups: SettingSection[] = [
|
||||
return status === SubscriptionStatus.TRIAL
|
||||
? strings.trialOnGoing(trialEndDate)
|
||||
: status === SubscriptionStatus.ACTIVE
|
||||
? strings.subRenewOn(expiryDate)
|
||||
: status === SubscriptionStatus.CANCELED ||
|
||||
status === SubscriptionStatus.PAUSED
|
||||
? strings.subEndsOn(expiryDate)
|
||||
: status === SubscriptionStatus.EXPIRED
|
||||
? subscriptionDaysLeft.time < -3
|
||||
? strings.subEnded()
|
||||
: strings.accountDowngradedIn(3)
|
||||
: strings.neverHesitate();
|
||||
? strings.subRenewOn(expiryDate)
|
||||
: status === SubscriptionStatus.CANCELED ||
|
||||
status === SubscriptionStatus.PAUSED
|
||||
? strings.subEndsOn(expiryDate)
|
||||
: status === SubscriptionStatus.EXPIRED
|
||||
? subscriptionDaysLeft.time < -3
|
||||
? strings.subEnded()
|
||||
: strings.accountDowngradedIn(3)
|
||||
: strings.neverHesitate();
|
||||
}
|
||||
|
||||
return strings.neverHesitate();
|
||||
@@ -286,17 +333,10 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "change-password",
|
||||
name: strings.changePassword(),
|
||||
// type: "screen",
|
||||
type: "screen",
|
||||
description: strings.changePasswordDesc(),
|
||||
// component: "change-password",
|
||||
icon: "form-textbox-password",
|
||||
modifer: () => {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
message:
|
||||
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
|
||||
});
|
||||
}
|
||||
component: "change-password",
|
||||
icon: "form-textbox-password"
|
||||
},
|
||||
{
|
||||
id: "change-email",
|
||||
@@ -929,8 +969,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => (current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
novault: false,
|
||||
title: strings.createVault()
|
||||
requestType: VaultRequestType.CreateVault,
|
||||
title: strings.createVault(),
|
||||
buttonTitle: strings.create()
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -942,9 +983,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () =>
|
||||
openVault({
|
||||
changePassword: true,
|
||||
novault: true,
|
||||
title: strings.changeVaultPassword()
|
||||
requestType: VaultRequestType.ChangePassword,
|
||||
title: strings.changeVaultPassword(),
|
||||
buttonTitle: strings.change()
|
||||
})
|
||||
},
|
||||
{
|
||||
@@ -955,9 +996,10 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
clearVault: true,
|
||||
novault: true,
|
||||
title: strings.clearVault() + "?"
|
||||
requestType: VaultRequestType.ClearVault,
|
||||
title: strings.clearVault() + "?",
|
||||
buttonTitle: strings.clear(),
|
||||
positiveButtonType: "errorShade"
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -969,9 +1011,10 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
deleteVault: true,
|
||||
novault: true,
|
||||
title: strings.deleteVault() + "?"
|
||||
requestType: VaultRequestType.DeleteVault,
|
||||
title: strings.deleteVault() + "?",
|
||||
buttonTitle: strings.delete(),
|
||||
positiveButtonType: "errorShade"
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -989,13 +1032,15 @@ export const settingsGroups: SettingSection[] = [
|
||||
getter: (current) => (current as VaultStatusType)?.biometryEnrolled,
|
||||
modifer: (current) => {
|
||||
const _current = current as VaultStatusType;
|
||||
const isRevoking = _current.biometryEnrolled;
|
||||
openVault({
|
||||
fingerprintAccess: !_current.biometryEnrolled,
|
||||
revokeFingerprintAccess: _current.biometryEnrolled,
|
||||
novault: true,
|
||||
title: _current.biometryEnrolled
|
||||
requestType: isRevoking
|
||||
? VaultRequestType.RevokeFingerprint
|
||||
: VaultRequestType.EnableFingerprint,
|
||||
title: isRevoking
|
||||
? strings.revokeBiometricUnlock()
|
||||
: strings.vaultEnableBiometrics()
|
||||
: strings.vaultEnableBiometrics(),
|
||||
buttonTitle: isRevoking ? strings.revoke() : strings.enable()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1199,7 +1244,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "backup-now",
|
||||
id: "backup-now-with-attachments",
|
||||
name: strings.backupNowWithAttachments(),
|
||||
description: strings.backupNowWithAttachmentsDesc(),
|
||||
hidden: () => !useUserStore.getState().user,
|
||||
@@ -1501,6 +1546,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
id: "join-telegram",
|
||||
name: strings.joinTelegram(),
|
||||
description: strings.joinTelegramDesc(),
|
||||
icon: "sc-telegram",
|
||||
iconFamily: "evilicons",
|
||||
iconSize: 35,
|
||||
modifer: () => {
|
||||
Linking.openURL("https://t.me/notesnook").catch(() => {
|
||||
/* empty */
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { TextInput } from "react-native";
|
||||
import { Settings } from "../../stores/use-setting-store";
|
||||
import { FeatureId } from "@notesnook/common";
|
||||
import { IconProps } from "../../components/ui/AppIcon";
|
||||
|
||||
export type SettingSection = {
|
||||
id: string;
|
||||
@@ -34,6 +35,8 @@ export type SettingSection = {
|
||||
name?: string | ((current?: unknown) => string);
|
||||
description?: string | ((current: unknown) => string);
|
||||
icon?: string;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
iconSize?: number;
|
||||
property?: keyof Settings;
|
||||
sections?: SettingSection[];
|
||||
component?: string;
|
||||
|
||||
@@ -319,9 +319,8 @@ async function run(
|
||||
};
|
||||
}
|
||||
|
||||
await sleep(300);
|
||||
|
||||
if (canShowCompletionStatus) {
|
||||
await sleep(500);
|
||||
presentBackupCompleteSheet(path);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,25 +32,34 @@ import {
|
||||
} from "../utils/events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export enum VaultRequestType {
|
||||
CreateVault = "createVault",
|
||||
LockNote = "lockNote",
|
||||
UnlockNote = "unlockNote",
|
||||
PermanentUnlock = "permanentUnlock",
|
||||
GoToEditor = "goToEditor",
|
||||
ShareNote = "shareNote",
|
||||
CopyNote = "copyNote",
|
||||
DeleteNote = "deleteNote",
|
||||
EnableFingerprint = "enableFingerprint",
|
||||
RevokeFingerprint = "revokeFingerprint",
|
||||
ChangePassword = "changePassword",
|
||||
ClearVault = "clearVault",
|
||||
DeleteVault = "deleteVault",
|
||||
CustomAction = "customAction"
|
||||
}
|
||||
|
||||
export type Vault = {
|
||||
item: Note;
|
||||
novault: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
locked: boolean;
|
||||
permanant: boolean;
|
||||
goToEditor: boolean;
|
||||
share: boolean;
|
||||
deleteNote: boolean;
|
||||
fingerprintAccess: boolean;
|
||||
revokeFingerprintAccess: boolean;
|
||||
changePassword: boolean;
|
||||
clearVault: boolean;
|
||||
deleteVault: boolean;
|
||||
copyNote: boolean;
|
||||
customActionTitle: string;
|
||||
customActionParagraph: string;
|
||||
onUnlock: (
|
||||
item?: Note;
|
||||
requestType: VaultRequestType;
|
||||
title?: string;
|
||||
description?: string;
|
||||
paragraph?: string;
|
||||
buttonTitle?: string;
|
||||
positiveButtonType?: "errorShade" | "transparent" | "accent";
|
||||
customActionTitle?: string;
|
||||
customActionParagraph?: string;
|
||||
onUnlock?: (
|
||||
item: Note & {
|
||||
content?: NoteContent<false>;
|
||||
},
|
||||
@@ -87,7 +96,7 @@ export const eSendEvent = (eventName: string, ...args: any[]) => {
|
||||
eventManager.publish(eventName, ...args);
|
||||
};
|
||||
|
||||
export const openVault = (data: Partial<Vault>) => {
|
||||
export const openVault = (data: Vault) => {
|
||||
eSendEvent(eOpenVaultDialog, data);
|
||||
};
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ const updateAvailableMessage = (
|
||||
icon: "update",
|
||||
type: "normal",
|
||||
id: "app-update"
|
||||
}) as Message;
|
||||
} as Message);
|
||||
|
||||
export function setUpdateAvailableMessage(
|
||||
version: GithubVersionInfo | CheckVersionResponse
|
||||
|
||||
@@ -195,8 +195,8 @@ const NotebookItem = ({
|
||||
!nestedNotebooks?.placeholders.length
|
||||
? "book-outline"
|
||||
: isExpanded
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
@@ -538,10 +538,10 @@ export const Search = ({
|
||||
{searchKeyword
|
||||
? `No results found for "${searchKeyword}"`
|
||||
: mode === "appendNote"
|
||||
? "No notes"
|
||||
: mode === "selectTags"
|
||||
? "No tags"
|
||||
: "No notebooks"}
|
||||
? "No notes"
|
||||
: mode === "selectTags"
|
||||
? "No tags"
|
||||
: "No notebooks"}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import { initDatabase, useShareStore } from "./store";
|
||||
import { isTablet } from "react-native-device-info";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { DefaultAppStyles } from "../utils/styles";
|
||||
import { encodeHTML5 } from "entities";
|
||||
|
||||
const getLinkPreview = (url: string) => {
|
||||
return getPreviewData(url, 5000);
|
||||
@@ -81,7 +82,7 @@ async function sanitizeHtml(site: string, title?: string) {
|
||||
return (
|
||||
html +
|
||||
"<hr/>" +
|
||||
`<p>Clipped from <a href="${site}">${title || site}</a></p>
|
||||
`<p>Clipped from <a href="${site}">${encodeHTML5(title || site)}</a></p>
|
||||
<p>Date clipped ${getFormattedDate(Date.now())}</p>`
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -96,7 +97,7 @@ function makeHtmlFromUrl(url: string) {
|
||||
function makeHtmlFromPlainText(text: string) {
|
||||
if (!text) return "";
|
||||
|
||||
return `<p>${text
|
||||
return `<p>${encodeHTML5(text)
|
||||
.replace(/[\n]+/g, "\n")
|
||||
.replace(/(?:\r\n|\r|\n)/g, "</p><p>")}</p>`;
|
||||
}
|
||||
@@ -419,8 +420,8 @@ const ShareView = () => {
|
||||
let html = !rawData.value
|
||||
? ""
|
||||
: isURL(rawData?.value)
|
||||
? makeHtmlFromUrl(rawData?.value)
|
||||
: makeHtmlFromPlainText(rawData?.value);
|
||||
? makeHtmlFromUrl(rawData?.value)
|
||||
: makeHtmlFromPlainText(rawData?.value);
|
||||
setNote((note) => {
|
||||
note.content.data = html;
|
||||
noteContent.current = html;
|
||||
|
||||
@@ -76,18 +76,14 @@ export const useThemeStore = create<ThemeStore>((set, get) => ({
|
||||
? (Appearance.getColorScheme() as "dark" | "light")
|
||||
: SettingsService.get().colorScheme,
|
||||
setDarkTheme: (darkTheme) => {
|
||||
switchThemeWithAnimation(() => {
|
||||
set({ darkTheme });
|
||||
changeSystemBarColors();
|
||||
SettingsService.setProperty("darkTheme", darkTheme);
|
||||
});
|
||||
set({ darkTheme });
|
||||
changeSystemBarColors();
|
||||
SettingsService.setProperty("darkTheme", darkTheme);
|
||||
},
|
||||
setLightTheme: (lightTheme) => {
|
||||
switchThemeWithAnimation(() => {
|
||||
set({ lightTheme });
|
||||
changeSystemBarColors();
|
||||
SettingsService.setProperty("lighTheme", lightTheme);
|
||||
});
|
||||
set({ lightTheme });
|
||||
changeSystemBarColors();
|
||||
SettingsService.setProperty("lighTheme", lightTheme);
|
||||
},
|
||||
setColorScheme: (colorScheme) => {
|
||||
switchThemeWithAnimation(() => {
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface UserStore {
|
||||
appLocked: boolean;
|
||||
lockApp: (verified: boolean) => void;
|
||||
disableAppLockRequests: boolean;
|
||||
setDisableAppLockRequests: (shouldBlockVerifyUser: boolean) => void;
|
||||
setDisableAppLockRequests: (disableAppLockRequests: boolean) => void;
|
||||
profile?: Partial<Profile>;
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ function show(event: any, text: string, position = 2) {
|
||||
duration: 2000,
|
||||
autoHide: true,
|
||||
clickToHide: true,
|
||||
shadow: true
|
||||
shadow: false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { notesnook } from "../test.ids";
|
||||
import { TestBuilder } from "./utils";
|
||||
|
||||
describe("Sort & filter", () => {
|
||||
it.only("Sort by date-edited/date-created", async () => {
|
||||
it("Sort by date-edited/date-created", async () => {
|
||||
await TestBuilder.create()
|
||||
.prepare()
|
||||
.createNote("Note 1", "Note 1")
|
||||
@@ -34,7 +34,7 @@ describe("Sort & filter", () => {
|
||||
.element(by.web.className("ProseMirror"))
|
||||
.typeText("Edited ", true);
|
||||
})
|
||||
.pressBack(1)
|
||||
.pressBack(2)
|
||||
.waitAndTapById("icon-sort")
|
||||
.wait(500)
|
||||
.waitAndTapByText("Date created")
|
||||
|
||||
@@ -46,9 +46,10 @@ describe("Tags", () => {
|
||||
.waitAndTapByText('Add "#testtag"')
|
||||
.isVisibleByText("#testtag")
|
||||
.pressBack(2)
|
||||
.wait(1000)
|
||||
.openSideMenu()
|
||||
.waitAndTapById("tab-tags")
|
||||
.waitAndTapByText("#testtag")
|
||||
.waitAndTapByText("testtag")
|
||||
.processResult(async (note) => {
|
||||
await TestBuilder.create().isVisibleByText(note.body).run();
|
||||
})
|
||||
|
||||
@@ -32,10 +32,7 @@ const testvars = {
|
||||
|
||||
class Element {
|
||||
element: Detox.NativeElement;
|
||||
constructor(
|
||||
public type: "id" | "text",
|
||||
public value: string
|
||||
) {
|
||||
constructor(public type: "id" | "text", public value: string) {
|
||||
if (type == "id") {
|
||||
this.element = element(by.id(value)).atIndex(0);
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { expect } from "detox";
|
||||
import { notesnook } from "../test.ids";
|
||||
import { TestBuilder, Tests } from "./utils";
|
||||
import { Element, TestBuilder, Tests } from "./utils";
|
||||
|
||||
async function lockNote() {
|
||||
await TestBuilder.create()
|
||||
@@ -67,6 +67,11 @@ async function goToPrivacySecuritySettings() {
|
||||
.waitAndTapById("sidemenu-settings-icon")
|
||||
.wait()
|
||||
.waitAndTapByText("Settings")
|
||||
.addStep(async () => {
|
||||
const element = new Element("id", "settings-list");
|
||||
await element.element.scroll(200, "down");
|
||||
})
|
||||
.wait(500)
|
||||
.waitAndTapByText("Vault")
|
||||
.run();
|
||||
}
|
||||
|
||||
BIN
apps/mobile/fonts/EvilIcons.ttf
Normal file
BIN
apps/mobile/fonts/EvilIcons.ttf
Normal file
Binary file not shown.
@@ -17,9 +17,10 @@ Notifications.init();
|
||||
|
||||
enableFreeze(true);
|
||||
NetInfo.configure({
|
||||
reachabilityUrl: "https://notesnook.com",
|
||||
reachabilityUrl: "https://api.notesnook.com/health",
|
||||
reachabilityTest: (response) => {
|
||||
if (!response) return false;
|
||||
console.log("reachabilty test", response.status);
|
||||
return response?.status >= 200 && response?.status < 300;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
65AA857E25E6DDEC00772A01 /* NotesWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AA857D25E6DDEC00772A01 /* NotesWidget.swift */; };
|
||||
65AA858025E6DDEE00772A01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65AA857F25E6DDEE00772A01 /* Assets.xcassets */; };
|
||||
65AA858425E6DDEE00772A01 /* NotesWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 65AA857725E6DDEC00772A01 /* NotesWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
65AD1F4A2F6289110033176F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 65AD1F492F6289110033176F /* EvilIcons.ttf */; };
|
||||
65AD1F4B2F6289110033176F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 65AD1F492F6289110033176F /* EvilIcons.ttf */; };
|
||||
65B5014425A672B200E2D264 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B5014325A672B200E2D264 /* ShareViewController.m */; };
|
||||
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65B5014525A672B200E2D264 /* MainInterface.storyboard */; };
|
||||
65B5014B25A672B200E2D264 /* Make Note.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 65B5014025A672B200E2D264 /* Make Note.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -129,6 +131,7 @@
|
||||
65AA857D25E6DDEC00772A01 /* NotesWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesWidget.swift; sourceTree = "<group>"; };
|
||||
65AA857F25E6DDEE00772A01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
65AA858125E6DDEE00772A01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
65AD1F492F6289110033176F /* EvilIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = EvilIcons.ttf; path = ../fonts/EvilIcons.ttf; sourceTree = SOURCE_ROOT; };
|
||||
65B5014025A672B200E2D264 /* Make Note.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Make Note.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
65B5014325A672B200E2D264 /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = "<group>"; };
|
||||
65B5014625A672B200E2D264 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
@@ -291,6 +294,7 @@
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
65AD1F492F6289110033176F /* EvilIcons.ttf */,
|
||||
653F4E042EA3887C009A42A2 /* Inter-Bold.ttf */,
|
||||
653F4E052EA3887C009A42A2 /* Inter-Italic.ttf */,
|
||||
653F4E062EA3887C009A42A2 /* Inter-Regular.ttf */,
|
||||
@@ -536,6 +540,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
659201D82EA78265006B16DB /* Inter-Bold.ttf in Resources */,
|
||||
65AD1F4B2F6289110033176F /* EvilIcons.ttf in Resources */,
|
||||
659201D92EA78265006B16DB /* Inter-Italic.ttf in Resources */,
|
||||
659201DA2EA78265006B16DB /* Inter-Regular.ttf in Resources */,
|
||||
659201DB2EA78265006B16DB /* Inter-SemiBold.ttf in Resources */,
|
||||
@@ -581,6 +586,7 @@
|
||||
659201DE2EA7826D006B16DB /* Inter-Regular.ttf in Resources */,
|
||||
659201DF2EA7826D006B16DB /* Inter-SemiBold.ttf in Resources */,
|
||||
65C400DF2A80B6B600AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
|
||||
65AD1F4A2F6289110033176F /* EvilIcons.ttf in Resources */,
|
||||
65C149872A61151B005C40F1 /* extension.bundle in Resources */,
|
||||
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */,
|
||||
);
|
||||
@@ -1029,7 +1035,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
CURRENT_PROJECT_VERSION = 2178;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1104,7 +1110,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
MARKETING_VERSION = 3.3.18;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1135,7 +1141,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
CURRENT_PROJECT_VERSION = 2178;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1210,7 +1216,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
MARKETING_VERSION = 3.3.18;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1367,7 +1373,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
CURRENT_PROJECT_VERSION = 2178;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1379,7 +1385,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
MARKETING_VERSION = 3.3.18;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1410,7 +1416,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
CURRENT_PROJECT_VERSION = 2178;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1423,7 +1429,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
MARKETING_VERSION = 3.3.18;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1453,7 +1459,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
CURRENT_PROJECT_VERSION = 2178;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1534,7 +1540,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
MARKETING_VERSION = 3.3.18;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1565,7 +1571,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
CURRENT_PROJECT_VERSION = 2178;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1647,7 +1653,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
MARKETING_VERSION = 3.3.18;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1724,10 +1730,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -1792,10 +1795,7 @@
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
<string>Inter-Regular.ttf</string>
|
||||
<string>Inter-Bold.ttf</string>
|
||||
<string>MaterialCommunityIcons.ttf</string>
|
||||
<string>EvilIcons.ttf</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
|
||||
31
apps/mobile/package-lock.json
generated
31
apps/mobile/package-lock.json
generated
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.13-beta.1",
|
||||
"version": "3.3.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.13-beta.1",
|
||||
"version": "3.3.17",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@ammarahmed/notifee-react-native": "^7.4.11",
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.2",
|
||||
"@ammarahmed/notifee-react-native": "^7.4.12",
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.3",
|
||||
"@ammarahmed/react-native-eventsource": "1.1.0",
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.5",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.8",
|
||||
"@ammarahmed/react-native-upload": "^6.31.0",
|
||||
"@ammarahmed/react-native-upload": "^6.33.0",
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@callstack/repack": "~5.2.1",
|
||||
@@ -181,7 +181,6 @@
|
||||
"otplib": "12.0.1",
|
||||
"patch-package": "7.0.0",
|
||||
"pixelmatch": "^6.0.0",
|
||||
"prettier": "3.5.3",
|
||||
"react-native-bundle-visualizer": "^3.1.1",
|
||||
"react-native-cli-bump-version": "1.5.0",
|
||||
"react-refresh": "0.14.0",
|
||||
@@ -300,7 +299,7 @@
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
"@notesnook/ui": "file:../ui",
|
||||
"@social-embed/lib": "^0.1.0-next.7",
|
||||
"@social-embed/lib": "^0.1.0-next.11",
|
||||
"@tiptap/core": "2.6.6",
|
||||
"@tiptap/extension-blockquote": "^2.6.6",
|
||||
"@tiptap/extension-bullet-list": "^2.6.6",
|
||||
@@ -490,18 +489,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ammarahmed/notifee-react-native": {
|
||||
"version": "7.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/notifee-react-native/-/notifee-react-native-7.4.11.tgz",
|
||||
"integrity": "sha512-9zKhONgir2FN/sMj/rsKAhDwfxadDuxB6LJILKxpGwzqvcIuG70oCKWmBBG9LJJpSfcVpNTdxBoH9RtNZTZSsQ==",
|
||||
"version": "7.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/notifee-react-native/-/notifee-react-native-7.4.12.tgz",
|
||||
"integrity": "sha512-rZPtHdWZ3PxtMHreVszo0ehw6gnsabw7fpgDv8ztYcMNj4A22ZTyfTAQrSZ534D7iDkWgHi2my82b31MsnlQzQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@ammarahmed/react-native-background-fetch": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-background-fetch/-/react-native-background-fetch-4.2.2.tgz",
|
||||
"integrity": "sha512-PLBlb/DD+6Fv1auxdD6VRxtuC7GKPhQqR69bovRm5oGavbdSzRr+ZTr8AyNMCcayxKGdgEX2JysrJg8WIySSFQ==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-background-fetch/-/react-native-background-fetch-4.2.3.tgz",
|
||||
"integrity": "sha512-cgO9EEFoU1HYuDNMpdTTke7DYzEDQfLsyd/cWIZ5Lpp4zz76+dgp1+LBqbLYdSenhJU1Omghq3xWPsoWDRaemQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ammarahmed/react-native-eventsource": {
|
||||
@@ -538,9 +537,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@ammarahmed/react-native-upload": {
|
||||
"version": "6.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-upload/-/react-native-upload-6.31.0.tgz",
|
||||
"integrity": "sha512-GwMDd2IND3nuS4l13pQPP8zA/GJoxQKz0pxCGUoWj65fbPuZPnuUA2Rpf4jZzLlgY62vdjI/TqmX+rPiwwCceQ==",
|
||||
"version": "6.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-upload/-/react-native-upload-6.33.0.tgz",
|
||||
"integrity": "sha512-hvr/LN1wgbhZv3WsRhrAPC1D8YW0ClgrE5NArqUgHWreSHBCS2zVQhEferTt5fGESaJvx+c69VqaIPqzzb8Y3A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.13",
|
||||
"version": "3.3.18",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package && node ./scripts/optimize-fonts.mjs",
|
||||
"run-android": "react-native run-android --active-arch-only",
|
||||
"run-android": "react-native run-android --active-arch-only --mode=release",
|
||||
"build": "echo 'No build script defined'",
|
||||
"run-ios": "react-native run-ios",
|
||||
"start": "react-native start",
|
||||
@@ -26,13 +26,13 @@
|
||||
"react": "19.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ammarahmed/notifee-react-native": "^7.4.11",
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.2",
|
||||
"@ammarahmed/notifee-react-native": "^7.4.12",
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.3",
|
||||
"@ammarahmed/react-native-eventsource": "1.1.0",
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.5",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.8",
|
||||
"@ammarahmed/react-native-upload": "^6.31.0",
|
||||
"@ammarahmed/react-native-upload": "^6.33.0",
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@callstack/repack": "~5.2.1",
|
||||
@@ -197,7 +197,6 @@
|
||||
"otplib": "12.0.1",
|
||||
"patch-package": "7.0.0",
|
||||
"pixelmatch": "^6.0.0",
|
||||
"prettier": "3.5.3",
|
||||
"react-native-bundle-visualizer": "^3.1.1",
|
||||
"react-native-cli-bump-version": "1.5.0",
|
||||
"react-refresh": "0.14.0",
|
||||
@@ -208,4 +207,4 @@
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
diff --git a/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java b/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java
|
||||
index f4630ac..a3a837f 100644
|
||||
--- a/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java
|
||||
+++ b/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java
|
||||
@@ -13,7 +13,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
-public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
|
||||
+public class ThemeSwitchAnimationModule extends com.themeswitchanimation.ThemeSwitchAnimationModuleSpec {
|
||||
public static final String NAME = "ThemeSwitchAnimationModule";
|
||||
|
||||
private ReactContext reactContext;
|
||||
@@ -38,6 +38,12 @@ public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
|
||||
this.isAnimating = true;
|
||||
this.rootView = (ViewGroup) getCurrentActivity().getWindow().getDecorView();
|
||||
this.capturedImageBitmap = captureScreenshot(this.rootView, this.reactContext);
|
||||
+ if (this.capturedImageBitmap == null) {
|
||||
+ reactContext
|
||||
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
+ .emit("FINISHED_FREEZING_SCREEN", null);
|
||||
+ return;
|
||||
+ }
|
||||
this.capturedImageView = createImageView(this.capturedImageBitmap, this.reactContext);
|
||||
|
||||
reactContext.runOnUiQueueThread(() -> {
|
||||
@@ -57,6 +63,9 @@ public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isAnimating) {
|
||||
+ if (capturedImageView == null) {
|
||||
+ cleanUp();
|
||||
+ }
|
||||
switch (animationType) {
|
||||
case "circular":
|
||||
Animations.performCircleAnimation(capturedImageView, rootView, (int) duration, cxRatio, cyRatio, reactContext, new Runnable() {
|
||||
@@ -94,16 +103,22 @@ public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
|
||||
capturedImageBitmap.recycle();
|
||||
capturedImageBitmap = null;
|
||||
}
|
||||
- rootView.removeView(capturedImageView);
|
||||
+ if (capturedImageView != null) {
|
||||
+ rootView.removeView(capturedImageView);
|
||||
+ }
|
||||
isAnimating = false;
|
||||
}
|
||||
|
||||
public static Bitmap captureScreenshot(View rootView, ReactContext reactContext) {
|
||||
- Bitmap capturedImageBitmap = Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
- Canvas canvas = new Canvas(capturedImageBitmap);
|
||||
- rootView.draw(canvas);
|
||||
-
|
||||
- return capturedImageBitmap;
|
||||
+ try {
|
||||
+ Bitmap capturedImageBitmap = Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
+ Canvas canvas = new Canvas(capturedImageBitmap);
|
||||
+ rootView.draw(canvas);
|
||||
+
|
||||
+ return capturedImageBitmap;
|
||||
+ } catch (Exception e) {
|
||||
+ return null;
|
||||
+ }
|
||||
}
|
||||
|
||||
public static ImageView createImageView(Bitmap capturedImageBitmap, ReactContext reactContext) {
|
||||
@@ -89,6 +89,50 @@ index 978045f..59c64f1 100644
|
||||
- implementation 'com.github.florent37:viewtooltip:1.1.6'
|
||||
+ implementation 'com.github.florent37:ViewTooltip:f79a8955ef'
|
||||
}
|
||||
diff --git a/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java b/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java
|
||||
index e987ffb..e898a40 100644
|
||||
--- a/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java
|
||||
+++ b/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java
|
||||
@@ -11,9 +11,11 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
-import com.facebook.react.uimanager.UIManagerModule;
|
||||
+import com.facebook.react.bridge.UIManager;
|
||||
import com.facebook.react.uimanager.UIBlock;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyManager;
|
||||
+import com.facebook.react.uimanager.UIManagerHelper;
|
||||
+import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.github.florent37.viewtooltip.ViewTooltip;
|
||||
|
||||
public class RNTooltipsModule extends ReactContextBaseJavaModule {
|
||||
@@ -34,12 +36,12 @@ public class RNTooltipsModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@ReactMethod
|
||||
public void Show(final int targetId, final int parentId, final ReadableMap props, final Callback onHide) {
|
||||
- this.reactContext.getNativeModule(UIManagerModule.class).prependUIBlock(new UIBlock() {
|
||||
|
||||
- @Override
|
||||
- public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
|
||||
|
||||
- final ViewGroup target = (ViewGroup) nativeViewHierarchyManager.resolveView(targetId);
|
||||
+ UIManager uiManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC);
|
||||
+ if (uiManager == null) return;
|
||||
+
|
||||
+ final ViewGroup target = (ViewGroup) uiManager.resolveView(targetId);
|
||||
|
||||
reactContext.runOnUiQueueThread(new Runnable() {
|
||||
@Override
|
||||
@@ -121,8 +123,7 @@ public class RNTooltipsModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
});
|
||||
- }
|
||||
- });
|
||||
+
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
diff --git a/node_modules/react-native-tooltips/ios/RNTooltips.h b/node_modules/react-native-tooltips/ios/RNTooltips.h
|
||||
index a150e3e..096ad86 100644
|
||||
--- a/node_modules/react-native-tooltips/ios/RNTooltips.h
|
||||
@@ -116,7 +160,7 @@ index fb96466..75a6fdb 100644
|
||||
|
||||
@interface TooltipDelegate : NSObject <SexyTooltipDelegate>
|
||||
|
||||
@@ -7,9 +9,15 @@
|
||||
@@ -7,9 +9,15 @@ @interface TooltipDelegate : NSObject <SexyTooltipDelegate>
|
||||
@end
|
||||
|
||||
@implementation TooltipDelegate
|
||||
@@ -132,7 +176,7 @@ index fb96466..75a6fdb 100644
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -17,77 +25,120 @@
|
||||
@@ -17,77 +25,120 @@ - (void)tooltipDidDismiss:(SexyTooltip *)tooltip {
|
||||
|
||||
@implementation RNTooltips
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user