mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
1 Commits
fix/sync-q
...
fix-loadin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
735ab21ee9 |
@@ -21,7 +21,4 @@ packages/editor/styles/
|
||||
packages/editor/languages/
|
||||
|
||||
# editor mobile
|
||||
packages/editor-mobile/build.bundle
|
||||
|
||||
# snapshots
|
||||
tap-snapshots
|
||||
packages/editor-mobile/build.bundle
|
||||
4
.github/dco.yml
vendored
4
.github/dco.yml
vendored
@@ -1,4 +0,0 @@
|
||||
allowRemediationCommits:
|
||||
individual: true
|
||||
require:
|
||||
members: false
|
||||
64
.github/workflows/android.publish.internal.yml
vendored
64
.github/workflows/android.publish.internal.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: Publish @notesnook/android-internal
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
architecture: 'x64'
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: internal
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
26
.github/workflows/android.publish.yml
vendored
26
.github/workflows/android.publish.yml
vendored
@@ -50,18 +50,7 @@ jobs:
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1.1.1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
@@ -102,4 +91,15 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
12
.github/workflows/desktop.publish.yml
vendored
12
.github/workflows/desktop.publish.yml
vendored
@@ -127,9 +127,7 @@ jobs:
|
||||
path: ./apps/web/desktop/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i
|
||||
npm i dmg-license
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
@@ -215,17 +213,17 @@ jobs:
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
npx electron-builder --linux snap:x64 -p never
|
||||
npx electron-builder --linux snap -p never
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
- name: Build AppImage deb and rpm
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
npx electron-builder --linux AppImage deb rpm -p always
|
||||
else
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
npx electron-builder --linux AppImage deb rpm -p never
|
||||
fi
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
|
||||
57
.github/workflows/editor.tests.yml
vendored
57
.github/workflows/editor.tests.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: Test @notesnook/editor
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/editor/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
- "synchronize"
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build editor
|
||||
run: npx nx build @notesnook/editor
|
||||
|
||||
- name: Run all @notesnook/editor tests
|
||||
run: npx nx test @notesnook/editor
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,5 +6,4 @@ dist
|
||||
nx-cloud.env
|
||||
.idea
|
||||
.eslintcache
|
||||
.env.local
|
||||
.nyc_output
|
||||
.env.local
|
||||
11
.vscode/extensions.json
vendored
11
.vscode/extensions.json
vendored
@@ -1,11 +0,0 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-playwright.playwright",
|
||||
"firefox-devtools.vscode-firefox-debug",
|
||||
"msjsdiag.vscode-react-native"
|
||||
]
|
||||
}
|
||||
72
.vscode/notesnook.code-workspace
vendored
72
.vscode/notesnook.code-workspace
vendored
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "✨ notesnook",
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"name": "🚀 @notesnook/mobile",
|
||||
"path": "../apps/mobile"
|
||||
},
|
||||
{
|
||||
"name": "🚀 @notesnook/web",
|
||||
"path": "../apps/web"
|
||||
},
|
||||
{
|
||||
"name": "🚀 @notesnook/desktop",
|
||||
"path": "../apps/web/desktop"
|
||||
},
|
||||
{
|
||||
"name": "🚀 @notesnook/web-clipper",
|
||||
"path": "../extensions/web-clipper"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/clipper",
|
||||
"path": "../packages/clipper"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/core",
|
||||
"path": "../packages/core"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/crypto",
|
||||
"path": "../packages/crypto"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/crypto-worker",
|
||||
"path": "../packages/crypto-worker"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/editor",
|
||||
"path": "../packages/editor"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/editor-mobile",
|
||||
"path": "../packages/editor-mobile"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/logger",
|
||||
"path": "../packages/logger"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/streamable-fs",
|
||||
"path": "../packages/streamable-fs"
|
||||
},
|
||||
{
|
||||
"name": "📦 @notesnook/theme",
|
||||
"path": "../packages/theme"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"javascript.format.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modificationsIfAvailable",
|
||||
"eslint.packageManager": "npm",
|
||||
"eslint.run": "onSave",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"eslint.execArgv": ["--cache"]
|
||||
}
|
||||
}
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
|
||||
|
||||
Ugh! Bugs!
|
||||
|
||||
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
|
||||
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
|
||||
|
||||
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
|
||||
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
|
||||
|
||||
@@ -28,9 +28,15 @@ If you've thought of a way that Notesnook could be better, we want to hear about
|
||||
|
||||
Before you open a new feature request, please make sure it's not a duplicate. **Duplicate feature requests & bug reports are closed immediately.**
|
||||
|
||||
### Improving documentation
|
||||
|
||||
All app related documentation can be found in the `docs/` directory. All the files in this directory are simple markdown files which you can edit either directly on GitHub, or in your favorite text editor.
|
||||
|
||||
If you find a mistake in the documentation or would like to contribute something new to the documentation, feel free to open a pull request; creating an issue for this is not required.
|
||||
|
||||
### Helping out in the issue tracker
|
||||
|
||||
New issues are always opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
There are always new issues getting opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
|
||||
In addition to this, you can help out in the following ways:
|
||||
|
||||
@@ -62,7 +68,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
|
||||
|
||||
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
|
||||
2. Run `npm run bootstrap` in the repository root.
|
||||
3. If you've fixed a bug or added code that should be tested, add tests!
|
||||
3. If you’ve fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`npm run test`).
|
||||
5. Format your code with prettier (`npm run prettier`).
|
||||
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
|
||||
@@ -85,11 +91,7 @@ The DCO is a declaration attached to every contribution made by every developer
|
||||
Signed-off-by: Jane Smith <jane.smith@email.com>
|
||||
```
|
||||
|
||||
On VSCode, it looks like this:
|
||||
|
||||

|
||||
|
||||
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message automatically.
|
||||
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message.
|
||||
|
||||
#### Include `<scope>:` in your commit message
|
||||
|
||||
@@ -131,11 +133,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
|
||||
|
||||
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
|
||||
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb’s Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
|
||||
## Git Branch Organization
|
||||
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don’t use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
|
||||
1. All tests must be passing at all times
|
||||
2. There should be as few breaking changes as possible
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
|
||||
|
||||
## Developer guide
|
||||
|
||||
|
||||
2
apps/mobile/.vscode/launch.json
vendored
2
apps/mobile/.vscode/launch.json
vendored
@@ -25,4 +25,4 @@
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
apps/mobile/.vscode/settings.json
vendored
Normal file
1
apps/mobile/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
## Build instructions
|
||||
|
||||
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
|
||||
### Setting up the development environment
|
||||
### Setting up development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
@@ -57,7 +57,7 @@ npm install
|
||||
|
||||
### Running the app on Android
|
||||
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
|
||||
|
||||
```bash
|
||||
npm run start:android
|
||||
@@ -78,11 +78,11 @@ npm run start:ios
|
||||
|
||||
## Developer guide
|
||||
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible:
|
||||
We try to keep the stack as lean as possible
|
||||
|
||||
1. React Native
|
||||
2. Typescript/Javascript
|
||||
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible:
|
||||
|
||||
The app codebase is distributed over two primary directories. `native/` and `app/`.
|
||||
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
|
||||
|
||||
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
|
||||
- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
|
||||
- `hooks/`: Hooks for different app logic
|
||||
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `screens`: Navigator screens.
|
||||
- `services`: Parts of code that do a specific function. For example, the `sync` service 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.
|
||||
- `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions etc.
|
||||
|
||||
There are several other folders at the root:
|
||||
|
||||
@@ -115,11 +115,11 @@ There are several other folders at the root:
|
||||
|
||||
### Running the tests
|
||||
|
||||
When you are done making the required changes, you 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:
|
||||
When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
|
||||
### Android
|
||||
|
||||
To run the tests on Android, you will need to create an emulator device on your system:
|
||||
To run the tests on android, you will need to create an emulator device on your system:
|
||||
|
||||
```
|
||||
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
|
||||
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
|
||||
|
||||
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
|
||||
|
||||
Once you have created an emulator device, build the Android apks:
|
||||
Once you have created an emulator device, build the android apks
|
||||
|
||||
```
|
||||
npm run build:android
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
Finally run the tests
|
||||
|
||||
```
|
||||
npm run test:android
|
||||
@@ -141,9 +141,9 @@ npm run test:android
|
||||
|
||||
### iOS
|
||||
|
||||
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
|
||||
To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
|
||||
|
||||
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
|
||||
First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
|
||||
|
||||
```
|
||||
brew tap wix/brew
|
||||
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
|
||||
npm run build:ios
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
Finally run the tests:
|
||||
|
||||
```
|
||||
npm run test:ios
|
||||
|
||||
@@ -16,7 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "react-native-gesture-handler";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -32,6 +32,7 @@ import { useUserStore } from "./stores/use-user-store";
|
||||
import { View } from "react-native";
|
||||
import { useState } from "react";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
|
||||
NetInfo.configure({
|
||||
reachabilityUrl: "https://notesnook.com",
|
||||
reachabilityTest: (response) => {
|
||||
@@ -50,7 +51,6 @@ const App = () => {
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
useUserStore.getState().setVerifyUser(true);
|
||||
}
|
||||
//BackgroundSync.start();
|
||||
setInit(true);
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -68,3 +68,14 @@ db.host(
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
export async function loadDatabase() {
|
||||
// if (!DB) {
|
||||
// let module = await import(/* webpackChunkName: "notes-core" */ 'notes-core/api/index');
|
||||
// DB = module.default;
|
||||
// }
|
||||
// db = new DB(Storage, Platform.OS === 'ios' ? EventSource : AndroidEventSource, filesystem);
|
||||
// if (DOMParser) {
|
||||
// await DOMParser.prepare();
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Dimensions, Platform } from "react-native";
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
* @returns
|
||||
*/
|
||||
export async function compressToBase64(path, type) {
|
||||
const { width, scale } = Dimensions.get("window");
|
||||
const response = await ImageResizer.createResizedImage(
|
||||
path,
|
||||
width * scale,
|
||||
9999,
|
||||
type,
|
||||
80,
|
||||
0,
|
||||
undefined,
|
||||
true,
|
||||
{
|
||||
mode: "contain",
|
||||
onlyScaleDown: true
|
||||
}
|
||||
);
|
||||
const base64 = await RNFetchBlob.fs.readFile(
|
||||
Platform.OS === "ios" ? response.uri?.replace("file://", "") : response.uri,
|
||||
"base64"
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
|
||||
return base64;
|
||||
}
|
||||
@@ -28,16 +28,8 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir } from "./utils";
|
||||
import { getFileNameWithExtension } from "@notesnook/core/utils/filename";
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hash,
|
||||
global = true,
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false
|
||||
}
|
||||
) {
|
||||
export default async function downloadAttachment(hash, global = true) {
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
@@ -45,13 +37,11 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
let folder = {};
|
||||
if (!options.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -64,11 +54,6 @@ export default async function downloadAttachment(
|
||||
)
|
||||
return;
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.metadata.filename,
|
||||
attachment.metadata.type
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
@@ -78,50 +63,46 @@ export default async function downloadAttachment(
|
||||
hash: attachment.metadata.hash,
|
||||
hashType: attachment.metadata.hashType,
|
||||
mime: attachment.metadata.type,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
fileName: attachment.metadata.filename,
|
||||
uri: folder.uri,
|
||||
chunkSize: attachment.chunkSize
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
options.cache ? "cache" : "file"
|
||||
);
|
||||
let fileUri = await Sodium.decryptFile(key, info, "file");
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: attachment.metadata.filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (!options.silent) {
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!attachment.metadata?.type?.startsWith("image")
|
||||
) {
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
if (!options.silent) {
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
fileUri = folder.uri + `/${attachment.metadata.filename}`;
|
||||
}
|
||||
console.log("saved file uri: ", fileUri);
|
||||
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${attachment.metadata.filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
component: (
|
||||
<ShareComponent
|
||||
uri={fileUri}
|
||||
name={attachment.metadata.filename}
|
||||
padding={12}
|
||||
/>
|
||||
)
|
||||
});
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
console.log("download attachment error: ", e);
|
||||
|
||||
@@ -23,16 +23,14 @@ import {
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64
|
||||
writeEncrypted
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
writeEncrypted,
|
||||
uploadFile: cancelable(uploadFile),
|
||||
downloadFile: cancelable(downloadFile),
|
||||
deleteFile,
|
||||
|
||||
@@ -21,8 +21,6 @@ import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { cacheDir, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
@@ -31,9 +29,6 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
@@ -41,20 +36,8 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
...cipherData,
|
||||
hash: filename
|
||||
},
|
||||
cipherData.outputType === "base64"
|
||||
? isPng || isJpeg
|
||||
? "cache"
|
||||
: "base64"
|
||||
: "text"
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
);
|
||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||
const dCachePath = `${cacheDir}/${output}`;
|
||||
output = await compressToBase64(
|
||||
`file://${dCachePath}`,
|
||||
isPng ? "PNG" : "JPEG"
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
@@ -63,26 +46,17 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
uri: ""
|
||||
});
|
||||
return {
|
||||
hash: hash,
|
||||
type: "xxh64"
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64({ data, key }) {
|
||||
export async function writeEncrypted(filename, { data, type, key }) {
|
||||
console.log("file input: ", { type, key });
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
console.log(filepath);
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
|
||||
console.log("encrypted file output: ", output);
|
||||
return {
|
||||
...output,
|
||||
|
||||
@@ -36,11 +36,7 @@ function getFileExtension(filename) {
|
||||
var ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
@@ -50,6 +46,7 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
const encryptionProgress = useAttachmentStore(
|
||||
(state) => state.encryptionProgress
|
||||
);
|
||||
|
||||
const onPress = () => {
|
||||
Actions.present(attachment, setAttachments, attachment.metadata.hash);
|
||||
};
|
||||
@@ -125,9 +122,7 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentProgress ||
|
||||
(encryptionProgress && encryptionProgress !== "0.00") ||
|
||||
encryption ? (
|
||||
{currentProgress || encryptionProgress || encryption ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
|
||||
@@ -17,33 +17,70 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import {
|
||||
eCloseAttachmentDialog,
|
||||
eOpenAttachmentsDialog
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Toast } from "../toast";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import SheetWrapper from "../ui/sheet";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
|
||||
export const AttachmentDialog = ({ data }) => {
|
||||
export const AttachmentDialog = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [note, setNote] = useState(data);
|
||||
const [attachments, setAttachments] = useState(
|
||||
data
|
||||
? db.attachments.ofNote(data.id, "all")
|
||||
: [...(db.attachments.all || [])]
|
||||
);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
const actionSheetRef = useRef();
|
||||
const [attachments, setAttachments] = useState([]);
|
||||
const attachmentSearchValue = useRef();
|
||||
const searchTimer = useRef();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenAttachmentsDialog, open);
|
||||
eSubscribeEvent(eCloseAttachmentDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
|
||||
eUnSubscribeEvent(eCloseAttachmentDialog, close);
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
const open = (data) => {
|
||||
if (data?.id) {
|
||||
setNote(data);
|
||||
let _attachments = db.attachments.ofNote(data.id, "all");
|
||||
setAttachments(_attachments);
|
||||
} else {
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
actionSheetRef.current?.show();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const close = () => {
|
||||
actionSheetRef.current?.hide();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onChangeText = (text) => {
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
@@ -67,101 +104,107 @@ export const AttachmentDialog = ({ data }) => {
|
||||
<AttachmentItem setAttachments={setAttachments} attachment={item} />
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
onClose={async () => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={note ? "Attachments" : "Manage attachments"}
|
||||
paragraph="Tap on an attachment to view properties"
|
||||
button={{
|
||||
title: "Check all",
|
||||
type: "grayAccent",
|
||||
loading: loading,
|
||||
onPress: async () => {
|
||||
setLoading(true);
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
{!note ? (
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={10}
|
||||
initialNumToRender={10}
|
||||
windowSize={5}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
data={attachments}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
<DialogHeader
|
||||
title={note ? "Attachments" : "Manage attachments"}
|
||||
paragraph="Tap on an attachment to view properties"
|
||||
button={{
|
||||
title: "Check all",
|
||||
type: "grayAccent",
|
||||
loading: loading,
|
||||
onPress: async () => {
|
||||
setLoading(true);
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
{!note ? (
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
nestedScrollEnabled
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
data={attachments}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog data={note} />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
@@ -82,6 +83,9 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
{loading ? (
|
||||
<BaseDialog transparent={true} visible={true} animation="fade" />
|
||||
) : null}
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
@@ -130,7 +134,6 @@ export const Login = ({ changeMode }) => {
|
||||
marginTop: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
@@ -169,7 +172,7 @@ export const Login = ({ changeMode }) => {
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
editable={step === LoginSteps.emailAuth}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
@@ -191,7 +194,6 @@ export const Login = ({ changeMode }) => {
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
@@ -203,7 +205,6 @@ export const Login = ({ changeMode }) => {
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
@@ -227,10 +228,7 @@ export const Login = ({ changeMode }) => {
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
onPress={login}
|
||||
type="accent"
|
||||
title={
|
||||
loading
|
||||
@@ -250,7 +248,6 @@ export const Login = ({ changeMode }) => {
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
|
||||
@@ -44,10 +44,8 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
const [username, provider] = email.split("@");
|
||||
if (username.length === 1) return `****@${provider}`;
|
||||
return email.replace(/(.{1})(.*)(?=@)/, function (gp1, gp2, gp3) {
|
||||
if (!email) return null;
|
||||
return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) {
|
||||
for (let i = 0; i < gp3.length; i++) {
|
||||
gp2 += "*";
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import umami from "../../common/analytics";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
@@ -30,6 +31,7 @@ import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { SvgView } from "../ui/svg";
|
||||
@@ -77,6 +79,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
hideAuth();
|
||||
umami.pageView("/account-created", "/welcome/signup");
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
@@ -96,6 +99,9 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<BaseDialog transparent={true} visible={true} animation="fade" />
|
||||
) : null}
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
@@ -144,7 +150,6 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
textAlign: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
@@ -232,10 +237,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
signup();
|
||||
}}
|
||||
onPress={signup}
|
||||
type="accent"
|
||||
title={loading ? null : "Agree and continue"}
|
||||
/>
|
||||
|
||||
@@ -54,7 +54,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
app: "Enter the 6 digit code from your authenticator app to continue logging in",
|
||||
sms: "Enter the 6 digit code sent to your phone number to continue logging in",
|
||||
email: "Enter the 6 digit code sent to your email to continue logging in",
|
||||
recoveryCode: "Enter the recovery code to continue logging in"
|
||||
recoveryCode: "Enter the 8 digit recovery code to continue logging in"
|
||||
};
|
||||
|
||||
const secondaryMethodsText = {
|
||||
@@ -65,7 +65,9 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
};
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6) return;
|
||||
const length = currentMethod.method === "recoveryCode" ? 8 : 6;
|
||||
|
||||
if (!code.current || code.current.length !== length) return;
|
||||
setLoading(true);
|
||||
inputRef.current?.blur();
|
||||
await onMfaLogin(
|
||||
@@ -200,26 +202,22 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
<>
|
||||
<Input
|
||||
placeholder={
|
||||
currentMethod.method === "recoveryCode"
|
||||
? "xxxxx-xxxxx"
|
||||
: "xxxxxx"
|
||||
currentMethod.method === "recoveryCode" ? "xxxxxxxx" : "xxxxxx"
|
||||
}
|
||||
testID={"input.totp"}
|
||||
maxLength={
|
||||
currentMethod.method === "recoveryCode" ? undefined : 6
|
||||
}
|
||||
maxLength={currentMethod.method === "recoveryCode" ? 8 : 6}
|
||||
fwdRef={inputRef}
|
||||
textAlign="center"
|
||||
onChangeText={(value) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
onNext();
|
||||
}}
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
height: 60,
|
||||
textAlign: "center",
|
||||
letterSpacing: 10,
|
||||
width: 250
|
||||
width: null
|
||||
}}
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
@@ -302,9 +300,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
|
||||
TwoFactorVerification.present = (onMfaLogin, data, context) => {
|
||||
presentSheet({
|
||||
component: () => (
|
||||
<TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />
|
||||
),
|
||||
component: <TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />,
|
||||
context: context || "two_factor_verify",
|
||||
onClose: () => {
|
||||
onMfaLogin();
|
||||
|
||||
@@ -75,7 +75,6 @@ export const useLogin = (onFinishLogin) => {
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
@@ -94,8 +93,6 @@ export const useLogin = (onFinishLogin) => {
|
||||
}
|
||||
}
|
||||
}, mfaInfo);
|
||||
} else {
|
||||
finishWithError(new Error("Unable to send 2FA code"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { KeyboardAvoidingView, Platform, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { KeyboardAvoidingView, Platform, SafeAreaView } from "react-native";
|
||||
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { Header } from "../header";
|
||||
@@ -29,7 +28,6 @@ export const Container = ({ children }) => {
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
@@ -38,12 +36,10 @@ export const Container = ({ children }) => {
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{!introCompleted ? null : (
|
||||
@@ -54,7 +50,7 @@ export const Container = ({ children }) => {
|
||||
)}
|
||||
|
||||
{children}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import React from "react";
|
||||
import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { AnnouncementDialog } from "../announcements";
|
||||
import { AttachmentDialog } from "../attachments";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -33,6 +34,10 @@ import MergeConflicts from "../merge-conflicts";
|
||||
import PremiumDialog from "../premium";
|
||||
import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { AddNotebookSheet } from "../sheets/add-notebook";
|
||||
import AddToNotebookSheet from "../sheets/add-to";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import PublishNoteSheet from "../sheets/publish-note";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
@@ -46,6 +51,7 @@ const DialogProvider = () => {
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<AddTopicDialog colors={colors} />
|
||||
<AddNotebookSheet colors={colors} />
|
||||
<PremiumDialog colors={colors} />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
@@ -55,8 +61,12 @@ const DialogProvider = () => {
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<AddToNotebookSheet colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<PublishNoteSheet />
|
||||
<ManageTagsSheet />
|
||||
<AttachmentDialog />
|
||||
{loading ? null : <Expiring />}
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Text, View, ViewStyle } from "react-native";
|
||||
import { Text, View } from "react-native";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
@@ -41,7 +41,6 @@ type DialogHeaderProps = {
|
||||
padding?: number;
|
||||
centered?: boolean;
|
||||
titlePart?: string;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
const DialogHeader = ({
|
||||
@@ -51,8 +50,7 @@ const DialogHeader = ({
|
||||
paragraphColor,
|
||||
padding,
|
||||
centered,
|
||||
titlePart,
|
||||
style
|
||||
titlePart
|
||||
}: DialogHeaderProps) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
|
||||
@@ -64,8 +62,7 @@ const DialogHeader = ({
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
minHeight: 50,
|
||||
paddingHorizontal: padding,
|
||||
...style
|
||||
paddingHorizontal: padding
|
||||
}}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -35,7 +35,6 @@ import BaseDialog from "./base-dialog";
|
||||
import DialogButtons from "./dialog-buttons";
|
||||
import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -55,11 +54,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
input: false,
|
||||
inputPlaceholder: "Enter some text",
|
||||
defaultValue: "",
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent"
|
||||
}
|
||||
disableBackdropClosing: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,7 +87,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setVisible(true);
|
||||
setInputValue(data.defaultValue);
|
||||
},
|
||||
[context]
|
||||
);
|
||||
@@ -143,9 +137,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
paragraph={dialogInfo.paragraph}
|
||||
paragraphColor={dialogInfo.paragraphColor}
|
||||
padding={12}
|
||||
style={{
|
||||
minHeight: 0
|
||||
}}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
@@ -172,28 +163,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setInputValue(!inputValue);
|
||||
}}
|
||||
icon={
|
||||
inputValue
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
style={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={inputValue ? dialogInfo.check.type : "gray"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { View } from "react-native";
|
||||
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
@@ -31,7 +30,6 @@ import Navigation from "../../../services/navigation";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eCloseAddTopicDialog,
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddTopicDialog
|
||||
} from "../../../utils/events";
|
||||
import { sleep } from "../../../utils/time";
|
||||
@@ -42,7 +40,6 @@ import DialogHeader from "../../dialog/dialog-header";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { Toast } from "../../toast";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
export class AddTopicDialog extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -62,12 +59,14 @@ export class AddTopicDialog extends React.Component {
|
||||
|
||||
addNewTopic = async () => {
|
||||
try {
|
||||
this.setState({ loading: true });
|
||||
if (!this.title || this.title?.trim() === "") {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({ loading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,13 +78,10 @@ export class AddTopicDialog extends React.Component {
|
||||
|
||||
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
|
||||
}
|
||||
this.setState({ loading: false });
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
});
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
useRelationStore.getState().update();
|
||||
Navigation.queueRoutesForUpdate("Notebooks", "Notebook", "TopicNotes");
|
||||
useMenuStore.getState().setMenuPins();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -181,6 +177,7 @@ export class AddTopicDialog extends React.Component {
|
||||
positiveTitle={this.toEdit ? "Save" : "Add"}
|
||||
onPressNegative={() => this.close()}
|
||||
onPressPositive={() => this.addNewTopic()}
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
</DialogContainer>
|
||||
<Toast context="local" />
|
||||
|
||||
@@ -135,6 +135,7 @@ const ResultDialog = () => {
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
onPress={close}
|
||||
height={50}
|
||||
fontSize={SIZE.md + 2}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -207,7 +207,13 @@ export class VaultDialog extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"TopicNotes",
|
||||
"TaggedNotes",
|
||||
"ColoredNotes"
|
||||
);
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import Tag from "../ui/tag";
|
||||
|
||||
@@ -63,15 +64,13 @@ export const Title = () => {
|
||||
}
|
||||
if (data.y > 150) {
|
||||
if (!hide) return;
|
||||
titleState[currentScreen.id] = false;
|
||||
setHide(false);
|
||||
} else {
|
||||
if (hide) return;
|
||||
titleState[currentScreen.id] = true;
|
||||
setHide(true);
|
||||
}
|
||||
},
|
||||
[currentScreen.id, currentScreen.name, hide]
|
||||
[currentScreen.name, hide]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -86,6 +85,10 @@ export const Title = () => {
|
||||
}
|
||||
}, [currentScreen.id, currentScreen.name]);
|
||||
|
||||
useEffect(() => {
|
||||
titleState[currentScreen.id] = hide;
|
||||
}, [currentScreen.id, hide]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eScrollEvent, onScroll);
|
||||
return () => {
|
||||
@@ -109,16 +112,25 @@ export const Title = () => {
|
||||
{!hide && !isHidden ? (
|
||||
<Heading
|
||||
onPress={navigateToNotebook}
|
||||
numberOfLines={1}
|
||||
size={SIZE.xl}
|
||||
numberOfLines={isTopic ? 2 : 1}
|
||||
size={isTopic ? SIZE.md + 2 : SIZE.xl}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginTop: Platform.OS === "ios" ? -1 : 0
|
||||
}}
|
||||
color={currentScreen.color || colors.heading}
|
||||
>
|
||||
{isTopic ? (
|
||||
<Paragraph numberOfLines={1} size={SIZE.xs + 1}>
|
||||
{notebook?.title}
|
||||
{"\n"}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{isTag ? (
|
||||
<Heading size={SIZE.xl} color={colors.accent}>
|
||||
<Heading
|
||||
size={isTopic ? SIZE.md + 2 : SIZE.xl}
|
||||
color={colors.accent}
|
||||
>
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
@@ -18,26 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import ImageViewer from "react-native-image-zoom-viewer";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { cacheDir } from "../../common/filesystem/utils";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import dataurl from "@notesnook/core/utils/dataurl";
|
||||
|
||||
const ImagePreview = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [image, setImage] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("ImagePreview", open);
|
||||
@@ -47,29 +39,9 @@ const ImagePreview = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = async (image) => {
|
||||
const open = (image) => {
|
||||
setImage(image);
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
let hash = image.hash;
|
||||
if (!hash && dataurl.toObject(image.src)) {
|
||||
const data = dataurl.toObject(image.src);
|
||||
if (!data) return;
|
||||
hash = await Sodium.hashFile({
|
||||
data: data.data,
|
||||
type: "base64",
|
||||
uri: ""
|
||||
});
|
||||
}
|
||||
if (!hash) return;
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
setImage("file://" + path);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
@@ -87,60 +59,44 @@ const ImagePreview = () => {
|
||||
backgroundColor: "black"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
indeterminate
|
||||
color={colors.accent}
|
||||
borderColor="transparent"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<ImageViewer
|
||||
enableImageZoom={true}
|
||||
renderIndicator={() => <></>}
|
||||
enableSwipeDown
|
||||
useNativeDriver
|
||||
onSwipeDown={close}
|
||||
saveToLocalByLongPress={false}
|
||||
renderHeader={() => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
height: 80,
|
||||
marginTop: 0,
|
||||
paddingHorizontal: 12,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
paddingTop: Platform.OS === "android" ? 30 : 0
|
||||
<ImageViewer
|
||||
enableImageZoom={true}
|
||||
renderIndicator={() => <></>}
|
||||
enableSwipeDown
|
||||
useNativeDriver
|
||||
onSwipeDown={close}
|
||||
saveToLocalByLongPress={false}
|
||||
renderHeader={() => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
height: 80,
|
||||
marginTop: 0,
|
||||
paddingHorizontal: 12,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
paddingTop: 30
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="close"
|
||||
color="white"
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="close"
|
||||
color="white"
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
imageUrls={[
|
||||
{
|
||||
url: image
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
imageUrls={[
|
||||
{
|
||||
url: image
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</BaseDialog>
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
Image,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
View
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import umami from "../../common/analytics";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -49,6 +50,9 @@ export const SVG_Z =
|
||||
|
||||
const Intro = ({ navigation }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const isTelemetryEnabled = useSettingStore(
|
||||
(state) => state.settings.telemetry
|
||||
);
|
||||
@@ -60,6 +64,12 @@ const Intro = ({ navigation }) => {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!introCompleted) {
|
||||
umami.pageView("/welcome", "", []);
|
||||
}
|
||||
}, [introCompleted]);
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="notesnook.splashscreen"
|
||||
@@ -161,6 +171,8 @@ const Intro = ({ navigation }) => {
|
||||
navigation.navigate("AppLock", {
|
||||
welcome: true
|
||||
});
|
||||
|
||||
umami.pageView("/home", "/welcome");
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,17 +17,18 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import notifee from "@notifee/react-native";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import { checkVersion } from "react-native-check-version";
|
||||
import Config from "react-native-config";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { DatabaseLogger, db, loadDatabase } from "../../common/database";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { setRateAppMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -37,9 +38,7 @@ import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { AndroidModule } from "../../utils";
|
||||
import { eOpenAnnouncementDialog } from "../../utils/events";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { SVG } from "../auth/background";
|
||||
@@ -54,6 +53,8 @@ import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import Config from "react-native-config";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
|
||||
const Launcher = React.memo(
|
||||
function Launcher() {
|
||||
@@ -70,7 +71,7 @@ const Launcher = React.memo(
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
|
||||
const dbInitCompleted = useRef(false);
|
||||
const loadNotes = useCallback(async () => {
|
||||
if (verifyUser) {
|
||||
return;
|
||||
@@ -81,25 +82,28 @@ const Launcher = React.memo(
|
||||
initialize();
|
||||
setImmediate(() => {
|
||||
setLoading(false);
|
||||
if (!dbInitCompleted.current) {
|
||||
setImmediate(() => doAppLoadActions());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [setLoading, verifyUser]);
|
||||
}, [doAppLoadActions, setLoading, verifyUser]);
|
||||
|
||||
const init = useCallback(async () => {
|
||||
if (!db.isInitialized) {
|
||||
if (!dbInitCompleted.current) {
|
||||
await RNBootSplash.hide({ fade: true });
|
||||
await loadDatabase();
|
||||
DatabaseLogger.info("Initializing database");
|
||||
await db.init();
|
||||
dbInitCompleted.current = true;
|
||||
}
|
||||
|
||||
if (db.migrations.required() && !verifyUser) {
|
||||
presentSheet({
|
||||
component: <Migrate />,
|
||||
onClose: async () => {
|
||||
if (!db.isInitialized) {
|
||||
await db.init();
|
||||
}
|
||||
await db.init();
|
||||
loadNotes();
|
||||
},
|
||||
disableClosing: true
|
||||
@@ -118,6 +122,9 @@ const Launcher = React.memo(
|
||||
if (!loading) {
|
||||
doAppLoadActions();
|
||||
}
|
||||
return () => {
|
||||
dbInitCompleted.current = false;
|
||||
};
|
||||
}, [doAppLoadActions, loading]);
|
||||
|
||||
const doAppLoadActions = useCallback(async () => {
|
||||
@@ -126,7 +133,6 @@ const Launcher = React.memo(
|
||||
eSendEvent("session_expired");
|
||||
return;
|
||||
}
|
||||
notifee.setBadgeCount(0);
|
||||
await useMessageStore.getState().setAnnouncement();
|
||||
if (NewFeature.present()) return;
|
||||
if (await checkAppUpdateAvailable()) return;
|
||||
@@ -145,7 +151,7 @@ const Launcher = React.memo(
|
||||
}, [introCompleted]);
|
||||
|
||||
const checkAppUpdateAvailable = async () => {
|
||||
if (__DEV__ || Config.isTesting === "true") return;
|
||||
if (__DEV__) return;
|
||||
try {
|
||||
const version =
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
@@ -193,11 +199,6 @@ const Launcher = React.memo(
|
||||
|
||||
const onUnlockBiometrics = useCallback(async () => {
|
||||
if (!(await BiometricService.isBiometryAvailable())) return;
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await AndroidModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
}
|
||||
|
||||
let verified = await BiometricService.validateUser(
|
||||
"Unlock to access your notes",
|
||||
""
|
||||
|
||||
@@ -24,10 +24,8 @@ import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import Notebook from "../../../screens/notebook";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
@@ -41,6 +39,10 @@ import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const navigateToTopic = (topic) => {
|
||||
TopicNotes.navigate(topic, true);
|
||||
};
|
||||
|
||||
function navigateToTag(item) {
|
||||
const tag = db.tags.tag(item.id);
|
||||
if (!tag) return;
|
||||
@@ -53,48 +55,30 @@ const showActionSheet = (item) => {
|
||||
|
||||
function getNotebook(item) {
|
||||
const isTrash = item.type === "trash";
|
||||
const currentId = useNavigationStore.getState().currentScreen.id;
|
||||
if (isTrash) return [];
|
||||
const items = [];
|
||||
const notebooks = db.relations.to(item, "notebook") || [];
|
||||
if (isTrash || !item.notebooks || item.notebooks.length < 1) return [];
|
||||
|
||||
for (let notebook of notebooks) {
|
||||
if (items.length > 1) break;
|
||||
if (notebook.id === currentId) continue;
|
||||
items.push(notebook);
|
||||
}
|
||||
return item.notebooks.reduce(function (prev, curr) {
|
||||
if (prev && prev.length > 0) return prev;
|
||||
const topicId = curr.topics[0];
|
||||
const notebook = db.notebooks?.notebook(curr.id)?.data;
|
||||
if (!notebook) return [];
|
||||
const topic = notebook.topics.find((t) => t.id === topicId);
|
||||
if (!topic) return [];
|
||||
|
||||
if (item.notebooks) {
|
||||
for (let nb of item.notebooks) {
|
||||
if (items.length > 1) break;
|
||||
const notebook = db.notebooks?.notebook(nb.id)?.data;
|
||||
if (!notebook) continue;
|
||||
for (let topicId of nb.topics) {
|
||||
if (items.length > 1) break;
|
||||
if (topicId === currentId) continue;
|
||||
const topic = notebook.topics.find((t) => t.id === topicId);
|
||||
if (!topic) continue;
|
||||
items.push(topic);
|
||||
return [
|
||||
{
|
||||
title: `${notebook?.title} › ${topic?.title}`,
|
||||
notebook: notebook,
|
||||
topic: topic
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function getTags(item) {
|
||||
const noteTags = item.tags?.slice(0, 3) || [];
|
||||
const tags = [];
|
||||
for (const tagName of noteTags) {
|
||||
const tag = db.tags.tag(tagName);
|
||||
if (!tag) continue;
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
];
|
||||
}, []);
|
||||
}
|
||||
|
||||
const NoteItem = ({
|
||||
item,
|
||||
isTrash,
|
||||
tags,
|
||||
dateBy = "dateCreated",
|
||||
noOpen = false
|
||||
}) => {
|
||||
@@ -104,13 +88,11 @@ const NoteItem = ({
|
||||
);
|
||||
const compactMode = notesListMode === "compact";
|
||||
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
|
||||
const _update = useRelationStore((state) => state.updater);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
|
||||
const notebooks = React.useMemo(() => getNotebook(item), [item]);
|
||||
const reminders = db.relations.from(item, "reminder");
|
||||
const reminder = getUpcomingReminder(reminders);
|
||||
const _update = useRelationStore((state) => state.updater);
|
||||
const noteColor = COLORS_NOTE[item.color?.toLowerCase()];
|
||||
const tags = getTags(item);
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
@@ -130,17 +112,12 @@ const NoteItem = ({
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{notebooks?.map((item) => (
|
||||
{notebooks?.map((_item) => (
|
||||
<Button
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
title={_item.title}
|
||||
key={_item}
|
||||
height={25}
|
||||
icon={item.type === "topic" ? "bookmark" : "book-outline"}
|
||||
icon="book-outline"
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
@@ -155,13 +132,7 @@ const NoteItem = ({
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (item.type === "topic") {
|
||||
TopicNotes.navigate(item, true);
|
||||
} else {
|
||||
Notebook.navigate(item);
|
||||
}
|
||||
}}
|
||||
onPress={() => navigateToTopic(_item.topic)}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo(
|
||||
function NoteWrapper({ item, index, dateBy, isSheet }) {
|
||||
function NoteWrapper({ item, index, tags, dateBy, isSheet }) {
|
||||
const isTrash = item.type === "trash";
|
||||
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
|
||||
|
||||
@@ -113,7 +113,7 @@ export const NoteWrapper = React.memo(
|
||||
isSheet={isSheet}
|
||||
item={item}
|
||||
>
|
||||
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
|
||||
<NoteItem item={item} dateBy={dateBy} tags={tags} isTrash={isTrash} />
|
||||
</SelectionWrapper>
|
||||
);
|
||||
},
|
||||
@@ -125,6 +125,10 @@ export const NoteWrapper = React.memo(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (JSON.stringify(prev.tags) !== JSON.stringify(next.tags)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prev.item !== next.item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,16 @@ export const openNotebookTopic = (item) => {
|
||||
negativeText: "Delete",
|
||||
positivePress: async () => {
|
||||
await db.trash.restore(item.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Tags",
|
||||
"Notes",
|
||||
"Notebooks",
|
||||
"Favorites",
|
||||
"Trash",
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
|
||||
@@ -56,6 +56,7 @@ const ReminderItem = React.memo(
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
flexShrink: 1,
|
||||
opacity: item.disabled ? 0.5 : 1
|
||||
}}
|
||||
@@ -93,13 +94,13 @@ const ReminderItem = React.memo(
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 5,
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 6,
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 3,
|
||||
alignItems: "center",
|
||||
marginTop: 5,
|
||||
justifyContent: "flex-start",
|
||||
alignSelf: "flex-start",
|
||||
marginRight: 10,
|
||||
height: 30
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -122,13 +123,13 @@ const ReminderItem = React.memo(
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 5,
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 6,
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 3,
|
||||
alignItems: "center",
|
||||
marginTop: 5,
|
||||
justifyContent: "flex-start",
|
||||
alignSelf: "flex-start",
|
||||
marginRight: 10,
|
||||
height: 30
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
<Icon name="reload" size={SIZE.md} color={colors.accent} />
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, { SlideInUp, SlideOutDown } from "react-native-reanimated";
|
||||
import { db } from "../../../common/database";
|
||||
import { openVault, ToastEvent } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { useTrashStore } from "../../../stores/use-trash-store";
|
||||
import { dWidth, getElevation, toTXT } from "../../../utils";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
|
||||
export const ActionStrip = ({ note, setActionStrip }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
|
||||
const setMenuPins = useMenuStore((state) => state.setMenuPins);
|
||||
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
|
||||
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
|
||||
|
||||
const [isPinnedToMenu, setIsPinnedToMenu] = useState(false);
|
||||
const [width, setWidth] = useState(dWidth - 16);
|
||||
useEffect(() => {
|
||||
if (note.type === "note") return;
|
||||
setIsPinnedToMenu(db.shortcuts.exists(note.id));
|
||||
}, [note.id, note.type]);
|
||||
|
||||
const updateNotes = () => {
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
};
|
||||
|
||||
const actions = [
|
||||
{
|
||||
title: "Pin " + note.type,
|
||||
icon: note.pinned ? "pin-off" : "pin",
|
||||
visible: note.type === "note" || note.type === "notebook",
|
||||
onPress: async () => {
|
||||
if (!note.id) return;
|
||||
|
||||
if (note.type === "note") {
|
||||
await db.notes.note(note.id).pin();
|
||||
} else {
|
||||
await db.notebooks.notebook(note.id).pin();
|
||||
setNotebooks();
|
||||
}
|
||||
updateNotes();
|
||||
setActionStrip(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Add to favorites",
|
||||
icon: note.favorite ? "star-off" : "star",
|
||||
onPress: async () => {
|
||||
if (!note.id) return;
|
||||
if (note.type === "note") {
|
||||
await db.notes.note(note.id).favorite();
|
||||
} else {
|
||||
await db.notebooks.notebook(note.id).favorite();
|
||||
}
|
||||
updateNotes();
|
||||
setActionStrip(false);
|
||||
},
|
||||
visible: note.type === "note",
|
||||
color: !note.favorite ? "orange" : null
|
||||
},
|
||||
|
||||
{
|
||||
title: isPinnedToMenu
|
||||
? "Remove Shortcut from Menu"
|
||||
: "Add Shortcut to Menu",
|
||||
icon: isPinnedToMenu ? "link-variant-remove" : "link-variant",
|
||||
onPress: async () => {
|
||||
try {
|
||||
if (isPinnedToMenu) {
|
||||
await db.shortcuts.remove(note.id);
|
||||
ToastEvent.show({
|
||||
heading: "Shortcut removed from menu",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
if (note.type === "topic") {
|
||||
await db.shortcuts.add({
|
||||
item: {
|
||||
type: "topic",
|
||||
id: note.id,
|
||||
notebookId: note.notebookId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await db.shortcuts.add({
|
||||
item: {
|
||||
type: "notebook",
|
||||
id: note.id
|
||||
}
|
||||
});
|
||||
}
|
||||
ToastEvent.show({
|
||||
heading: "Shortcut added to menu",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
setIsPinnedToMenu(db.shortcuts.exists(note.id));
|
||||
setMenuPins();
|
||||
|
||||
setActionStrip(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
visible: note.type !== "note" && note.type !== "reminder"
|
||||
},
|
||||
{
|
||||
title: "Copy Note",
|
||||
icon: "content-copy",
|
||||
visible: note.type === "note",
|
||||
onPress: async () => {
|
||||
if (note.locked) {
|
||||
openVault({
|
||||
copyNote: true,
|
||||
novault: true,
|
||||
locked: true,
|
||||
item: note,
|
||||
title: "Copy note",
|
||||
description: "Unlock note to copy to clipboard."
|
||||
});
|
||||
} else {
|
||||
let text = await toTXT(note);
|
||||
text = `${note.title}\n \n ${text}`;
|
||||
Clipboard.setString(text);
|
||||
ToastEvent.show({
|
||||
heading: "Note copied to clipboard",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
setActionStrip(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Restore " + note.itemType,
|
||||
icon: "delete-restore",
|
||||
onPress: async () => {
|
||||
await db.trash.restore(note.id);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Trash",
|
||||
"Notebooks"
|
||||
);
|
||||
|
||||
ToastEvent.show({
|
||||
heading:
|
||||
note.type === "note"
|
||||
? "Note restored from trash"
|
||||
: "Notebook restored from trash",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
setActionStrip(false);
|
||||
},
|
||||
visible: note.type === "trash"
|
||||
},
|
||||
{
|
||||
title: "Delete" + note.itemType,
|
||||
icon: "delete",
|
||||
visible: note.type === "trash",
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
title: "Permanent delete",
|
||||
paragraph: `Are you sure you want to delete this ${note.itemType} permanantly from trash?`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
setActionStrip(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Delete" + note.type,
|
||||
icon: "delete",
|
||||
visible: note.type !== "trash",
|
||||
onPress: async () => {
|
||||
if (note.type === "reminder") {
|
||||
presentDialog({
|
||||
title: `Delete ${note.type}`,
|
||||
paragraph: "This reminder will be removed",
|
||||
positivePress: async () => {
|
||||
const routes = [];
|
||||
await db.reminders.remove(note.id);
|
||||
routes.push("Reminders");
|
||||
Navigation.queueRoutesForUpdate(...routes);
|
||||
useRelationStore.getState().update();
|
||||
},
|
||||
positiveText: "Delete",
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteItems(note);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setActionStrip(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Close",
|
||||
icon: "close",
|
||||
onPress: () => setActionStrip(false),
|
||||
color: colors.light,
|
||||
bg: colors.red,
|
||||
visible: true
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
onLayout={(event) => {
|
||||
setWidth(event.nativeEvent.layout.width);
|
||||
}}
|
||||
entering={SlideInUp.springify().mass(0.4)}
|
||||
exiting={SlideOutDown}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
width: "102%",
|
||||
height: "100%",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="accent"
|
||||
title="Select"
|
||||
icon="check"
|
||||
tooltipText="Select Item"
|
||||
onPress={() => {
|
||||
if (!selectionMode) {
|
||||
setSelectionMode(true);
|
||||
}
|
||||
setSelectedItem(note);
|
||||
setActionStrip(false);
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 12,
|
||||
...getElevation(5)
|
||||
}}
|
||||
height={30}
|
||||
/>
|
||||
{actions.map((item) =>
|
||||
item.visible ? (
|
||||
<View
|
||||
key={item.icon}
|
||||
style={{
|
||||
width: width / 1.4 / actions.length,
|
||||
height: width / 1.4 / actions.length,
|
||||
backgroundColor: item.bg || colors.nav,
|
||||
borderRadius: 100,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
...getElevation(5),
|
||||
marginLeft: 15
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color={item.color || colors.heading}
|
||||
onPress={item.onPress}
|
||||
tooltipText={item.title}
|
||||
top={60}
|
||||
bottom={60}
|
||||
name={item.icon}
|
||||
size={width / 2.8 / actions.length}
|
||||
/>
|
||||
</View>
|
||||
) : null
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -17,13 +17,19 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { history } from "../../../utils";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import { ActionStrip } from "./action-strip";
|
||||
import { Filler } from "./back-fill";
|
||||
import { SelectionIcon } from "./selection";
|
||||
import { useRef } from "react";
|
||||
|
||||
const SelectionWrapper = ({
|
||||
children,
|
||||
@@ -36,6 +42,7 @@ const SelectionWrapper = ({
|
||||
}) => {
|
||||
const itemId = useRef(item.id);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [actionStrip, setActionStrip] = useState(false);
|
||||
const notebooksListMode = useSettingStore(
|
||||
(state) => state.settings.notebooksListMode
|
||||
);
|
||||
@@ -49,18 +56,35 @@ const SelectionWrapper = ({
|
||||
|
||||
if (item.id !== itemId.current) {
|
||||
itemId.current = item.id;
|
||||
if (actionStrip) {
|
||||
setActionStrip(false);
|
||||
}
|
||||
}
|
||||
|
||||
const _onLongPress = () => {
|
||||
if (!useSelectionStore.getState().selectionMode) {
|
||||
useSelectionStore.getState().setSelectionMode(true);
|
||||
}
|
||||
useSelectionStore.getState().setSelectedItem(item);
|
||||
if (history.selectedItemsList.length > 0) return;
|
||||
setActionStrip(!actionStrip);
|
||||
};
|
||||
|
||||
const _onPress = async () => {
|
||||
if (actionStrip) {
|
||||
setActionStrip(false);
|
||||
return;
|
||||
}
|
||||
await onPress();
|
||||
};
|
||||
|
||||
const closeStrip = () => {
|
||||
setActionStrip(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("navigate", closeStrip);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent("navigate", closeStrip);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<PressableButton
|
||||
customColor={isSheet ? colors.transGray : "transparent"}
|
||||
@@ -87,10 +111,15 @@ const SelectionWrapper = ({
|
||||
) : null}
|
||||
<SelectionIcon
|
||||
compactMode={compactMode}
|
||||
setActionStrip={setActionStrip}
|
||||
item={item}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
{children}
|
||||
|
||||
{actionStrip ? (
|
||||
<ActionStrip note={item} setActionStrip={setActionStrip} />
|
||||
) : null}
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
|
||||
export const SelectionIcon = ({ item }) => {
|
||||
export const SelectionIcon = ({ setActionStrip, item }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
@@ -35,6 +35,7 @@ export const SelectionIcon = ({ item }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
setActionStrip(false);
|
||||
let exists = selectedItemsList.filter(
|
||||
(o) => o.dateCreated === item.dateCreated
|
||||
);
|
||||
@@ -49,9 +50,16 @@ export const SelectionIcon = ({ item }) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedItemsList, item.id, selectionMode, item.dateCreated, selected]);
|
||||
}, [
|
||||
selectedItemsList,
|
||||
item.id,
|
||||
selectionMode,
|
||||
setActionStrip,
|
||||
item.dateCreated,
|
||||
selected
|
||||
]);
|
||||
|
||||
return selectionMode ? (
|
||||
return selectionMode && selected ? (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
@@ -65,13 +73,11 @@ export const SelectionIcon = ({ item }) => {
|
||||
}}
|
||||
pointerEvents="none"
|
||||
>
|
||||
{selected ? (
|
||||
<Icon
|
||||
size={SIZE.xl}
|
||||
color={selected ? colors.accent : colors.icon}
|
||||
name={"check"}
|
||||
/>
|
||||
) : null}
|
||||
<Icon
|
||||
size={SIZE.xl}
|
||||
color={selected ? colors.accent : colors.icon}
|
||||
name="check"
|
||||
/>
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -53,52 +53,45 @@ export const Card = ({ color, warning }) => {
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
width: 40,
|
||||
backgroundColor:
|
||||
messageBoardState.type === "error"
|
||||
? hexToRGBA(colors.red, 0.15)
|
||||
: hexToRGBA(color, 0.15),
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
backgroundColor:
|
||||
messageBoardState.type === "error"
|
||||
? hexToRGBA(colors.red, 0.15)
|
||||
: hexToRGBA(color, 0.15),
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.errorText : color
|
||||
}
|
||||
name={messageBoardState.icon}
|
||||
/>
|
||||
</View>
|
||||
<Icon
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.errorText : color
|
||||
}
|
||||
name={messageBoardState.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
flexShrink: 1,
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{messageBoardState.message}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
flexShrink: 1,
|
||||
marginRight: 10
|
||||
flexWrap: "wrap",
|
||||
flexShrink: 1
|
||||
}}
|
||||
color={colors.heading}
|
||||
>
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{messageBoardState.message}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
flexShrink: 1
|
||||
}}
|
||||
color={colors.heading}
|
||||
>
|
||||
{messageBoardState.actionText}
|
||||
</Paragraph>
|
||||
</View>
|
||||
{messageBoardState.actionText}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
@@ -57,10 +57,24 @@ const RenderItem = ({ item, index, type, ...restArgs }) => {
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
const totalNotes = getTotalNotes(item);
|
||||
const tags =
|
||||
item.tags
|
||||
?.slice(0, 3)
|
||||
?.map((item) => {
|
||||
let tag = db.tags.tag(item);
|
||||
|
||||
if (!tag) return null;
|
||||
return {
|
||||
title: tag.title,
|
||||
id: tag.id,
|
||||
alias: tag.alias
|
||||
};
|
||||
})
|
||||
.filter((t) => t !== null) || [];
|
||||
return (
|
||||
<Item
|
||||
item={item}
|
||||
tags={tags}
|
||||
dateBy={dateBy}
|
||||
index={index}
|
||||
type={type}
|
||||
@@ -89,9 +103,7 @@ const List = ({
|
||||
ListHeader,
|
||||
warning,
|
||||
isSheet = false,
|
||||
onMomentumScrollEnd,
|
||||
handlers,
|
||||
ScrollComponent
|
||||
onMomentumScrollEnd
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const scrollRef = useRef();
|
||||
@@ -139,21 +151,14 @@ const List = ({
|
||||
[screen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSendEvent(eScrollEvent, {
|
||||
y: 0,
|
||||
screen
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
let styles = {
|
||||
width: "100%",
|
||||
minHeight: 1,
|
||||
minWidth: 1
|
||||
};
|
||||
|
||||
const ListView = ScrollComponent ? ScrollComponent : FlashList;
|
||||
const _keyExtractor = (item) => item.id || item.title;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
@@ -162,8 +167,7 @@ const List = ({
|
||||
}}
|
||||
entering={type === "search" ? undefined : FadeInDown}
|
||||
>
|
||||
<ListView
|
||||
{...handlers}
|
||||
<FlashList
|
||||
style={styles}
|
||||
ref={scrollRef}
|
||||
testID={notesnook.list.id}
|
||||
@@ -218,14 +222,12 @@ const List = ({
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
{listData ? (
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -85,7 +85,13 @@ const MergeConflicts = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
if (editorController.current?.note?.id === note.id) {
|
||||
// reload the note in editor
|
||||
eSendEvent(eOnLoadNote, {
|
||||
|
||||
@@ -44,7 +44,16 @@ export default function NotePreview({ session, content, note }) {
|
||||
async function restore() {
|
||||
if (note && note.type === "trash") {
|
||||
await db.trash.restore(note.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Tags",
|
||||
"Notes",
|
||||
"Notebooks",
|
||||
"Favorites",
|
||||
"Trash",
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
@@ -64,7 +73,13 @@ export default function NotePreview({ session, content, note }) {
|
||||
}
|
||||
eSendEvent(eCloseSheet, "note_history");
|
||||
eSendEvent(eCloseSheet);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
|
||||
ToastEvent.show({
|
||||
heading: "Note restored successfully",
|
||||
|
||||
@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { LAUNCH_ROCKET } from "../../assets/images/assets";
|
||||
import umami from "../../common/analytics";
|
||||
import { db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -47,6 +48,7 @@ import { Walkthrough } from "../walkthroughs";
|
||||
import { features } from "./features";
|
||||
import { Group } from "./group";
|
||||
import { PricingPlans } from "./pricing-plans";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export const Component = ({ close, promo }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -58,6 +60,7 @@ export const Component = ({ close, promo }) => {
|
||||
|
||||
const onPress = async () => {
|
||||
if (user) {
|
||||
umami.pageView("/pro-plans", "/pro-screen");
|
||||
presentSheet({
|
||||
context: "pricing_plans",
|
||||
component: (
|
||||
@@ -66,6 +69,7 @@ export const Component = ({ close, promo }) => {
|
||||
});
|
||||
} else {
|
||||
close();
|
||||
umami.pageView("/signup", "/pro-screen");
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.trialSignup);
|
||||
}, 400);
|
||||
|
||||
@@ -17,9 +17,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, Text, View } from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import umami from "../../common/analytics";
|
||||
import { db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import {
|
||||
@@ -47,6 +48,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { PricingItem } from "./pricing-item";
|
||||
import { useCallback } from "react";
|
||||
|
||||
const promoCyclesMonthly = {
|
||||
1: "first month",
|
||||
@@ -139,6 +141,10 @@ export const PricingPlans = ({
|
||||
setBuying(false);
|
||||
return;
|
||||
}
|
||||
umami.pageView(
|
||||
"/iap-native",
|
||||
`${compact ? "pro-sheet" : "pro-screen"}/pro-plans`
|
||||
);
|
||||
await RNIap.requestSubscription(
|
||||
product?.productId,
|
||||
false,
|
||||
|
||||
@@ -22,10 +22,12 @@ import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useMenuStore } from "../../stores/use-menu-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { dWidth } from "../../utils";
|
||||
import { COLORS_NOTE } from "../../utils/color-scheme";
|
||||
import { refreshNotesPage } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
@@ -33,7 +35,9 @@ import { PressableButton } from "../ui/pressable";
|
||||
export const ColorTags = ({ item }) => {
|
||||
const [note, setNote] = useState(item);
|
||||
const setColorNotes = useMenuStore((state) => state.setColorNotes);
|
||||
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
let width = dimensions.width > 600 ? 600 : 500;
|
||||
|
||||
const changeColor = async (color) => {
|
||||
if (note.color === color.name) {
|
||||
await db.notes.note(note.id).uncolor();
|
||||
@@ -43,7 +47,13 @@ export const ColorTags = ({ item }) => {
|
||||
let _note = db.notes.note(note.id).data;
|
||||
setNote({ ..._note });
|
||||
setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
eSendEvent(refreshNotesPage);
|
||||
};
|
||||
|
||||
@@ -61,12 +71,11 @@ export const ColorTags = ({ item }) => {
|
||||
key={color.value}
|
||||
onPress={() => changeColor(color)}
|
||||
customStyle={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
width: DDS.isTab ? width / 10 : dWidth / 9,
|
||||
height: DDS.isTab ? width / 10 : dWidth / 9,
|
||||
borderRadius: 100,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: isTablet ? 10 : undefined
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{note.color?.toLowerCase() === color.name ? (
|
||||
@@ -81,11 +90,12 @@ export const ColorTags = ({ item }) => {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
flexGrow: isTablet ? undefined : 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingRight: 0,
|
||||
width: "100%",
|
||||
marginVertical: 10,
|
||||
marginTop: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: isTablet ? "center" : "space-between"
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
{Object.keys(COLORS_NOTE).map(_renderColor)}
|
||||
|
||||
@@ -43,41 +43,36 @@ export const DateMeta = ({ item }) => {
|
||||
}
|
||||
};
|
||||
|
||||
function getDateMeta() {
|
||||
let keys = Object.keys(item);
|
||||
if (keys.includes("dateEdited"))
|
||||
keys.splice(
|
||||
keys.findIndex((k) => k === "dateModified"),
|
||||
1
|
||||
);
|
||||
return keys.filter((key) => key.startsWith("date") && key !== "date");
|
||||
}
|
||||
|
||||
const renderItem = (key) => (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
{timeConverter(item[key])}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
const renderItem = (key) =>
|
||||
key.startsWith("date") && key !== "date" ? (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{timeConverter(item[key])}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 5,
|
||||
marginTop: 5,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.nav,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{getDateMeta().map(renderItem)}
|
||||
{Object.keys(item).map(renderItem)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,9 +16,10 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
@@ -26,62 +27,64 @@ import SearchService from "../../services/search";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { COLORS_NOTE } from "../../utils/color-scheme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { ReminderTime } from "../ui/reminder-time";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ColorTags } from "./color-tags";
|
||||
import { DateMeta } from "./date-meta";
|
||||
import { DevMode } from "./dev-mode";
|
||||
import { Items } from "./items";
|
||||
import Notebooks from "./notebooks";
|
||||
import { Synced } from "./synced";
|
||||
import { Tags, TagStrip } from "./tags";
|
||||
const Line = ({ top = 6, bottom = 6 }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.nav,
|
||||
width: "100%",
|
||||
marginTop: top,
|
||||
marginBottom: bottom
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
import { Tags } from "./tags";
|
||||
import { Topics } from "./topics";
|
||||
import {
|
||||
getUpcomingReminder
|
||||
} from "@notesnook/core/collections/reminders";
|
||||
import { ReminderTime } from "../ui/reminder-time";
|
||||
export const Properties = ({
|
||||
close = () => {},
|
||||
item,
|
||||
buttons = [],
|
||||
getRef
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const alias = item.alias || item.title;
|
||||
const isColor = !!COLORS_NOTE[item.title];
|
||||
const reminders = db.relations.from(item, "reminder");
|
||||
const current = getUpcomingReminder(reminders);
|
||||
|
||||
const onScrollEnd = () => {
|
||||
getRef().current?.handleChildScrollEnd();
|
||||
};
|
||||
|
||||
if (!item || !item.id) {
|
||||
return (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
Start writing to save your note.
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={onScrollEnd}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
style={{
|
||||
backgroundColor: colors.bg,
|
||||
paddingHorizontal: 0,
|
||||
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
|
||||
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
data={[0]}
|
||||
keyExtractor={() => "properties-scroll-item"}
|
||||
renderItem={() => (
|
||||
<View>
|
||||
>
|
||||
{!item || !item.id ? (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
Start writing to save your note.
|
||||
</Paragraph>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 5,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 5,
|
||||
zIndex: 10
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
@@ -93,8 +96,25 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
{alias}
|
||||
</Heading>
|
||||
|
||||
{item.type === "note" ? (
|
||||
<TagStrip close={close} item={item} />
|
||||
{item.headline || item.description ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 5
|
||||
}}
|
||||
numberOfLines={2}
|
||||
color={colors.icon}
|
||||
>
|
||||
{(item.type === "notebook" ||
|
||||
item.itemType === "notebook" ||
|
||||
item.type === "reminder") &&
|
||||
item?.description
|
||||
? item.description
|
||||
: null}
|
||||
{(item.type === "note" || item.itemType === "note") &&
|
||||
item?.headline
|
||||
? item.headline
|
||||
: null}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.type === "reminder" ? (
|
||||
@@ -104,52 +124,55 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
fontSize={SIZE.xs + 1}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.type === "note" ? <Tags close={close} item={item} /> : null}
|
||||
|
||||
<Topics item={item} close={close} />
|
||||
</View>
|
||||
<Line top={12} />
|
||||
|
||||
{item.type === "note" ? (
|
||||
<Notebooks note={item} close={close} />
|
||||
) : null}
|
||||
|
||||
<DateMeta item={item} />
|
||||
<Line bottom={0} />
|
||||
{item.type === "note" ? <Tags close={close} item={item} /> : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Notebooks note={item} close={close} />
|
||||
</View>
|
||||
|
||||
<Items
|
||||
item={item}
|
||||
buttons={buttons}
|
||||
close={() => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
SearchService.updateAndSearch();
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
<Synced item={item} close={close} />
|
||||
<DevMode item={item} />
|
||||
|
||||
{DDS.isTab ? (
|
||||
<View
|
||||
style={{
|
||||
height: 20
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<SheetProvider context="properties" />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderTopWidth: 1,
|
||||
borderColor: colors.nav
|
||||
}}
|
||||
/>
|
||||
|
||||
{item.type === "note" ? <ColorTags close={close} item={item} /> : null}
|
||||
|
||||
<Items
|
||||
item={item}
|
||||
buttons={buttons}
|
||||
close={() => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
SearchService.updateAndSearch();
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
<Synced item={item} close={close} />
|
||||
<DevMode item={item} />
|
||||
|
||||
{DDS.isTab ? (
|
||||
<View
|
||||
style={{
|
||||
height: 20
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -161,51 +184,50 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
switch (type) {
|
||||
case "trash":
|
||||
props[0] = item;
|
||||
props.push(["delete", "restore"]);
|
||||
props.push(["PermDelete", "Restore"]);
|
||||
break;
|
||||
case "note":
|
||||
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
|
||||
android = Platform.OS === "android" ? ["PinToNotif"] : [];
|
||||
props[0] = db.notes.note(item.id).data;
|
||||
props.push([
|
||||
"notebooks",
|
||||
"add-reminder",
|
||||
"share",
|
||||
"export",
|
||||
"copy",
|
||||
"publish",
|
||||
"pin",
|
||||
"favorite",
|
||||
"attachments",
|
||||
"lock-unlock",
|
||||
"trash",
|
||||
"remove-from-topic",
|
||||
"remove-from-notebook",
|
||||
"history",
|
||||
"read-only",
|
||||
"reminders",
|
||||
"local-only",
|
||||
"duplicate",
|
||||
"Add to notebook",
|
||||
"Add-Reminder",
|
||||
"Share",
|
||||
"Export",
|
||||
"Copy",
|
||||
"Publish",
|
||||
"Pin",
|
||||
"Favorite",
|
||||
"Attachments",
|
||||
"Vault",
|
||||
"Delete",
|
||||
"RemoveTopic",
|
||||
"History",
|
||||
"ReadOnly",
|
||||
"Reminders",
|
||||
"Local only",
|
||||
"Duplicate",
|
||||
...android,
|
||||
...buttons
|
||||
]);
|
||||
break;
|
||||
case "notebook":
|
||||
props[0] = db.notebooks.notebook(item.id).data;
|
||||
props.push(["edit-notebook", "pin", "add-shortcut", "trash"]);
|
||||
props.push(["Edit Notebook", "Pin", "Add Shortcut", "Delete"]);
|
||||
break;
|
||||
case "topic":
|
||||
props[0] = db.notebooks
|
||||
.notebook(item.notebookId)
|
||||
.topics.topic(item.id)._topic;
|
||||
props.push(["move-notes", "edit-topic", "add-shortcut", "trash"]);
|
||||
props.push(["Move notes", "Edit Topic", "Add Shortcut", "Delete"]);
|
||||
break;
|
||||
case "tag":
|
||||
props[0] = db.tags.tag(item.id);
|
||||
props.push(["add-shortcut", "trash", "rename-tag"]);
|
||||
props.push(["Add Shortcut", "Delete", "Rename Tag"]);
|
||||
break;
|
||||
case "reminder": {
|
||||
props[0] = db.reminders.reminder(item.id);
|
||||
props.push(["edit-reminder", "trash", "disable-reminder"]);
|
||||
props.push(["Edit reminder", "Delete", "ReminderOnOff"]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -217,7 +239,7 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
close={() => {
|
||||
close();
|
||||
}}
|
||||
actionSheetRef={ref}
|
||||
getRef={() => ref}
|
||||
item={props[0]}
|
||||
buttons={props[1]}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { FlatList, ScrollView, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useActions } from "../../hooks/use-actions";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -32,19 +33,19 @@ export const Items = ({ item, buttons, close }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const actions = useActions({ item, close });
|
||||
const data = actions.filter((i) => buttons.indexOf(i.id) > -1 && !i.hidden);
|
||||
const data = actions.filter((i) => buttons.indexOf(i.name) > -1 && !i.hidden);
|
||||
|
||||
let width = dimensions.width > 600 ? 600 : dimensions.width;
|
||||
let columnItemsCount = DDS.isLargeTablet() ? 7 : 5;
|
||||
let columnItemWidth = DDS.isTab
|
||||
? (width - 12) / columnItemsCount
|
||||
: (width - 12) / columnItemsCount;
|
||||
? (width - 24) / columnItemsCount
|
||||
: (width - 24) / columnItemsCount;
|
||||
|
||||
const _renderRowItem = ({ item }) => (
|
||||
<View
|
||||
onPress={item.func}
|
||||
key={item.id}
|
||||
testID={"icon-" + item.id}
|
||||
key={item.name}
|
||||
testID={"icon-" + item.name}
|
||||
style={{
|
||||
alignItems: "center",
|
||||
width: columnItemWidth,
|
||||
@@ -57,7 +58,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
customStyle={{
|
||||
height: columnItemWidth - 12,
|
||||
width: columnItemWidth - 12,
|
||||
borderRadius: 10,
|
||||
borderRadius: 5,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
@@ -71,22 +72,21 @@ export const Items = ({ item, buttons, close }) => {
|
||||
color={
|
||||
item.on
|
||||
? colors.accent
|
||||
: item.id.match(/(delete|trash)/g)
|
||||
: item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.errorText
|
||||
: colors.icon
|
||||
}
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph size={SIZE.xs} style={{ textAlign: "center" }}>
|
||||
<Paragraph size={SIZE.xs + 1} style={{ textAlign: "center" }}>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderColumnItem = (item) => (
|
||||
const renderColumnItem = ({ item }) => (
|
||||
<Button
|
||||
key={item.name + item.title}
|
||||
buttonType={{
|
||||
text: item.on
|
||||
? colors.accent
|
||||
@@ -108,107 +108,31 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
const renderTopBarItem = (item, index) => {
|
||||
const isLast = index === topBarItems.length;
|
||||
return (
|
||||
<PressableButton
|
||||
onPress={item.func}
|
||||
key={item.id}
|
||||
testID={"icon-" + item.id}
|
||||
customStyle={{
|
||||
alignSelf: "flex-start",
|
||||
width: topBarItemWidth,
|
||||
marginRight: isLast ? 0 : 10,
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
onPress={item.func}
|
||||
type={item.on ? "shade" : "gray"}
|
||||
customStyle={{
|
||||
height: topBarItemWidth,
|
||||
width: topBarItemWidth,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
marginBottom: DDS.isTab ? 7 : 3.5,
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
size={DDS.isTab ? SIZE.xxl : SIZE.md + 4}
|
||||
color={
|
||||
item.on
|
||||
? colors.accent
|
||||
: item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.errorText
|
||||
: colors.icon
|
||||
}
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
const topBarItemsList = [
|
||||
"pin",
|
||||
"favorite",
|
||||
"copy",
|
||||
"share",
|
||||
"export",
|
||||
"lock-unlock",
|
||||
"publish"
|
||||
];
|
||||
const topBarItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) > -1
|
||||
);
|
||||
|
||||
const bottomGridItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) === -1
|
||||
);
|
||||
|
||||
const topBarItemWidth =
|
||||
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
|
||||
|
||||
return item.type === "note" ? (
|
||||
<>
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
{topBarItems.map(renderTopBarItem)}
|
||||
</ScrollView>
|
||||
|
||||
<FlatList
|
||||
data={bottomGridItems}
|
||||
keyExtractor={(item) => item.title}
|
||||
key={columnItemsCount + "key"}
|
||||
numColumns={columnItemsCount}
|
||||
disableVirtualization={true}
|
||||
style={{
|
||||
marginTop: item.type !== "note" ? 10 : 0,
|
||||
paddingTop: 10,
|
||||
marginLeft: 6
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignSelf: "center",
|
||||
width: buttons.length < 5 ? "100%" : null,
|
||||
paddingLeft: buttons.length < 5 ? 10 : 0
|
||||
}}
|
||||
renderItem={_renderRowItem}
|
||||
/>
|
||||
</>
|
||||
<FlatList
|
||||
data={data}
|
||||
keyExtractor={(item) => item.title}
|
||||
key={columnItemsCount + "key"}
|
||||
numColumns={columnItemsCount}
|
||||
style={{
|
||||
marginTop: item.type !== "note" ? 10 : 0,
|
||||
paddingTop: 10
|
||||
}}
|
||||
columnWrapperStyle={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignSelf: "center",
|
||||
width: buttons.length < 5 ? "100%" : null,
|
||||
paddingLeft: buttons.length < 5 ? 10 : 0
|
||||
}}
|
||||
renderItem={_renderRowItem}
|
||||
/>
|
||||
) : (
|
||||
<View data={data}>{data.map(renderColumnItem)}</View>
|
||||
<FlatList
|
||||
data={data}
|
||||
keyExtractor={(item) => item.title}
|
||||
renderItem={renderColumnItem}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,49 +23,39 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import Notebook from "../../screens/notebook";
|
||||
import { TopicNotes } from "../../screens/notes/topic-notes";
|
||||
import { presentSheet, ToastEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useNotebookStore } from "../../stores/use-notebook-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
|
||||
export default function Notebooks({ note, close, full }) {
|
||||
export default function Notebooks({ note, close }) {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const notebooks = useNotebookStore((state) => state.notebooks);
|
||||
function getNotebooks(item) {
|
||||
let filteredNotebooks = [];
|
||||
const relations = db.relations.to(note, "notebook");
|
||||
filteredNotebooks.push(
|
||||
...relations.map((notebook) => ({
|
||||
...notebook,
|
||||
topics: []
|
||||
}))
|
||||
);
|
||||
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
|
||||
|
||||
for (let notebookReference of item.notebooks) {
|
||||
let notebook = {
|
||||
...(notebooks.find((item) => item.id === notebookReference.id) || {})
|
||||
};
|
||||
if (notebook.id) {
|
||||
notebook.topics = notebook.topics.filter((topic) => {
|
||||
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
|
||||
});
|
||||
const index = filteredNotebooks.findIndex(
|
||||
(item) => item.id === notebook.id
|
||||
);
|
||||
if (index > -1) {
|
||||
filteredNotebooks[index].topics = notebook.topics;
|
||||
} else {
|
||||
filteredNotebooks.push(notebook);
|
||||
}
|
||||
function getNotebooks(item) {
|
||||
if (!item.notebooks || item.notebooks.length < 1) return [];
|
||||
let notebooks = [];
|
||||
for (let notebook of item.notebooks) {
|
||||
let item_notebook = db.notebooks.notebook(notebook.id);
|
||||
if (item_notebook) {
|
||||
let data = {
|
||||
id: notebook.id,
|
||||
title: item_notebook.title,
|
||||
topics: notebook.topics
|
||||
.map((item) => {
|
||||
let topic = item_notebook.topics.topic(item)?._topic;
|
||||
if (!topic) return null;
|
||||
return {
|
||||
id: topic.id,
|
||||
title: topic.title
|
||||
};
|
||||
})
|
||||
.filter((i) => i !== null)
|
||||
};
|
||||
notebooks.push(data);
|
||||
}
|
||||
}
|
||||
return filteredNotebooks;
|
||||
return notebooks;
|
||||
}
|
||||
const noteNotebooks = getNotebooks(note);
|
||||
|
||||
const navigateNotebook = (id) => {
|
||||
let item = db.notebooks.notebook(id)?.data;
|
||||
@@ -78,134 +68,85 @@ export default function Notebooks({ note, close, full }) {
|
||||
if (!item) return;
|
||||
TopicNotes.navigate(item, true);
|
||||
};
|
||||
const renderItem = (item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
padding: 6,
|
||||
borderWidth: full ? 0 : 1,
|
||||
borderColor: colors.nav,
|
||||
borderRadius: 10,
|
||||
backgroundColor: full ? "transparent" : colors.nav,
|
||||
minHeight: 42
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="book-outline"
|
||||
color={colors.accent}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 5
|
||||
}}
|
||||
/>
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
maxWidth: "50%"
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
onPress={() => {
|
||||
navigateNotebook(item.id);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: 8,
|
||||
borderLeftColor: colors.nav,
|
||||
borderLeftWidth: 1,
|
||||
paddingLeft: 8
|
||||
}}
|
||||
>
|
||||
{item.topics.map((topic) => (
|
||||
<Button
|
||||
key={topic.id}
|
||||
onPress={() => {
|
||||
navigateTopic(topic.id, item.id);
|
||||
close();
|
||||
}}
|
||||
onLongPress={async () => {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.id,
|
||||
topic: topic.id
|
||||
},
|
||||
note
|
||||
);
|
||||
useNotebookStore.getState().setNotebooks();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
ToastEvent.show({
|
||||
heading: "Note removed from topic",
|
||||
context: "local",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
title={topic.title}
|
||||
type="gray"
|
||||
height={30}
|
||||
fontSize={SIZE.xs + 1}
|
||||
icon="bookmark-outline"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<View style={{ width: 10 }} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
return noteNotebooks.length === 0 ? null : (
|
||||
return !note.notebooks || note.notebooks.length === 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
marginTop: 6
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.nav
|
||||
}}
|
||||
>
|
||||
{full
|
||||
? noteNotebooks.map(renderItem)
|
||||
: noteNotebooks.slice(0, 1).map(renderItem)}
|
||||
|
||||
{noteNotebooks.length > 1 && !full ? (
|
||||
<Button
|
||||
title={`See all linked notebooks`}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
marginRight: 12,
|
||||
paddingHorizontal: 0,
|
||||
backgroundColor: "transparent"
|
||||
}}
|
||||
type="gray"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
height={20}
|
||||
{getNotebooks(note).map((item) => (
|
||||
<PressableButton
|
||||
key={item.id}
|
||||
onPress={() => {
|
||||
presentSheet({
|
||||
context: "properties",
|
||||
component: (ref, close) => (
|
||||
<Notebooks note={note} close={close} full={true} />
|
||||
)
|
||||
});
|
||||
navigateNotebook(item.id);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
customStyle={{
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
marginTop: 5,
|
||||
paddingVertical: 6
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="book-outline"
|
||||
color={colors.accent}
|
||||
size={SIZE.sm + 1}
|
||||
style={{
|
||||
marginRight: 5
|
||||
}}
|
||||
/>
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
maxWidth: "50%"
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: 8,
|
||||
borderLeftColor: colors.nav,
|
||||
borderLeftWidth: 1,
|
||||
paddingLeft: 8
|
||||
}}
|
||||
>
|
||||
{item.topics.map((topic) => (
|
||||
<Button
|
||||
key={topic.id}
|
||||
onPress={() => {
|
||||
navigateTopic(topic.id, item.id);
|
||||
close();
|
||||
}}
|
||||
title={topic.title}
|
||||
type="grayBg"
|
||||
height={22}
|
||||
fontSize={SIZE.xs + 1}
|
||||
icon="bookmark-outline"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<View style={{ width: 10 }} />
|
||||
</ScrollView>
|
||||
</PressableButton>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export const Synced = ({ item, close }) => {
|
||||
fontSize={SIZE.xs + 1}
|
||||
title="Learn more"
|
||||
height={30}
|
||||
type="grayAccent"
|
||||
type="transparent"
|
||||
/>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
@@ -21,12 +21,12 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { TaggedNotes } from "../../screens/notes/tagged";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eOpenTagsDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import { Button } from "../ui/button";
|
||||
import { ColorTags } from "./color-tags";
|
||||
export const Tags = ({ item, close }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
|
||||
@@ -34,53 +34,41 @@ export const Tags = ({ item, close }) => {
|
||||
<View
|
||||
style={{
|
||||
marginTop: 5,
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTagsSheet.present(item);
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.accent
|
||||
}}
|
||||
title="Add tags"
|
||||
type="grayBg"
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
height={30}
|
||||
fontSize={SIZE.xs + 1}
|
||||
<View
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 8
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
<ColorTags item={item} />
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const TagStrip = ({ item, close }) => {
|
||||
return item.tags?.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{item.tags.map((tag) =>
|
||||
tag ? <TagItem key={tag} tag={tag} close={close} /> : null
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenTagsDialog, item);
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.accent
|
||||
}}
|
||||
title="Add tags"
|
||||
type="grayBg"
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
height={25}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
/>
|
||||
{item.tags.map((item) =>
|
||||
item ? <TagItem key={item} tag={item} close={close} /> : null
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
@@ -95,23 +83,23 @@ const TagItem = ({ tag, close }) => {
|
||||
};
|
||||
|
||||
const style = {
|
||||
paddingHorizontal: 0,
|
||||
paddingHorizontal: 8,
|
||||
marginVertical: 5,
|
||||
borderRadius: 100,
|
||||
marginRight: 10,
|
||||
marginTop: 0,
|
||||
backgroundColor: "transparent"
|
||||
marginRight: 5
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={"#" + tag}
|
||||
// buttonType={{
|
||||
// text: colors.accent
|
||||
// }}
|
||||
type="grayBg"
|
||||
height={20}
|
||||
height={25}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={style}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,18 +21,17 @@ import React, { useCallback, useEffect } from "react";
|
||||
import { BackHandler, Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SearchService from "../../services/search";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eOpenMoveNoteDialog } from "../../utils/events";
|
||||
import { deleteItems } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import MoveNoteSheet from "../sheets/add-to";
|
||||
import ExportNotesSheet from "../sheets/export-notes";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
@@ -48,9 +47,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const screen = currentScreen.name;
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
SearchService.prepareSearch?.();
|
||||
const allItems = SearchService.getSearchInformation()?.get() || [];
|
||||
const allSelected = allItems.length === selectedItemsList.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
tabBarRef.current?.lock();
|
||||
@@ -64,7 +61,13 @@ export const SelectionHeader = React.memo(() => {
|
||||
selectedItemsList.forEach((item) => {
|
||||
db.notes.note(item.id).favorite();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
clearSelection();
|
||||
}
|
||||
};
|
||||
@@ -76,7 +79,16 @@ export const SelectionHeader = React.memo(() => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.restore(...noteIds);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Trash",
|
||||
"Notebooks",
|
||||
"Tags"
|
||||
);
|
||||
|
||||
clearSelection();
|
||||
ToastEvent.show({
|
||||
@@ -103,7 +115,16 @@ export const SelectionHeader = React.memo(() => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.delete(...noteIds);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Trash",
|
||||
"Notebooks",
|
||||
"Tags"
|
||||
);
|
||||
clearSelection();
|
||||
}
|
||||
},
|
||||
@@ -128,15 +149,14 @@ export const SelectionHeader = React.memo(() => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: Platform.OS === "android" ? 50 + insets.top : 50,
|
||||
height: 50 + insets.top,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : null,
|
||||
backgroundColor: colors.bg,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
zIndex: 999,
|
||||
paddingHorizontal: 12,
|
||||
marginVertical: 10
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -177,7 +197,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md} color={colors.accent}>
|
||||
{selectedItemsList.length}
|
||||
{selectedItemsList.length + " Selected"}
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
@@ -188,21 +208,20 @@ export const SelectionHeader = React.memo(() => {
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
{/* <ActionIcon
|
||||
onPress={async () => {
|
||||
useSelectionStore
|
||||
.getState()
|
||||
.setAll(allSelected ? [] : [...allItems]);
|
||||
// await sleep(100);
|
||||
// eSendEvent(eOpenMoveNoteDialog);
|
||||
useSelectionStore.getState().setAll([...SearchService.getSearchInformation().data]);
|
||||
|
||||
}}
|
||||
tooltipText="Select all"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={allSelected ? colors.accent : colors.pri}
|
||||
color={colors.pri}
|
||||
name="select-all"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{screen === "Trash" ||
|
||||
screen === "Notebooks" ||
|
||||
@@ -213,14 +232,12 @@ export const SelectionHeader = React.memo(() => {
|
||||
onPress={async () => {
|
||||
//setSelectionMode(false);
|
||||
await sleep(100);
|
||||
MoveNoteSheet.present();
|
||||
eSendEvent(eOpenMoveNoteDialog);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.pri}
|
||||
tooltipText="Add to notebooks"
|
||||
tooltipPosition={4}
|
||||
name="plus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
@@ -228,8 +245,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
onPress={async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
}}
|
||||
tooltipText="Export"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
@@ -240,41 +255,35 @@ export const SelectionHeader = React.memo(() => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === "TopicNotes" || screen === "Notebook" ? (
|
||||
{screen === "TopicNotes" ? (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
const currentScreen =
|
||||
const currentTopic =
|
||||
useNavigationStore.getState().currentScreen;
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: currentTopic.notebookId,
|
||||
topic: currentTopic.id
|
||||
},
|
||||
...selectedItemsList.map((item) => item.id)
|
||||
);
|
||||
|
||||
if (screen === "Notebook") {
|
||||
for (const item of selectedItemsList) {
|
||||
await db.relations.unlink(
|
||||
{ type: "notebook", id: currentScreen.id },
|
||||
item
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: currentScreen.notebookId,
|
||||
topic: currentScreen.id
|
||||
},
|
||||
...selectedItemsList.map((item) => item.id)
|
||||
);
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText={`Remove from ${
|
||||
screen === "Notebook" ? "notebook" : "topic"
|
||||
}`}
|
||||
tooltipPosition={4}
|
||||
testID="select-minus"
|
||||
color={colors.pri}
|
||||
name="minus"
|
||||
@@ -288,8 +297,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Remove from favorites"
|
||||
tooltipPosition={4}
|
||||
color={colors.pri}
|
||||
name="star-off"
|
||||
size={SIZE.xl}
|
||||
@@ -320,8 +327,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
return;
|
||||
}}
|
||||
color={colors.pri}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
name="delete"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
@@ -336,8 +341,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
color={colors.pri}
|
||||
onPress={restoreItem}
|
||||
name="delete-restore"
|
||||
tooltipText="Restore"
|
||||
tooltipPosition={4}
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
|
||||
@@ -347,8 +350,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
}}
|
||||
color={colors.pri}
|
||||
onPress={deleteItem}
|
||||
tooltipText="Delete"
|
||||
tooltipPosition={4}
|
||||
name="delete"
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
const SheetProvider = ({ context = "global" }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [data, setData] = useState(null);
|
||||
const [dialogData, setDialogData] = useState(null);
|
||||
const actionSheetRef = useRef();
|
||||
const editor = useRef({
|
||||
refocus: false
|
||||
@@ -52,35 +52,42 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
}, [close, open, visible]);
|
||||
|
||||
const open = useCallback(
|
||||
async (payload) => {
|
||||
if (!payload.context) payload.context = "global";
|
||||
if (payload.context !== context) return;
|
||||
setData(payload);
|
||||
async (data) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
if (visible || dialogData) {
|
||||
setDialogData(null);
|
||||
setVisible(false);
|
||||
await sleep(500);
|
||||
}
|
||||
setDialogData(data);
|
||||
setVisible(true);
|
||||
if (payload.editor) {
|
||||
if (data.editor) {
|
||||
editor.current.refocus = false;
|
||||
if (editorState().keyboardState) {
|
||||
// tiny.call(EditorWebView, tiny.cacheRange + tiny.blur);
|
||||
editor.current.refocus = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
[context]
|
||||
[context, dialogData, visible]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (visible && data) {
|
||||
if (data.editor) await sleep(100);
|
||||
if (visible && dialogData) {
|
||||
if (dialogData.editor) await sleep(100);
|
||||
actionSheetRef.current?.setModalVisible(true);
|
||||
return;
|
||||
} else {
|
||||
if (editor.current?.refocus) {
|
||||
editorState().isFocused = true;
|
||||
// tiny.call(EditorWebView, tiny.restoreRange + tiny.clearRange);
|
||||
editor.current.refocus = false;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [visible, data]);
|
||||
}, [visible, dialogData]);
|
||||
|
||||
const close = useCallback(
|
||||
(ctx) => {
|
||||
@@ -91,31 +98,34 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
[context]
|
||||
);
|
||||
|
||||
return !visible || !data ? null : (
|
||||
return !visible || !dialogData ? null : (
|
||||
<SheetWrapper
|
||||
fwdRef={actionSheetRef}
|
||||
gestureEnabled={!data?.progress && !data?.disableClosing}
|
||||
closeOnTouchBackdrop={!data?.progress && !data?.disableClosing}
|
||||
gestureEnabled={!dialogData?.progress && !dialogData?.disableClosing}
|
||||
closeOnTouchBackdrop={
|
||||
!dialogData?.progress && !dialogData?.disableClosing
|
||||
}
|
||||
onClose={() => {
|
||||
data.onClose && data.onClose();
|
||||
dialogData.onClose && dialogData.onClose();
|
||||
setVisible(false);
|
||||
setData(null);
|
||||
setDialogData(null);
|
||||
}}
|
||||
bottomPadding={!data.noBottomPadding}
|
||||
enableGesturesInScrollView={data.enableGesturesInScrollView}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom:
|
||||
!data.progress && !data.icon && !data.title && !data.paragraph
|
||||
!dialogData.progress &&
|
||||
!dialogData.icon &&
|
||||
!dialogData.title &&
|
||||
!dialogData.paragraph
|
||||
? 0
|
||||
: 10,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{data?.progress ? (
|
||||
{dialogData?.progress ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginTop: 15
|
||||
@@ -125,48 +135,47 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.icon ? (
|
||||
{dialogData?.icon ? (
|
||||
<Icon
|
||||
color={colors[data.iconColor] || colors.accent}
|
||||
name={data.icon}
|
||||
color={colors[dialogData.iconColor] || colors.accent}
|
||||
name={dialogData.icon}
|
||||
size={50}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.title ? <Heading> {data?.title}</Heading> : null}
|
||||
{dialogData?.title ? <Heading> {dialogData?.title}</Heading> : null}
|
||||
|
||||
{data?.paragraph ? (
|
||||
{dialogData?.paragraph ? (
|
||||
<Paragraph style={{ textAlign: "center" }}>
|
||||
{data?.paragraph}
|
||||
{dialogData?.paragraph}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{typeof data.component === "function"
|
||||
? data.component(
|
||||
{typeof dialogData.component === "function"
|
||||
? dialogData.component(
|
||||
actionSheetRef,
|
||||
() => close(context),
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
setData((prevData) => {
|
||||
setDialogData((prevData) => {
|
||||
return {
|
||||
...prevData,
|
||||
...data
|
||||
};
|
||||
});
|
||||
},
|
||||
colors
|
||||
}
|
||||
)
|
||||
: data.component}
|
||||
: dialogData.component}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: data.valueArray ? 12 : 0
|
||||
marginBottom: dialogData.valueArray ? 12 : 0
|
||||
}}
|
||||
>
|
||||
{data.valueArray &&
|
||||
data.valueArray.map((v) => (
|
||||
{dialogData.valueArray &&
|
||||
dialogData.valueArray.map((v) => (
|
||||
<Button
|
||||
title={v}
|
||||
type="gray"
|
||||
@@ -188,12 +197,12 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{data?.action ? (
|
||||
{dialogData?.action ? (
|
||||
<Button
|
||||
onPress={data.action}
|
||||
key={data.actionText}
|
||||
title={data.actionText}
|
||||
accentColor={data.iconColor || "accent"}
|
||||
onPress={dialogData.action}
|
||||
key={dialogData.actionText}
|
||||
title={dialogData.actionText}
|
||||
accentColor={dialogData.iconColor || "accent"}
|
||||
accentText="light"
|
||||
type="accent"
|
||||
height={45}
|
||||
@@ -205,14 +214,15 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.actionsArray &&
|
||||
data?.actionsArray.map((item) => (
|
||||
{dialogData?.actionsArray &&
|
||||
dialogData?.actionsArray.map((item) => (
|
||||
<Button
|
||||
onPress={item.action}
|
||||
key={item.accentText}
|
||||
title={item.actionText}
|
||||
icon={item.icon && item.icon}
|
||||
type={item.type || "accent"}
|
||||
height={50}
|
||||
style={{
|
||||
marginBottom: 10
|
||||
}}
|
||||
@@ -221,7 +231,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{data?.learnMore ? (
|
||||
{dialogData?.learnMore ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
@@ -229,7 +239,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
onPress={data.learnMorePress}
|
||||
onPress={dialogData.learnMorePress}
|
||||
color={colors.icon}
|
||||
>
|
||||
<Icon
|
||||
@@ -237,7 +247,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
name="information-outline"
|
||||
size={SIZE.xs}
|
||||
/>{" "}
|
||||
{data.learnMore}
|
||||
{dialogData.learnMore}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -25,34 +25,41 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import { ToastEvent, presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eCloseAddNotebookDialog,
|
||||
eOpenAddNotebookDialog
|
||||
} from "../../../utils/events";
|
||||
import { ph, pv, SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Button } from "../../ui/button";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import { Toast } from "../../toast";
|
||||
|
||||
let refs = [];
|
||||
|
||||
export class AddNotebookSheet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
refs = [];
|
||||
this.state = {
|
||||
notebook: props.notebook,
|
||||
topics:
|
||||
props.notebook?.topics?.map((item) => {
|
||||
return item.title;
|
||||
}) || [],
|
||||
notebook: null,
|
||||
visible: false,
|
||||
topics: [],
|
||||
description: null,
|
||||
titleFocused: false,
|
||||
descFocused: false,
|
||||
@@ -61,14 +68,13 @@ export class AddNotebookSheet extends React.Component {
|
||||
editTopic: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
this.title = props.notebook?.title;
|
||||
this.description = props.notebook?.description;
|
||||
this.title = null;
|
||||
this.description = null;
|
||||
this.listRef;
|
||||
this.prevItem = null;
|
||||
this.prevIndex = null;
|
||||
this.currentSelectedInput = null;
|
||||
this.id = props.notebook?.id;
|
||||
this.id = null;
|
||||
this.backPressCount = 0;
|
||||
this.currentInputValue = null;
|
||||
this.titleRef;
|
||||
@@ -77,22 +83,57 @@ export class AddNotebookSheet extends React.Component {
|
||||
this.hiddenInput = createRef();
|
||||
this.topicInputRef = createRef();
|
||||
this.addingTopic = false;
|
||||
this.actionSheetRef = props.actionSheetRef;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
refs = [];
|
||||
this.actionSheetRef = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
sleep(300).then(() => {
|
||||
!this.state.notebook && this.titleRef?.focus();
|
||||
});
|
||||
eSubscribeEvent(eOpenAddNotebookDialog, this.open);
|
||||
eSubscribeEvent(eCloseAddNotebookDialog, this.close);
|
||||
}
|
||||
|
||||
close = () => {
|
||||
componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenAddNotebookDialog, this.open);
|
||||
eUnSubscribeEvent(eCloseAddNotebookDialog, this.close);
|
||||
}
|
||||
|
||||
open = (notebook) => {
|
||||
refs = [];
|
||||
this.props.close(true);
|
||||
|
||||
if (notebook) {
|
||||
let topicsList = [];
|
||||
notebook.topics.forEach((item) => {
|
||||
topicsList.push(item.title);
|
||||
});
|
||||
this.id = notebook.id;
|
||||
this.title = notebook.title;
|
||||
this.description = notebook.description;
|
||||
|
||||
this.setState({
|
||||
topics: [...topicsList],
|
||||
visible: true,
|
||||
notebook: notebook
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
visible: true,
|
||||
notebook: null
|
||||
});
|
||||
}
|
||||
sleep(100).then(() => {
|
||||
this.actionSheetRef.current?.show();
|
||||
});
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.actionSheetRef.current?.hide();
|
||||
refs = [];
|
||||
this.prevIndex = null;
|
||||
this.prevItem = null;
|
||||
this.currentSelectedInput = null;
|
||||
this.title = null;
|
||||
this.description = null;
|
||||
this.currentInputValue = null;
|
||||
this.id = null;
|
||||
};
|
||||
|
||||
onDelete = (index) => {
|
||||
@@ -182,7 +223,6 @@ export class AddNotebookSheet extends React.Component {
|
||||
});
|
||||
|
||||
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
|
||||
this.close();
|
||||
} else {
|
||||
newNotebookId = await db.notebooks.add({
|
||||
title: this.title,
|
||||
@@ -192,9 +232,23 @@ export class AddNotebookSheet extends React.Component {
|
||||
});
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
this.close();
|
||||
await sleep(300);
|
||||
if (!notebook) {
|
||||
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = (forward = true) => {
|
||||
@@ -211,7 +265,7 @@ export class AddNotebookSheet extends React.Component {
|
||||
topics: prevTopics
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
this.listRef.scrollToEnd({ animated: true });
|
||||
}, 30);
|
||||
this.currentInputValue = null;
|
||||
} else {
|
||||
@@ -235,7 +289,7 @@ export class AddNotebookSheet extends React.Component {
|
||||
|
||||
if (forward) {
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
this.listRef.scrollToEnd({ animated: true });
|
||||
}, 30);
|
||||
}
|
||||
}
|
||||
@@ -245,166 +299,183 @@ export class AddNotebookSheet extends React.Component {
|
||||
willFocus && this.topicInputRef.current?.focus();
|
||||
};
|
||||
|
||||
renderTopicItem = ({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={this.props.colors}
|
||||
/>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
const { topics, topicInputFocused, notebook } = this.state;
|
||||
const { topics, visible, topicInputFocused, notebook } = this.state;
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "97%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
<SheetWrapper
|
||||
onOpen={async () => {
|
||||
this.topicsToDelete = [];
|
||||
await sleep(300);
|
||||
!this.state.notebook && this.titleRef?.focus();
|
||||
}}
|
||||
fwdRef={this.actionSheetRef}
|
||||
onClose={() => {
|
||||
this.close();
|
||||
this.setState({
|
||||
visible: false,
|
||||
topics: [],
|
||||
descFocused: false,
|
||||
titleFocused: false,
|
||||
editTopic: false,
|
||||
notebook: null
|
||||
});
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={this.close}
|
||||
>
|
||||
<TextInput
|
||||
ref={this.hiddenInput}
|
||||
style={{
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
position: "absolute"
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
maxHeight: DDS.isTab ? "90%" : "96%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook && notebook.dateCreated
|
||||
? "Edit Notebook"
|
||||
: "New Notebook"}
|
||||
</Heading>
|
||||
<Button
|
||||
title="Save"
|
||||
type="accent"
|
||||
height={40}
|
||||
<TextInput
|
||||
ref={this.hiddenInput}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
position: "absolute"
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<DialogHeader
|
||||
title={
|
||||
notebook && notebook.dateCreated
|
||||
? "Edit Notebook"
|
||||
: "New Notebook"
|
||||
}
|
||||
paragraph={
|
||||
notebook && notebook.dateCreated
|
||||
? "You are editing " + this.title + " notebook."
|
||||
: "Notebooks are the best way to organize your notes."
|
||||
}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
this.descriptionRef.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.descriptionRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
this.description = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
onSubmit={() => {
|
||||
this.topicInputRef.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.topicInputRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.topic}
|
||||
onChangeText={(value) => {
|
||||
this.currentInputValue = value;
|
||||
if (this.prevItem !== null) {
|
||||
refs[this.prevIndex].setNativeProps({
|
||||
text: value,
|
||||
style: {
|
||||
borderBottomColor: colors.accent
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
onSubmit={() => {
|
||||
this.onSubmit();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
button={{
|
||||
testID: "topic-add-button",
|
||||
icon: this.state.editTopic ? "check" : "plus",
|
||||
onPress: this.onSubmit,
|
||||
color: topicInputFocused ? colors.accent : colors.icon
|
||||
}}
|
||||
placeholder="Add a topic"
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={topics}
|
||||
ref={(ref) => (this.listRef = ref)}
|
||||
nestedScrollEnabled
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
onMomentumScrollEnd={() => {
|
||||
this.actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
renderItem={({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={colors}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
width="100%"
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
title={
|
||||
notebook && notebook.dateCreated
|
||||
? "Save changes"
|
||||
: "Create notebook"
|
||||
}
|
||||
type="accent"
|
||||
onPress={this.addNewNotebook}
|
||||
/>
|
||||
{/*
|
||||
{Platform.OS === 'ios' && (
|
||||
<View
|
||||
style={{
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
this.descriptionRef.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.descriptionRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
this.description = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
onSubmit={() => {
|
||||
this.topicInputRef.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.topicInputRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.topic}
|
||||
onChangeText={(value) => {
|
||||
this.currentInputValue = value;
|
||||
if (this.prevItem !== null) {
|
||||
refs[this.prevIndex].setNativeProps({
|
||||
text: value,
|
||||
style: {
|
||||
borderBottomColor: colors.accent
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
onSubmit={() => {
|
||||
this.onSubmit();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
button={{
|
||||
testID: "topic-add-button",
|
||||
icon: this.state.editTopic ? "check" : "plus",
|
||||
onPress: this.onSubmit,
|
||||
color: topicInputFocused ? colors.accent : colors.icon
|
||||
}}
|
||||
placeholder="Add a topic"
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={topics}
|
||||
ref={(ref) => (this.listRef = ref)}
|
||||
nestedScrollEnabled
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
ListFooterComponent={
|
||||
topics.length === 0 ? null : <View style={{ height: 50 }} />
|
||||
}
|
||||
renderItem={this.renderTopicItem}
|
||||
/>
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
</SheetWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNotebookSheet.present = (notebook) => {
|
||||
presentSheet({
|
||||
component: (ref, close, _update, colors) => (
|
||||
<AddNotebookSheet
|
||||
actionSheetRef={ref}
|
||||
notebook={notebook}
|
||||
close={close}
|
||||
colors={colors}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
|
||||
const topicRef = (ref) => (refs[index] = ref);
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export const SelectionContext = createContext({
|
||||
toggleSelection: (item) => null,
|
||||
deselect: (item) => null,
|
||||
select: (item) => null,
|
||||
deselectAll: () => null
|
||||
});
|
||||
export const SelectionProvider = SelectionContext.Provider;
|
||||
export const useSelectionContext = () => useContext(SelectionContext);
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { ListHeaderInputItem } from "./list-header-item.js";
|
||||
|
||||
export const FilteredList = ({
|
||||
data,
|
||||
itemType,
|
||||
onAddItem,
|
||||
hasHeaderSearch,
|
||||
listRef,
|
||||
...restProps
|
||||
}) => {
|
||||
const [filtered, setFiltered] = useState(data);
|
||||
const query = useRef();
|
||||
const onChangeText = useCallback(
|
||||
(value) => {
|
||||
query.current = value;
|
||||
try {
|
||||
if (!value) return setFiltered(data);
|
||||
const results = db.lookup[itemType + "s"]([...data], value);
|
||||
setFiltered(results);
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
}
|
||||
},
|
||||
[data, itemType]
|
||||
);
|
||||
const onSubmit = async (value) => {
|
||||
return await onAddItem(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onChangeText(query.current);
|
||||
}, [data, onChangeText]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
{...restProps}
|
||||
data={filtered}
|
||||
ref={listRef}
|
||||
ListHeaderComponent={
|
||||
hasHeaderSearch ? (
|
||||
<ListHeaderInputItem
|
||||
onSubmit={onSubmit}
|
||||
onChangeText={onChangeText}
|
||||
itemType={itemType}
|
||||
testID={"list-input" + itemType}
|
||||
placeholder={`Search or add a new ${itemType}`}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -17,65 +17,206 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import React, { createRef, useEffect, useState } from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SearchService from "../../../services/search";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { eOnTopicSheetUpdate } from "../../../utils/events";
|
||||
import { getTotalNotes } from "../../../utils";
|
||||
import { eOpenMoveNoteDialog } from "../../../utils/events";
|
||||
import layoutmanager from "../../../utils/layout-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { SelectionProvider } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { ListItem } from "./list-item";
|
||||
import { useItemSelectionStore } from "./store";
|
||||
import { useCallback } from "react";
|
||||
let newNotebookTitle = null;
|
||||
const notebookInput = createRef();
|
||||
const actionSheetRef = createRef();
|
||||
const AddToNotebookSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
|
||||
const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
function open(note) {
|
||||
setNote(note);
|
||||
setVisible(true);
|
||||
actionSheetRef.current?.setModalVisible(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenMoveNoteDialog, open);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenMoveNoteDialog, open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const _onClose = () => {
|
||||
setVisible(false);
|
||||
newNotebookTitle = null;
|
||||
setNote(null);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<SheetWrapper fwdRef={actionSheetRef} onClose={_onClose}>
|
||||
<MoveNoteComponent note={note} />
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddToNotebookSheet;
|
||||
|
||||
const MoveNoteComponent = ({ note }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
|
||||
const notebooks = useNotebookStore((state) =>
|
||||
state.notebooks.filter((n) => n?.type === "notebook")
|
||||
);
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
|
||||
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
|
||||
const onAddNotebook = async (title) => {
|
||||
if (!title || title.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
const [expanded, setExpanded] = useState("");
|
||||
const [notebookInputFocused, setNotebookInputFocused] = useState(false);
|
||||
const [noteExists, setNoteExists] = useState([]);
|
||||
const addNewNotebook = async () => {
|
||||
if (!newNotebookTitle || newNotebookTitle.trim().length === 0)
|
||||
return ToastEvent.show({
|
||||
heading: "Notebook title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
let id = await db.notebooks.add({
|
||||
title: newNotebookTitle,
|
||||
description: null,
|
||||
topics: [],
|
||||
id: null
|
||||
});
|
||||
|
||||
setExpanded(id);
|
||||
openAddTopicDialog(db.notebooks.notebook(id).data);
|
||||
notebookInput.current?.clear();
|
||||
notebookInput.current?.blur();
|
||||
setNotebooks();
|
||||
updateNoteExists();
|
||||
};
|
||||
|
||||
const addNewTopic = async (value, item) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await db.notebooks.add({
|
||||
title: title,
|
||||
description: null,
|
||||
topics: [],
|
||||
id: null
|
||||
});
|
||||
await db.notebooks.notebook(item.id).topics.add(value);
|
||||
setNotebooks();
|
||||
updateNoteExists();
|
||||
return true;
|
||||
};
|
||||
|
||||
const handlePress = async (item) => {
|
||||
let noteIds =
|
||||
selectedItemsList.length > 0
|
||||
? selectedItemsList.map((n) => n.id)
|
||||
: [note?.id];
|
||||
|
||||
if (getCount(item)) {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
} else {
|
||||
await db.notes.addToNotebook(
|
||||
{
|
||||
topic: item.id,
|
||||
id: item.notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
|
||||
setNotebooks();
|
||||
updateNoteExists();
|
||||
SearchService.updateAndSearch();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateNoteExists();
|
||||
}, [updateNoteExists]);
|
||||
|
||||
const updateNoteExists = useCallback(() => {
|
||||
if (!note?.id && selectedItemsList?.length === 0) return;
|
||||
|
||||
let notes =
|
||||
selectedItemsList.length > 0
|
||||
? selectedItemsList.map((n) => n.id)
|
||||
: [note?.id];
|
||||
let ids = [];
|
||||
|
||||
let notebooks = db.notebooks.all;
|
||||
for (let i = 0; i < notebooks.length; i++) {
|
||||
if (notebooks[i].topics) {
|
||||
for (let t = 0; t < notebooks[i].topics.length; t++) {
|
||||
let topic = notebooks[i].topics[t];
|
||||
if (topic.type !== "topic") continue;
|
||||
for (let id of notes) {
|
||||
const noteIds = db.notes?.topicReferences.get(topic.id);
|
||||
if (noteIds.indexOf(id) > -1) {
|
||||
if (ids.indexOf(notebooks[i].id) === -1) {
|
||||
ids.push(notebooks[i].id);
|
||||
}
|
||||
if (ids.indexOf(topic.id) === -1) ids.push(topic.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setNoteExists(ids);
|
||||
}, [note?.id, selectedItemsList]);
|
||||
|
||||
const openAddTopicDialog = (item) => {
|
||||
presentDialog({
|
||||
context: "move_note",
|
||||
@@ -85,218 +226,27 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
paragraph: "Add a new topic in " + item.title,
|
||||
positiveText: "Add",
|
||||
positivePress: (value) => {
|
||||
return onAddTopic(value, item);
|
||||
return addNewTopic(value, item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onAddTopic = useCallback(
|
||||
async (value, item) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await db.notebooks.notebook(item.id).topics.add(value);
|
||||
setNotebooks();
|
||||
return true;
|
||||
},
|
||||
[setNotebooks]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const getSelectedNotesCountInItem = React.useCallback(
|
||||
(item) => {
|
||||
switch (item.type) {
|
||||
case "notebook": {
|
||||
const notes = db.relations.from(item, "note");
|
||||
if (notes.length === 0) return 0;
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
notes.findIndex((note) => note.id === item.id) > -1
|
||||
? count++
|
||||
: undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
case "topic": {
|
||||
const noteIds = db.notes?.topicReferences.get(item.id);
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
noteIds.indexOf(item.id) > -1 ? count++ : undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedItemsList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
resetItemState();
|
||||
return () => {
|
||||
useItemSelectionStore.getState().setMultiSelect(false);
|
||||
useItemSelectionStore.getState().setItemState({});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const resetItemState = useCallback(
|
||||
(state) => {
|
||||
const itemState = {};
|
||||
const notebooks = db.notebooks.all;
|
||||
let count = 0;
|
||||
for (let notebook of notebooks) {
|
||||
itemState[notebook.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
: "deselected";
|
||||
if (itemState[notebook.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(notebook);
|
||||
} else {
|
||||
contextValue.deselect(notebook);
|
||||
}
|
||||
for (let topic of notebook.topics) {
|
||||
itemState[topic.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
|
||||
getSelectedNotesCountInItem(topic, selectedItemsList)
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
: "deselected";
|
||||
if (itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(topic);
|
||||
} else {
|
||||
contextValue.deselect(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 1) {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
} else {
|
||||
useItemSelectionStore.getState().setMultiSelect(false);
|
||||
}
|
||||
useItemSelectionStore.getState().setItemState(itemState);
|
||||
},
|
||||
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
|
||||
);
|
||||
|
||||
const getItemsForItem = (item) => {
|
||||
switch (item.type) {
|
||||
case "notebook":
|
||||
return item.topics?.filter((t) => t.type === "topic");
|
||||
}
|
||||
};
|
||||
|
||||
function areAllSelectedItemsInNotebook(notebook, items) {
|
||||
const notes = db.relations.from(notebook, "note");
|
||||
if (notes.length === 0) return false;
|
||||
return items.every((item) => {
|
||||
return notes.find((note) => note.id === item.id);
|
||||
});
|
||||
}
|
||||
|
||||
function areAllSelectedItemsInTopic(topic, items) {
|
||||
return items.every((item) => {
|
||||
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
const updateItemState = useCallback(function (item, state) {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
useItemSelectionStore.getState().setItemState({
|
||||
...itemState,
|
||||
...mergeState
|
||||
});
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
toggleSelection: (item) => {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
if (itemState[item.id] === "selected") {
|
||||
updateItemState(item, "deselected");
|
||||
} else {
|
||||
updateItemState(item, "selected");
|
||||
}
|
||||
},
|
||||
deselect: (item) => {
|
||||
updateItemState(item, "deselected");
|
||||
},
|
||||
select: (item) => {
|
||||
updateItemState(item, "selected");
|
||||
},
|
||||
deselectAll: (state) => {
|
||||
resetItemState(state);
|
||||
}
|
||||
}),
|
||||
[resetItemState, updateItemState]
|
||||
);
|
||||
|
||||
const getItemFromId = (id) => {
|
||||
for (const nb of notebooks) {
|
||||
if (nb.id === id) return nb;
|
||||
for (const tp of nb.topics) {
|
||||
if (tp.id === id) return tp;
|
||||
const getCount = (topic) => {
|
||||
if (!topic) return;
|
||||
let notes =
|
||||
selectedItemsList.length > 0
|
||||
? selectedItemsList.map((n) => n.id)
|
||||
: [note?.id];
|
||||
let count = 0;
|
||||
for (let id of notes) {
|
||||
const noteIds = db.notes?.topicReferences.get(topic.id);
|
||||
if (noteIds.indexOf(id) > -1) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
for (const id in itemState) {
|
||||
const item = getItemFromId(id);
|
||||
if (itemState[id] === "selected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
db.relations.add(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.addToNotebook(
|
||||
{
|
||||
topic: item.id,
|
||||
id: item.notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
} else if (itemState[id] === "deselected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setNotebooks();
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
SearchService.updateAndSearch();
|
||||
actionSheetRef.current?.hide();
|
||||
return count > 0 && notes.length > 0
|
||||
? `${count} of ${notes.length} selected notes exist in this topic. (Tap to remove)`
|
||||
: null;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -316,163 +266,216 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start"
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
style={{
|
||||
minHeight: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
title="Select notebooks"
|
||||
paragraph={
|
||||
!multiSelect
|
||||
? "Long press to enable multi-select."
|
||||
: "Select topics you want to add note(s) to."
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
height={35}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
title="Save"
|
||||
type={"accent"}
|
||||
onPress={onSave}
|
||||
title="Add to notebook"
|
||||
paragraph={"Add your notes to notebooks to find them easily."}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
<FlatList
|
||||
nestedScrollEnabled={true}
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
data={notebooks}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
}
|
||||
ListHeaderComponent={
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={notebookInput}
|
||||
onChangeText={(value) => {
|
||||
newNotebookTitle = value;
|
||||
}}
|
||||
testID={notesnook.ids.dialogs.addTo.addNotebook}
|
||||
blurOnSubmit={false}
|
||||
onFocusInput={() => {
|
||||
setNotebookInputFocused(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setNotebookInputFocused(false);
|
||||
}}
|
||||
button={{
|
||||
icon: "check",
|
||||
color: notebookInputFocused ? colors.accent : colors.icon,
|
||||
onPress: addNewNotebook
|
||||
}}
|
||||
onSubmit={addNewNotebook}
|
||||
placeholder="Create a new notebook"
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title="Reset selection"
|
||||
height={30}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
}}
|
||||
type="grayAccent"
|
||||
onPress={() => {
|
||||
resetItemState();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<SelectionProvider value={contextValue}>
|
||||
<FilteredList
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: dimensions.height * 0.85
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
notebooks.length > 0 ? null : (
|
||||
renderItem={({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: expanded ? colors.nav : "transparent",
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
if (!item.topics || item.topics.length === 0) {
|
||||
setExpanded(item.id);
|
||||
openAddTopicDialog(item);
|
||||
return;
|
||||
}
|
||||
layoutmanager.withAnimation(200);
|
||||
setExpanded(item.id === expanded ? null : item.id);
|
||||
setNotebookInputFocused(false);
|
||||
}}
|
||||
type="grayBg"
|
||||
customStyle={{
|
||||
height: 50,
|
||||
width: "100%",
|
||||
borderRadius: 5,
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
height: 50,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Icon name="book-outline" color={colors.icon} size={100} />
|
||||
<Paragraph style={{ marginBottom: 10 }}>
|
||||
You do not have any notebooks.
|
||||
</Paragraph>
|
||||
<View>
|
||||
<Heading
|
||||
color={
|
||||
noteExists.indexOf(item.id) > -1 ? colors.accent : null
|
||||
}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
{item.topics?.length > 0 ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{getTotalNotes(item) + " notes" + " & "}
|
||||
{item.topics.length === 1
|
||||
? item.topics.length + " topic"
|
||||
: item.topics.length + " topics"}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
name={expanded === item.id ? "plus" : "chevron-down"}
|
||||
color={expanded === item.id ? colors.accent : colors.pri}
|
||||
size={SIZE.xl}
|
||||
onPress={() => {
|
||||
if (expanded !== item.id) {
|
||||
setExpanded(item.id);
|
||||
return;
|
||||
}
|
||||
layoutmanager.withAnimation(200);
|
||||
setExpanded(item.id);
|
||||
openAddTopicDialog(item);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
data={notebooks}
|
||||
hasHeaderSearch={true}
|
||||
renderItem={({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
key={item.id}
|
||||
index={index}
|
||||
hasNotes={getSelectedNotesCountInItem(item) > 0}
|
||||
sheetRef={actionSheetRef}
|
||||
infoText={
|
||||
<>
|
||||
{item.topics.length === 1
|
||||
? item.topics.length + " topic"
|
||||
: item.topics.length + " topics"}
|
||||
</>
|
||||
}
|
||||
getListItems={getItemsForItem}
|
||||
getSublistItemProps={(topic) => ({
|
||||
hasNotes: getSelectedNotesCountInItem(topic) > 0,
|
||||
style: {
|
||||
marginBottom: 0,
|
||||
height: 40
|
||||
},
|
||||
onPress: (item) => {
|
||||
const itemState =
|
||||
useItemSelectionStore.getState().itemState;
|
||||
const currentState = itemState[item.id];
|
||||
if (currentState !== "selected") {
|
||||
resetItemState("deselected");
|
||||
contextValue.select(item);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
},
|
||||
key: item.id,
|
||||
type: "transparent"
|
||||
})}
|
||||
icon={(expanded) => ({
|
||||
name: expanded ? "chevron-up" : "chevron-down",
|
||||
color: expanded ? colors.accent : colors.pri
|
||||
})}
|
||||
onScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
hasSubList={true}
|
||||
hasHeaderSearch={false}
|
||||
type="grayBg"
|
||||
sublistItemType="topic"
|
||||
onAddItem={(title) => {
|
||||
return onAddTopic(title, item);
|
||||
}}
|
||||
onAddSublistItem={(item) => {
|
||||
openAddTopicDialog(item);
|
||||
}}
|
||||
onPress={(item) => {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
const currentState = itemState[item.id];
|
||||
if (currentState !== "selected") {
|
||||
resetItemState("deselected");
|
||||
contextValue.select(item);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
itemType="notebook"
|
||||
onAddItem={async (title) => {
|
||||
return await onAddNotebook(title);
|
||||
}}
|
||||
ListFooterComponent={<View style={{ height: 20 }} />}
|
||||
/>
|
||||
</SelectionProvider>
|
||||
</PressableButton>
|
||||
|
||||
{expanded === item.id ? (
|
||||
<FlatList
|
||||
nestedScrollEnabled
|
||||
data={item.topics?.filter((t) => t.type === "topic")}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 500
|
||||
}}
|
||||
renderItem={({ item, index }) => (
|
||||
<PressableButton
|
||||
onPress={() => handlePress(item, index)}
|
||||
type="gray"
|
||||
customStyle={{
|
||||
minHeight: 50,
|
||||
borderTopWidth: index === 0 ? 0 : 1,
|
||||
borderTopColor: index === 0 ? null : colors.nav,
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Paragraph color={colors.heading}>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{getTotalNotes(item) + " notes"}
|
||||
</Paragraph>
|
||||
{getCount(item) ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 5,
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 2,
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{getCount(item)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{noteExists.indexOf(item.id) > -1 ? (
|
||||
<Button
|
||||
onPress={() => handlePress(item, index)}
|
||||
icon="check"
|
||||
testID="icon-check"
|
||||
iconColor={colors.accent}
|
||||
iconSize={SIZE.lg}
|
||||
height={35}
|
||||
width={35}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</PressableButton>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MoveNoteSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
|
||||
enableGesturesInScrollView: false,
|
||||
noBottomPadding: true
|
||||
});
|
||||
};
|
||||
export default MoveNoteSheet;
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import Input from "../../ui/input";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export const ListHeaderInputItem = ({
|
||||
onSubmit,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
testID
|
||||
}) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [inputValue, setInputValue] = useState();
|
||||
const inputRef = useRef();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 10,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={inputRef}
|
||||
onChangeText={(value) => {
|
||||
setInputValue(value);
|
||||
onChangeText?.(value);
|
||||
}}
|
||||
testID={testID}
|
||||
blurOnSubmit={false}
|
||||
onFocusInput={() => {
|
||||
setFocused(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocused(false);
|
||||
}}
|
||||
button={{
|
||||
icon: inputValue ? "plus" : "magnify",
|
||||
color: focused ? colors.accent : colors.icon,
|
||||
onPress: async () => {
|
||||
const result = await onSubmit(inputValue);
|
||||
if (result) {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
marginBottom={5}
|
||||
/>
|
||||
{inputValue ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.shade,
|
||||
padding: 5,
|
||||
borderRadius: 5,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.accent}>
|
||||
Tap on + to add {`"${inputValue}"`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,292 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useSelectionContext } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { useItemSelectionStore } from "./store";
|
||||
|
||||
const SelectionIndicator = ({
|
||||
item,
|
||||
hasNotes,
|
||||
selectItem,
|
||||
onPress,
|
||||
onChange
|
||||
}) => {
|
||||
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
|
||||
const isSelected = itemState === "selected";
|
||||
const isIntermediate = itemState === "intermediate";
|
||||
const isRemoved = !isSelected && hasNotes;
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemState]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={
|
||||
isRemoved
|
||||
? colors.red
|
||||
: isIntermediate || isSelected
|
||||
? colors.accent
|
||||
: colors.icon
|
||||
}
|
||||
onPress={() => {
|
||||
if (multiSelect) return selectItem();
|
||||
onPress?.(item);
|
||||
}}
|
||||
onLongPress={() => {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
selectItem();
|
||||
}}
|
||||
testID={
|
||||
isRemoved
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: isIntermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
isRemoved
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: isIntermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListItem = ({
|
||||
item,
|
||||
index,
|
||||
icon,
|
||||
infoText,
|
||||
hasSubList,
|
||||
onPress,
|
||||
onScrollEnd,
|
||||
getListItems,
|
||||
style,
|
||||
type,
|
||||
sublistItemType,
|
||||
onAddItem,
|
||||
getSublistItemProps,
|
||||
hasHeaderSearch,
|
||||
onAddSublistItem,
|
||||
hasNotes,
|
||||
onChange,
|
||||
sheetRef
|
||||
}) => {
|
||||
const { toggleSelection } = useSelectionContext();
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
function selectItem() {
|
||||
toggleSelection(item);
|
||||
}
|
||||
|
||||
const getSelectedNotesCountInNotebookTopics = (item) => {
|
||||
if (item.type === "topic") return;
|
||||
|
||||
let count = 0;
|
||||
const noteIds = [];
|
||||
for (let topic of item.topics) {
|
||||
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
|
||||
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
useSelectionStore.getState().selectedItemsList.forEach((item) => {
|
||||
if (noteIds.indexOf(item.id) > -1) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
|
||||
}, [item]);
|
||||
|
||||
const onChangeSubItem = () => {
|
||||
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
marginBottom: 10,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
if (hasSubList) return setExpanded(!expanded);
|
||||
if (multiSelect) return selectItem();
|
||||
onPress?.(item);
|
||||
}}
|
||||
type={type}
|
||||
onLongPress={() => {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
selectItem();
|
||||
}}
|
||||
customStyle={{
|
||||
height: style?.height || 50,
|
||||
width: "100%",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<SelectionIndicator
|
||||
hasNotes={hasNotes}
|
||||
onPress={onPress}
|
||||
item={item}
|
||||
onChange={onChange}
|
||||
selectItem={selectItem}
|
||||
/>
|
||||
<View>
|
||||
{hasSubList && expanded ? (
|
||||
<Heading size={SIZE.md}>{item.title}</Heading>
|
||||
) : (
|
||||
<Paragraph size={SIZE.sm}>{item.title}</Paragraph>
|
||||
)}
|
||||
|
||||
{infoText ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{infoText}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{showSelectedIndicator ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.accent,
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: 100,
|
||||
marginRight: 12
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{onAddSublistItem ? (
|
||||
<IconButton
|
||||
name={"plus"}
|
||||
testID="add-item-icon"
|
||||
color={colors.pri}
|
||||
size={SIZE.xl}
|
||||
onPress={() => {
|
||||
onAddSublistItem(item);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{icon ? (
|
||||
<IconButton
|
||||
name={icon(expanded).name}
|
||||
color={icon(expanded).color}
|
||||
size={icon(expanded).size || SIZE.xl}
|
||||
onPress={
|
||||
hasSubList
|
||||
? () => setExpanded(!expanded)
|
||||
: icon(expanded).onPress
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</PressableButton>
|
||||
|
||||
{expanded && hasSubList ? (
|
||||
<FilteredList
|
||||
nestedScrollEnabled
|
||||
data={getListItems(item)}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
onMomentumScrollEnd={onScrollEnd}
|
||||
style={{
|
||||
width: "95%",
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 250
|
||||
}}
|
||||
itemType={sublistItemType}
|
||||
hasHeaderSearch={hasHeaderSearch}
|
||||
renderItem={({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
{...getSublistItemProps(item)}
|
||||
index={index}
|
||||
onChange={onChangeSubItem}
|
||||
onScrollEnd={onScrollEnd}
|
||||
/>
|
||||
)}
|
||||
onAddItem={onAddItem}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import create, { State } from "zustand";
|
||||
|
||||
type SelectionItemState = Record<
|
||||
string,
|
||||
"intermediate" | "selected" | "deselected"
|
||||
>;
|
||||
|
||||
export interface SelectionStore extends State {
|
||||
itemState: SelectionItemState;
|
||||
setItemState: (state: SelectionItemState) => void;
|
||||
multiSelect: boolean;
|
||||
setMultiSelect: (multiSelect: boolean) => void;
|
||||
}
|
||||
|
||||
export const useItemSelectionStore = create<SelectionStore>((set) => ({
|
||||
itemState: {},
|
||||
setItemState: (itemState) => {
|
||||
set({
|
||||
itemState
|
||||
});
|
||||
},
|
||||
multiSelect: false,
|
||||
setMultiSelect: (multiSelect) => set({ multiSelect })
|
||||
}));
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { RefObject, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -31,7 +31,7 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
|
||||
type ChangeEmailProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: () => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
};
|
||||
|
||||
@@ -277,6 +277,7 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
});
|
||||
});
|
||||
}}
|
||||
height={50}
|
||||
/>
|
||||
<Button
|
||||
title="Share"
|
||||
@@ -300,6 +301,7 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
}).catch(console.log);
|
||||
}
|
||||
}}
|
||||
height={50}
|
||||
/>
|
||||
<Button
|
||||
title="Export in another format"
|
||||
@@ -315,6 +317,7 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
setResult(null);
|
||||
setExporting(false);
|
||||
}}
|
||||
height={50}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
});
|
||||
});
|
||||
}}
|
||||
height={50}
|
||||
/>
|
||||
<Button
|
||||
title="Share"
|
||||
@@ -64,6 +65,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
shareFile: true
|
||||
}).catch(console.log);
|
||||
}}
|
||||
height={50}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -196,6 +196,7 @@ For example:
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
loading={loading}
|
||||
height={50}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
@@ -17,32 +17,51 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useTagStore } from "../../../stores/use-tag-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { eCloseTagsDialog, eOpenTagsDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import Input from "../../ui/input";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
const ManageTagsSheet = (props) => {
|
||||
import { useCallback } from "react";
|
||||
const ManageTagsSheet = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [note, setNote] = useState(props.note);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
const allTags = useTagStore((state) => state.tags);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [query, setQuery] = useState(null);
|
||||
const inputRef = useRef();
|
||||
const [focus, setFocus] = useState(false);
|
||||
const actionSheetRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
sortTags();
|
||||
}, [allTags, note, query, sortTags]);
|
||||
eSubscribeEvent(eOpenTagsDialog, open);
|
||||
eSubscribeEvent(eCloseTagsDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenTagsDialog, open);
|
||||
eUnSubscribeEvent(eCloseTagsDialog, close);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
sortTags();
|
||||
}
|
||||
}, [allTags, note, query, sortTags, visible]);
|
||||
|
||||
const sortTags = useCallback(() => {
|
||||
let _tags = [...allTags];
|
||||
@@ -69,9 +88,26 @@ const ManageTagsSheet = (props) => {
|
||||
setTags(combinedTags);
|
||||
}, [allTags, note, query]);
|
||||
|
||||
const open = useCallback(
|
||||
(item) => {
|
||||
setNote(item);
|
||||
useTagStore.getState().setTags();
|
||||
sortTags();
|
||||
setVisible(true);
|
||||
},
|
||||
[sortTags]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
useTagStore.getState().setTags();
|
||||
}, []);
|
||||
if (visible) {
|
||||
actionSheetRef.current?.show();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const close = () => {
|
||||
setQuery(null);
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
let _query = query;
|
||||
@@ -103,98 +139,115 @@ const ManageTagsSheet = (props) => {
|
||||
});
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
minHeight: focus ? "100%" : "60%"
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
onOpen={async () => {
|
||||
await sleep(300);
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
onClose={async () => {
|
||||
setQuery(null);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
button={{
|
||||
icon: "magnify",
|
||||
color: colors.accent,
|
||||
size: SIZE.lg
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
minHeight: "60%"
|
||||
}}
|
||||
testID="tag-input"
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(db.tags.sanitize(v));
|
||||
}}
|
||||
onFocusInput={() => {
|
||||
setFocus(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocus(false);
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
placeholder="Search or add a tag"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
>
|
||||
{query && query !== tags[0]?.title ? (
|
||||
<PressableButton
|
||||
key={"query_item"}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
onPress={onSubmit}
|
||||
type="accent"
|
||||
>
|
||||
<Heading size={SIZE.sm} color={colors.light}>
|
||||
Add {'"' + "#" + query + '"'}
|
||||
</Heading>
|
||||
<Icon name="plus" color={colors.light} size={SIZE.lg} />
|
||||
</PressableButton>
|
||||
) : null}
|
||||
{!allTags || allTags.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={50} color={colors.icon}>
|
||||
#
|
||||
</Heading>
|
||||
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
|
||||
You do not have any tags.
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
<Input
|
||||
button={{
|
||||
icon: "magnify",
|
||||
color: colors.accent,
|
||||
size: SIZE.lg
|
||||
}}
|
||||
testID="tag-input"
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(db.tags.sanitize(v));
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
height={50}
|
||||
placeholder="Search or add a tag"
|
||||
/>
|
||||
|
||||
{tags.map((item) => (
|
||||
<TagItem key={item.title} tag={item} note={note} setNote={setNote} />
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
>
|
||||
{query && query !== tags[0]?.title ? (
|
||||
<PressableButton
|
||||
key={"query_item"}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
onPress={onSubmit}
|
||||
type="accent"
|
||||
>
|
||||
<Heading size={SIZE.sm} color={colors.light}>
|
||||
Add {'"' + "#" + query + '"'}
|
||||
</Heading>
|
||||
<Icon name="plus" color={colors.light} size={SIZE.lg} />
|
||||
</PressableButton>
|
||||
) : null}
|
||||
{!allTags || allTags.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={50} color={colors.icon}>
|
||||
#
|
||||
</Heading>
|
||||
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
|
||||
You do not have any tags.
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{tags.map((item) => (
|
||||
<TagItem
|
||||
key={item.title}
|
||||
tag={item}
|
||||
note={note}
|
||||
setNote={setNote}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
ManageTagsSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref) => {
|
||||
return <ManageTagsSheet actionSheetRef={ref} note={note} />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default ManageTagsSheet;
|
||||
|
||||
const TagItem = ({ tag, note, setNote }) => {
|
||||
@@ -216,7 +269,13 @@ const TagItem = ({ tag, note, setNote }) => {
|
||||
console.error(e);
|
||||
}
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
}, 1);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ 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 { FlashList } from "@shopify/flash-list";
|
||||
import { NotebookType, NoteType, TopicType } from "app/utils/types";
|
||||
import React, { RefObject, useState } from "react";
|
||||
import { Platform, useWindowDimensions, View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -58,7 +58,7 @@ export const MoveNotes = ({
|
||||
}: {
|
||||
notebook: NotebookType;
|
||||
selectedTopic?: TopicType;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
fwdRef: RefObject<ActionSheet>;
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [currentNotebook, setCurrentNotebook] = useState(notebook);
|
||||
@@ -122,7 +122,15 @@ export const MoveNotes = ({
|
||||
db.notebooks?.notebook(currentNotebook.id).data as NotebookType
|
||||
);
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -269,6 +277,10 @@ export const MoveNotes = ({
|
||||
)}
|
||||
|
||||
<FlashList
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={() => {
|
||||
fwdRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -310,7 +322,15 @@ export const MoveNotes = ({
|
||||
},
|
||||
...selectedNoteIds
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
SearchService.updateAndSearch();
|
||||
eSendEvent(eCloseSheet);
|
||||
}}
|
||||
@@ -325,7 +345,7 @@ export const MoveNotes = ({
|
||||
|
||||
MoveNotes.present = (notebook: NotebookType, topic: TopicType) => {
|
||||
presentSheet({
|
||||
component: (ref: RefObject<ActionSheetRef>) => (
|
||||
component: (ref: RefObject<ActionSheet>) => (
|
||||
<MoveNotes fwdRef={ref} notebook={notebook} selectedTopic={topic} />
|
||||
)
|
||||
});
|
||||
|
||||
@@ -93,13 +93,7 @@ NewFeature.present = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!version || version === getVersion()) {
|
||||
SettingsService.set({
|
||||
version: getVersion()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version && version === getVersion()) return false;
|
||||
SettingsService.set({
|
||||
version: getVersion()
|
||||
});
|
||||
|
||||
@@ -18,14 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useAttachmentStore } from "../../../stores/use-attachment-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import {
|
||||
eClosePublishNoteDialog,
|
||||
eOpenPublishNoteDialog
|
||||
} from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
@@ -33,38 +41,75 @@ import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const PublishNoteSheet = ({ note: item, update }) => {
|
||||
let passwordValue = null;
|
||||
const PublishNoteSheet = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const actionSheetRef = useRef();
|
||||
const loading = useAttachmentStore((state) => state.loading);
|
||||
const [selfDestruct, setSelfDestruct] = useState(false);
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const [note, setNote] = useState(item);
|
||||
const [note, setNote] = useState(null);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const publishUrl =
|
||||
note &&
|
||||
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
|
||||
const isPublished = note && db?.monographs.isPublished(note?.id);
|
||||
const pwdInput = useRef();
|
||||
const passwordValue = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenPublishNoteDialog, open);
|
||||
eSubscribeEvent(eClosePublishNoteDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenPublishNoteDialog, open);
|
||||
eUnSubscribeEvent(eClosePublishNoteDialog, close);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = (item) => {
|
||||
if (!item) return;
|
||||
setNote(item);
|
||||
setPublishing(false);
|
||||
setSelfDestruct(false);
|
||||
setIsLocked(false);
|
||||
setVisible(true);
|
||||
passwordValue = null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
actionSheetRef.current?.show();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const close = () => {
|
||||
passwordValue = null;
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
const publishNote = async () => {
|
||||
if (publishing) return;
|
||||
setPublishLoading(true);
|
||||
setPublishing(true);
|
||||
|
||||
try {
|
||||
if (note?.id) {
|
||||
if (isLocked && !passwordValue) return;
|
||||
await db.monographs.publish(note.id, {
|
||||
selfDestruct: selfDestruct,
|
||||
password: isLocked && passwordValue.current
|
||||
password: isLocked && passwordValue
|
||||
});
|
||||
setNote(db.notes.note(note.id)?.data);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setPublishLoading(false);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
@@ -75,22 +120,23 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
});
|
||||
}
|
||||
|
||||
setPublishLoading(false);
|
||||
};
|
||||
|
||||
const setPublishLoading = (value) => {
|
||||
setPublishing(value);
|
||||
setPublishing(false);
|
||||
};
|
||||
|
||||
const deletePublishedNote = async () => {
|
||||
if (publishing) return;
|
||||
setPublishLoading(true);
|
||||
setPublishing(true);
|
||||
try {
|
||||
if (note?.id) {
|
||||
await db.monographs.unpublish(note.id);
|
||||
setNote(db.notes.note(note.id)?.data);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setPublishLoading(false);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
@@ -101,281 +147,267 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
});
|
||||
}
|
||||
actionSheetRef.current?.hide();
|
||||
setPublishLoading(false);
|
||||
setPublishing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
closeOnTouchBackdrop={!publishing}
|
||||
gestureEnabled={!publishing}
|
||||
onClose={async () => {
|
||||
passwordValue = null;
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={note?.title}
|
||||
paragraph={`Anyone with the link${
|
||||
isLocked ? " and password" : ""
|
||||
} of the published note can view it.`}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={note?.title}
|
||||
paragraph={`Anyone with the link${
|
||||
isLocked ? " and password" : ""
|
||||
} of the published note can view it.`}
|
||||
/>
|
||||
|
||||
{publishing ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
height: 150,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size={25} color={colors.accent} />
|
||||
<Paragraph
|
||||
{publishing ? (
|
||||
<View
|
||||
style={{
|
||||
textAlign: "center"
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
height: 150,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
Please wait...
|
||||
{loading && loading.current && loading.total
|
||||
? `\nDownloading attachments (${
|
||||
loading?.current / loading?.total
|
||||
})`
|
||||
: ""}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{isPublished && (
|
||||
<View
|
||||
<ActivityIndicator size={25} color={colors.accent} />
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Please wait...
|
||||
{loading && loading.current && loading.total
|
||||
? `\nDownloading attachments (${
|
||||
loading?.current / loading?.total
|
||||
})`
|
||||
: ""}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{isPublished && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 15,
|
||||
backgroundColor: colors.nav,
|
||||
padding: 12,
|
||||
borderRadius: 5
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.sm}>Published at:</Heading>
|
||||
<Paragraph size={SIZE.xs} numberOfLines={1}>
|
||||
{publishUrl}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(publishUrl, colors.accent);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 5,
|
||||
color: colors.pri
|
||||
}}
|
||||
>
|
||||
<Icon color={colors.accent} name="open-in-new" /> Open in
|
||||
browser
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
Clipboard.setString(publishUrl);
|
||||
ToastEvent.show({
|
||||
heading: "Note publish url copied",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
color={colors.accent}
|
||||
size={SIZE.lg}
|
||||
name="content-copy"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Seperator />
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 10,
|
||||
backgroundColor: colors.nav,
|
||||
padding: 12,
|
||||
borderRadius: 5
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
color={isLocked ? colors.accent : colors.icon}
|
||||
size={SIZE.lg}
|
||||
name={
|
||||
isLocked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>Published at:</Heading>
|
||||
<Paragraph size={SIZE.sm} numberOfLines={1}>
|
||||
{publishUrl}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(publishUrl, colors.accent);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 5,
|
||||
color: colors.pri
|
||||
}}
|
||||
>
|
||||
<Icon color={colors.accent} name="open-in-new" /> Open in
|
||||
browser
|
||||
<Heading size={SIZE.md}>Password protection</Heading>
|
||||
<Paragraph>
|
||||
Published note can only be viewed by someone with the
|
||||
password.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
Clipboard.setString(publishUrl);
|
||||
ToastEvent.show({
|
||||
heading: "Note publish url copied",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
color={colors.accent}
|
||||
size={SIZE.lg}
|
||||
name="content-copy"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
backgroundColor: colors.nav,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
color={isLocked ? colors.accent : colors.icon}
|
||||
size={SIZE.xl}
|
||||
name={
|
||||
isLocked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>Password protection</Heading>
|
||||
<Paragraph>
|
||||
Published note can only be viewed by someone with the password.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setSelfDestruct(!selfDestruct);
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.nav,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 5
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setSelfDestruct(!selfDestruct);
|
||||
}}
|
||||
color={selfDestruct ? colors.accent : colors.icon}
|
||||
size={SIZE.xl}
|
||||
name={
|
||||
selfDestruct
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>Self destruct</Heading>
|
||||
<Paragraph>
|
||||
Published note link will be automatically deleted once it is
|
||||
viewed by someone.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{isLocked ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={pwdInput}
|
||||
onChangeText={(value) => (passwordValue.current = value)}
|
||||
blurOnSubmit
|
||||
secureTextEntry
|
||||
defaultValue={passwordValue.current}
|
||||
placeholder="Enter Password"
|
||||
/>
|
||||
<Seperator half />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "center"
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{isPublished && (
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
setSelfDestruct(!selfDestruct);
|
||||
}}
|
||||
color={selfDestruct ? colors.accent : colors.icon}
|
||||
size={SIZE.lg}
|
||||
name={
|
||||
selfDestruct
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>Self destruct</Heading>
|
||||
<Paragraph>
|
||||
Published note link will be automatically deleted once it is
|
||||
viewed by someone.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{isLocked ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={deletePublishedNote}
|
||||
fontSize={SIZE.md}
|
||||
type="error"
|
||||
title="Unpublish"
|
||||
style={{
|
||||
width: "49%"
|
||||
}}
|
||||
<Input
|
||||
fwdRef={pwdInput}
|
||||
onChangeText={(value) => (passwordValue = value)}
|
||||
blurOnSubmit
|
||||
secureTextEntry
|
||||
defaultValue={passwordValue}
|
||||
placeholder="Enter Password"
|
||||
/>
|
||||
<Seperator half />
|
||||
</>
|
||||
)}
|
||||
<Seperator half />
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onPress={publishNote}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
style={{
|
||||
width: isPublished ? "49%" : 250,
|
||||
borderRadius: isPublished ? 5 : 100
|
||||
marginTop: 10
|
||||
}}
|
||||
height={50}
|
||||
type="accent"
|
||||
title={isPublished ? "Update" : "Publish"}
|
||||
title={isPublished ? "Update published note" : "Publish note"}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10,
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://docs.notesnook.com/monographs/",
|
||||
colors.accent
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Learn more about Notesnook Monograph
|
||||
</Paragraph>
|
||||
</View>
|
||||
{isPublished && (
|
||||
<>
|
||||
<Seperator half />
|
||||
<Button
|
||||
onPress={deletePublishedNote}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
height={50}
|
||||
type="error"
|
||||
title="Unpublish note"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 5,
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://docs.notesnook.com/monographs/",
|
||||
colors.accent
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Learn more about Notesnook Monograph
|
||||
</Paragraph>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
PublishNoteSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref, close, update) => (
|
||||
<PublishNoteSheet
|
||||
actionSheetRef={ref}
|
||||
close={close}
|
||||
update={update}
|
||||
note={note}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default PublishNoteSheet;
|
||||
|
||||
@@ -96,6 +96,7 @@ const RateAppSheet = () => {
|
||||
onPress={rateApp}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
height={50}
|
||||
type="accent"
|
||||
title="Rate now (It takes only a second)"
|
||||
/>
|
||||
@@ -120,12 +121,14 @@ const RateAppSheet = () => {
|
||||
fontSize={SIZE.md}
|
||||
type="error"
|
||||
width="48%"
|
||||
height={50}
|
||||
title="Never"
|
||||
/>
|
||||
<Button
|
||||
onPress={onClose}
|
||||
fontSize={SIZE.md}
|
||||
width="48%"
|
||||
height={50}
|
||||
type="grayBg"
|
||||
title="Later"
|
||||
/>
|
||||
|
||||
@@ -298,6 +298,7 @@ class RecoveryKeySheet extends React.Component {
|
||||
width="100%"
|
||||
type="grayAccent"
|
||||
fontSize={SIZE.md}
|
||||
height={50}
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
@@ -307,6 +308,7 @@ class RecoveryKeySheet extends React.Component {
|
||||
type="grayAccent"
|
||||
fontSize={SIZE.md}
|
||||
icon="qrcode"
|
||||
height={50}
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
@@ -316,6 +318,7 @@ class RecoveryKeySheet extends React.Component {
|
||||
type="grayAccent"
|
||||
icon="text"
|
||||
fontSize={SIZE.md}
|
||||
height={50}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
@@ -326,6 +329,7 @@ class RecoveryKeySheet extends React.Component {
|
||||
type="grayAccent"
|
||||
icon="cloud"
|
||||
fontSize={SIZE.md}
|
||||
height={50}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
@@ -345,6 +349,7 @@ class RecoveryKeySheet extends React.Component {
|
||||
<Button
|
||||
title="I have saved the key."
|
||||
width="100%"
|
||||
height={50}
|
||||
type="error"
|
||||
fontSize={SIZE.md}
|
||||
onPress={this.close}
|
||||
|
||||
@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
presentSheet,
|
||||
PresentSheetOptions
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { Reminder } from "../../../services/notifications";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
@@ -38,7 +37,7 @@ import { PressableButtonProps } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type RelationsListProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: () => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
item: { id: string; type: string };
|
||||
@@ -63,6 +62,8 @@ const IconsByType = {
|
||||
|
||||
export const RelationsList = ({
|
||||
actionSheetRef,
|
||||
close,
|
||||
update,
|
||||
item,
|
||||
referenceType,
|
||||
relationType,
|
||||
@@ -74,6 +75,7 @@ export const RelationsList = ({
|
||||
const [items, setItems] = useState<Reminder[]>([]);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const hasNoRelations = !items || items.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
setItems(
|
||||
db.relations?.[relationType]?.(
|
||||
@@ -121,7 +123,6 @@ export const RelationsList = ({
|
||||
) : (
|
||||
<List
|
||||
listData={items}
|
||||
ScrollComponent={FlashList}
|
||||
loading={false}
|
||||
type={referenceType}
|
||||
headerProps={null}
|
||||
|
||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import dayjs from "dayjs";
|
||||
import React, { RefObject } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type ReminderSheetProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: () => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
reminder?: Reminder;
|
||||
|
||||
@@ -17,13 +17,19 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { RefObject, useRef, useState } from "react";
|
||||
import { Platform, TextInput, View } from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import DateTimePickerModal from "react-native-modal-datetime-picker";
|
||||
import {
|
||||
presentSheet,
|
||||
PresentSheetOptions,
|
||||
ToastEvent,
|
||||
presentSheet
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
@@ -33,20 +39,16 @@ import Input from "../../ui/input";
|
||||
import dayjs from "dayjs";
|
||||
import DatePicker from "react-native-date-picker";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import Notifications, { Reminder } from "../../../services/notifications";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { NoteType } from "../../../utils/types";
|
||||
import { Dialog } from "../../dialog";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type ReminderSheetProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: (ctx?: string) => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
reminder?: Reminder;
|
||||
@@ -113,16 +115,10 @@ export default function ReminderSheet({
|
||||
>(reminder?.priority || SettingsService.get().reminderNotificationMode);
|
||||
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
|
||||
const [repeatFrequency, setRepeatFrequency] = useState(1);
|
||||
const referencedItem = reference
|
||||
? (db.notes?.note(reference.id)?.data as NoteType)
|
||||
: null;
|
||||
const title = useRef<string | undefined>(
|
||||
reminder?.title || referencedItem?.title
|
||||
);
|
||||
const title = useRef<string | undefined>(reminder?.title);
|
||||
const details = useRef<string | undefined>(reminder?.description);
|
||||
const titleRef = useRef<TextInput>(null);
|
||||
const timer = useRef<NodeJS.Timeout>();
|
||||
|
||||
const { height } = useWindowDimensions();
|
||||
const showDatePicker = () => {
|
||||
setDatePickerVisibility(true);
|
||||
};
|
||||
@@ -132,10 +128,9 @@ export default function ReminderSheet({
|
||||
};
|
||||
|
||||
const handleConfirm = (date: Date) => {
|
||||
timer.current = setTimeout(() => {
|
||||
hideDatePicker();
|
||||
setDate(date);
|
||||
}, 50);
|
||||
hideDatePicker();
|
||||
setDate(date);
|
||||
console.log(date);
|
||||
};
|
||||
function nth(n: number) {
|
||||
return (
|
||||
@@ -161,7 +156,7 @@ export default function ReminderSheet({
|
||||
|
||||
async function saveReminder() {
|
||||
try {
|
||||
if (!(await Notifications.checkAndRequestPermissions(true)))
|
||||
if (!(await Notifications.checkAndRequestPermissions()))
|
||||
throw new Error(
|
||||
"App does not have permission to schedule notifications"
|
||||
);
|
||||
@@ -200,8 +195,7 @@ export default function ReminderSheet({
|
||||
|
||||
if (!_reminder) {
|
||||
ToastEvent.show({
|
||||
heading: "Failed to add a new reminder",
|
||||
context: "local"
|
||||
heading: "Failed to add a new reminder"
|
||||
});
|
||||
}
|
||||
if (reference) {
|
||||
@@ -211,73 +205,47 @@ export default function ReminderSheet({
|
||||
});
|
||||
}
|
||||
Notifications.scheduleNotification(_reminder as Reminder);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"Notes",
|
||||
"NotesPage",
|
||||
"Reminders",
|
||||
"Favorites",
|
||||
"TopicNotes"
|
||||
);
|
||||
useRelationStore.getState().update();
|
||||
close?.();
|
||||
} catch (e) {
|
||||
ToastEvent.error(e as Error, undefined, "local");
|
||||
ToastEvent.error(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: DDS.isTab ? "90%" : "99.99%"
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<ScrollView
|
||||
onScrollEndDrag={() => actionSheetRef.current?.handleChildScrollEnd()}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
maxHeight: height * 0.85
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>Set reminder</Heading>
|
||||
<Button
|
||||
title="Save"
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
onPress={saveReminder}
|
||||
/>
|
||||
</View>
|
||||
<Dialog context="local" />
|
||||
<ScrollView>
|
||||
<Input
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
defaultValue={reminder?.title}
|
||||
placeholder="Remind me of..."
|
||||
onChangeText={(text) => (title.current = text)}
|
||||
wrapperStyle={{
|
||||
marginTop: 10
|
||||
}}
|
||||
containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={
|
||||
reminder ? reminder?.description : referencedItem?.headline
|
||||
}
|
||||
placeholder="Add a short note"
|
||||
defaultValue={reminder?.description}
|
||||
placeholder="Add a quick note"
|
||||
onChangeText={(text) => (details.current = text)}
|
||||
containerStyle={{
|
||||
maxHeight: 80
|
||||
}}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
inputStyle={{
|
||||
minHeight: 80,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
height={80}
|
||||
wrapperStyle={{
|
||||
marginBottom: 20
|
||||
}}
|
||||
containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
|
||||
/>
|
||||
|
||||
<View
|
||||
@@ -310,16 +278,6 @@ export default function ReminderSheet({
|
||||
mode as keyof typeof ReminderModes
|
||||
] as Reminder["mode"]
|
||||
);
|
||||
if (mode === "Repeat") {
|
||||
setSelectedDays((days) => {
|
||||
if (days.length > 0) return days;
|
||||
if (days.indexOf(date.getDay()) > -1) {
|
||||
return days;
|
||||
}
|
||||
days.push(date.getDay());
|
||||
return [...days];
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -461,13 +419,11 @@ export default function ReminderSheet({
|
||||
|
||||
<DatePicker
|
||||
date={date}
|
||||
maximumDate={dayjs(date).add(3, "months").toDate()}
|
||||
onDateChange={handleConfirm}
|
||||
textColor={colors.night ? "#ffffff" : "#000000"}
|
||||
fadeToColor={colors.bg}
|
||||
theme={colors.night ? "dark" : "light"}
|
||||
is24hourSource="locale"
|
||||
androidVariant="nativeAndroid"
|
||||
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
|
||||
/>
|
||||
|
||||
@@ -585,6 +541,15 @@ export default function ReminderSheet({
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
<Button
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
title="Save"
|
||||
type="accent"
|
||||
fontSize={SIZE.md}
|
||||
onPress={saveReminder}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -596,8 +561,6 @@ ReminderSheet.present = (
|
||||
) => {
|
||||
presentSheet({
|
||||
context: isSheet ? "local" : undefined,
|
||||
enableGesturesInScrollView: true,
|
||||
noBottomPadding: true,
|
||||
component: (ref, close, update) => (
|
||||
<ReminderSheet
|
||||
actionSheetRef={ref}
|
||||
|
||||
@@ -18,10 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "@notesnook/core/common";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { createRef, useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
@@ -35,7 +35,7 @@ import { initialize } from "../../../stores";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { timeConverter } from "../../../utils/time";
|
||||
import { sleep, timeConverter } from "../../../utils/time";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
@@ -44,18 +44,12 @@ import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
const actionSheetRef = createRef();
|
||||
let RNFetchBlob;
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const sheet = useRef();
|
||||
useEffect(() => {
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
sheet.current?.show();
|
||||
}, 1);
|
||||
};
|
||||
eSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eSubscribeEvent(eCloseRestoreDialog, close);
|
||||
return () => {
|
||||
@@ -64,15 +58,21 @@ const RestoreDataSheet = () => {
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
await sleep(30);
|
||||
actionSheetRef.current?.setModalVisible(true);
|
||||
};
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (restoring) {
|
||||
showIsWorking();
|
||||
return;
|
||||
}
|
||||
sheet.current?.hide();
|
||||
actionSheetRef.current?.setModalVisible(false);
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 150);
|
||||
}, 300);
|
||||
}, [restoring]);
|
||||
|
||||
const showIsWorking = () => {
|
||||
@@ -86,19 +86,15 @@ const RestoreDataSheet = () => {
|
||||
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
fwdRef={sheet}
|
||||
fwdRef={actionSheetRef}
|
||||
gestureEnabled={!restoring}
|
||||
closeOnTouchBackdrop={!restoring}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
close();
|
||||
}}
|
||||
onClose={close}
|
||||
>
|
||||
<RestoreDataComponent
|
||||
close={close}
|
||||
restoring={restoring}
|
||||
setRestoring={setRestoring}
|
||||
actionSheetRef={sheet}
|
||||
/>
|
||||
<Toast context="local" />
|
||||
</SheetWrapper>
|
||||
@@ -113,6 +109,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backupDirectoryAndroid, setBackupDirectoryAndroid] = useState(false);
|
||||
const [progress, setProgress] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = db.eventManager.subscribe(
|
||||
EVENTS.migrationProgress,
|
||||
@@ -126,9 +123,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 300);
|
||||
checkBackups();
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
@@ -367,6 +362,10 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
</View>
|
||||
<Seperator half />
|
||||
<FlatList
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
!restoring ? (
|
||||
loading ? (
|
||||
|
||||
@@ -25,22 +25,23 @@ import Navigation from "../../../services/navigation";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { GROUP, SORT } from "../../../utils/constants";
|
||||
import { refreshNotesPage } from "../../../utils/events";
|
||||
import layoutmanager from "../../../utils/layout-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
const Sort = ({ type, screen }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const isTopicSheet = screen === "TopicSheet";
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings.getGroupOptions(type)
|
||||
);
|
||||
const updateGroupOptions = async (_groupOptions) => {
|
||||
await db.settings.setGroupOptions(type, _groupOptions);
|
||||
|
||||
layoutmanager.withSpringAnimation(600);
|
||||
setGroupOptions(_groupOptions);
|
||||
setTimeout(() => {
|
||||
if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen);
|
||||
Navigation.queueRoutesForUpdate(screen);
|
||||
eSendEvent("groupOptionsUpdate");
|
||||
eSendEvent(refreshNotesPage);
|
||||
}, 1);
|
||||
@@ -51,9 +52,6 @@ const Sort = ({ type, screen }) => {
|
||||
...groupOptions,
|
||||
sortDirection: groupOptions.sortDirection === "asc" ? "desc" : "asc"
|
||||
};
|
||||
if (type === "topics") {
|
||||
_groupOptions.groupBy = "none";
|
||||
}
|
||||
await updateGroupOptions(_groupOptions);
|
||||
};
|
||||
|
||||
@@ -102,7 +100,7 @@ const Sort = ({ type, screen }) => {
|
||||
height={25}
|
||||
iconPosition="right"
|
||||
fontSize={SIZE.sm - 1}
|
||||
type="transparent"
|
||||
type="grayBg"
|
||||
buttonType={{
|
||||
text: colors.accent
|
||||
}}
|
||||
@@ -121,7 +119,7 @@ const Sort = ({ type, screen }) => {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
flexWrap: "wrap",
|
||||
borderBottomWidth: isTopicSheet ? 0 : 1,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.nav,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
@@ -168,9 +166,6 @@ const Sort = ({ type, screen }) => {
|
||||
...groupOptions,
|
||||
sortBy: type === "trash" ? "dateDeleted" : item
|
||||
};
|
||||
if (type === "topics") {
|
||||
_groupOptions.groupBy = "none";
|
||||
}
|
||||
await updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
iconSize={SIZE.md}
|
||||
@@ -180,70 +175,64 @@ const Sort = ({ type, screen }) => {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isTopicSheet ? null : (
|
||||
<>
|
||||
<Heading
|
||||
style={{
|
||||
marginLeft: 12
|
||||
<Heading
|
||||
style={{
|
||||
marginLeft: 12
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
Group by
|
||||
</Heading>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{Object.keys(GROUP).map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
testID={"btn-" + item}
|
||||
type={groupOptions.groupBy === GROUP[item] ? "grayBg" : "gray"}
|
||||
buttonType={{
|
||||
text:
|
||||
groupOptions.groupBy === GROUP[item]
|
||||
? colors.accent
|
||||
: colors.icon
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
Group by
|
||||
</Heading>
|
||||
onPress={async () => {
|
||||
let _groupOptions = {
|
||||
...groupOptions,
|
||||
groupBy: GROUP[item]
|
||||
};
|
||||
|
||||
<Seperator />
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{Object.keys(GROUP).map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
testID={"btn-" + item}
|
||||
type={groupOptions.groupBy === GROUP[item] ? "grayBg" : "gray"}
|
||||
buttonType={{
|
||||
text:
|
||||
groupOptions.groupBy === GROUP[item]
|
||||
? colors.accent
|
||||
: colors.icon
|
||||
}}
|
||||
onPress={async () => {
|
||||
let _groupOptions = {
|
||||
...groupOptions,
|
||||
groupBy: GROUP[item]
|
||||
};
|
||||
|
||||
if (item === "abc") {
|
||||
_groupOptions.sortBy = "title";
|
||||
_groupOptions.sortDirection = "asc";
|
||||
} else {
|
||||
if (groupOptions.sortBy === "title") {
|
||||
_groupOptions.sortBy = "dateEdited";
|
||||
_groupOptions.sortDirection = "desc";
|
||||
}
|
||||
}
|
||||
updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
height={40}
|
||||
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
|
||||
title={
|
||||
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
|
||||
if (item === "abc") {
|
||||
_groupOptions.sortBy = "title";
|
||||
_groupOptions.sortDirection = "asc";
|
||||
} else {
|
||||
if (groupOptions.sortBy === "title") {
|
||||
_groupOptions.sortBy = "dateEdited";
|
||||
_groupOptions.sortDirection = "desc";
|
||||
}
|
||||
style={{
|
||||
paddingHorizontal: 8,
|
||||
marginBottom: 10,
|
||||
marginRight: 10
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
height={40}
|
||||
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
|
||||
title={item.slice(0, 1).toUpperCase() + item.slice(1, item.length)}
|
||||
style={{
|
||||
paddingHorizontal: 8,
|
||||
marginBottom: 10,
|
||||
marginRight: 10
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,556 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import qclone from "qclone";
|
||||
import React, {
|
||||
createContext,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import ActionSheet, {
|
||||
ActionSheetRef,
|
||||
FlatList
|
||||
} from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { IconButton } from "../../../components/ui/icon-button";
|
||||
import { PressableButton } from "../../../components/ui/pressable";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import useNavigationStore, {
|
||||
NotebookScreenParams
|
||||
} from "../../../stores/use-navigation-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import {
|
||||
eOnNewTopicAdded,
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddTopicDialog
|
||||
} from "../../../utils/events";
|
||||
import { normalize, SIZE } from "../../../utils/size";
|
||||
import { GroupHeader, NotebookType, TopicType } from "../../../utils/types";
|
||||
|
||||
import { groupArray } from "@notesnook/core/utils/grouping";
|
||||
import Config from "react-native-config";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { openEditor } from "../../../screens/notes/common";
|
||||
import { getTotalNotes, history } from "../../../utils";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Properties } from "../../properties";
|
||||
import Sort from "../sort";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
|
||||
type ConfigItem = { id: string; type: string };
|
||||
class TopicSheetConfig {
|
||||
static storageKey: "$$sp";
|
||||
|
||||
static makeId(item: ConfigItem) {
|
||||
return `${TopicSheetConfig.storageKey}:${item.type}:${item.id}`;
|
||||
}
|
||||
|
||||
static get(item: ConfigItem) {
|
||||
return MMKV.getInt(TopicSheetConfig.makeId(item)) || 0;
|
||||
}
|
||||
|
||||
static set(item: ConfigItem, index = 0) {
|
||||
MMKV.setInt(TopicSheetConfig.makeId(item), index);
|
||||
}
|
||||
}
|
||||
|
||||
export const TopicsSheet = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const canShow =
|
||||
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes";
|
||||
const [notebook, setNotebook] = useState(
|
||||
canShow
|
||||
? db.notebooks?.notebook(
|
||||
currentScreen?.notebookId || currentScreen?.id || ""
|
||||
)?.data
|
||||
: null
|
||||
);
|
||||
const [selection, setSelection] = useState<TopicType[]>([]);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const ref = useRef<ActionSheetRef>(null);
|
||||
const isTopic = currentScreen.name === "TopicNotes";
|
||||
const [topics, setTopics] = useState(
|
||||
notebook
|
||||
? qclone(
|
||||
groupArray(notebook.topics, db.settings?.getGroupOptions("topics"))
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions("topics")
|
||||
);
|
||||
|
||||
const onRequestUpdate = React.useCallback(
|
||||
(data?: NotebookScreenParams) => {
|
||||
if (!canShow) return;
|
||||
if (!data) data = { item: notebook } as NotebookScreenParams;
|
||||
const _notebook = db.notebooks?.notebook(data.item?.id)
|
||||
?.data as NotebookType;
|
||||
if (_notebook) {
|
||||
setNotebook(_notebook);
|
||||
|
||||
setTopics(
|
||||
qclone(
|
||||
groupArray(_notebook.topics, db.settings?.getGroupOptions("topics"))
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
[canShow, notebook]
|
||||
);
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
setGroupOptions({ ...(db.settings?.getGroupOptions("topics") as any) });
|
||||
onRequestUpdate();
|
||||
}, [onRequestUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
const onTopicUpdate = () => {
|
||||
setTimeout(() => {
|
||||
onRequestUpdate();
|
||||
}, 1);
|
||||
};
|
||||
eSubscribeEvent(eOnTopicSheetUpdate, onTopicUpdate);
|
||||
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOnTopicSheetUpdate, onRequestUpdate);
|
||||
eUnSubscribeEvent(eOnNewTopicAdded, onTopicUpdate);
|
||||
};
|
||||
}, [onRequestUpdate]);
|
||||
|
||||
const PLACEHOLDER_DATA = {
|
||||
heading: "Topics",
|
||||
paragraph: "You have not added any topics yet.",
|
||||
button: "Add first topic",
|
||||
action: () => {
|
||||
eSendEvent(eOpenAddTopicDialog, { notebookId: notebook.id });
|
||||
},
|
||||
loading: "Loading notebook topics"
|
||||
};
|
||||
|
||||
const renderTopic = ({
|
||||
item,
|
||||
index
|
||||
}: {
|
||||
item: TopicType | GroupHeader;
|
||||
index: number;
|
||||
}) =>
|
||||
(item as GroupHeader).type === "header" ? null : (
|
||||
<TopicItem sheetRef={ref} item={item as TopicType} index={index} />
|
||||
);
|
||||
|
||||
const selectionContext = {
|
||||
selection: selection,
|
||||
enabled,
|
||||
setEnabled,
|
||||
toggleSelection: (item: TopicType) => {
|
||||
setSelection((state) => {
|
||||
const selection = [...state];
|
||||
const index = selection.findIndex(
|
||||
(selected) => selected.id === item.id
|
||||
);
|
||||
if (index > -1) {
|
||||
selection.splice(index, 1);
|
||||
if (selection.length === 0) {
|
||||
setEnabled(false);
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
selection.push(item);
|
||||
return selection;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (canShow) {
|
||||
setTimeout(() => {
|
||||
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
|
||||
const notebook = db.notebooks?.notebook(id as string)?.data;
|
||||
const snapPoint = isTopic
|
||||
? 0
|
||||
: TopicSheetConfig.get({
|
||||
type: isTopic ? "topic" : "notebook",
|
||||
id: currentScreen.id as string
|
||||
});
|
||||
|
||||
if (ref.current?.isOpen()) {
|
||||
ref.current?.snapToIndex(snapPoint);
|
||||
} else {
|
||||
ref.current?.show(snapPoint);
|
||||
}
|
||||
if (notebook) {
|
||||
onRequestUpdate({
|
||||
item: notebook
|
||||
} as any);
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
ref.current?.hide();
|
||||
}
|
||||
}, [
|
||||
canShow,
|
||||
currentScreen?.id,
|
||||
currentScreen.name,
|
||||
currentScreen?.notebookId,
|
||||
onRequestUpdate,
|
||||
isTopic
|
||||
]);
|
||||
|
||||
return (
|
||||
<ActionSheet
|
||||
ref={ref}
|
||||
isModal={false}
|
||||
containerStyle={{
|
||||
maxHeight: 300,
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15,
|
||||
backgroundColor: colors.bg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.nav,
|
||||
borderBottomWidth: 0
|
||||
}}
|
||||
openAnimationConfig={{
|
||||
friction: 10
|
||||
}}
|
||||
onSnapIndexChange={(index) => {
|
||||
setCollapsed(index === 0);
|
||||
TopicSheetConfig.set(
|
||||
{
|
||||
type: isTopic ? "topic" : "notebook",
|
||||
id: currentScreen.id as string
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
closable={!canShow}
|
||||
elevation={10}
|
||||
indicatorStyle={{
|
||||
width: 100,
|
||||
backgroundColor: colors.nav
|
||||
}}
|
||||
keyboardHandlerEnabled={false}
|
||||
snapPoints={Config.isTesting === "true" ? [100] : [25, 100]}
|
||||
initialSnapIndex={1}
|
||||
backgroundInteractionEnabled
|
||||
gestureEnabled
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
marginTop: -80
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={"accent"}
|
||||
accentText="light"
|
||||
onPress={openEditor}
|
||||
customStyle={{
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: normalize(60),
|
||||
width: normalize(60)
|
||||
}}
|
||||
>
|
||||
<Icon name="plus" color="white" size={SIZE.xxl} />
|
||||
</View>
|
||||
</PressableButton>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
height: 300,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 12,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
TOPICS
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{enabled ? (
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
history.selectedItemsList = selection;
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
selection.length > 1 ? "topics" : "topics"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selection.length > 1 ? "these topics?" : "this topic?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
await deleteItems();
|
||||
history.selectedItemsList = [];
|
||||
setEnabled(false);
|
||||
setSelection([]);
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
return;
|
||||
}}
|
||||
color={colors.pri}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
name="delete"
|
||||
size={22}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<IconButton
|
||||
name={
|
||||
groupOptions?.sortDirection === "asc"
|
||||
? "sort-ascending"
|
||||
: "sort-descending"
|
||||
}
|
||||
onPress={() => {
|
||||
presentSheet({
|
||||
component: <Sort screen="TopicSheet" type="topics" />
|
||||
});
|
||||
}}
|
||||
testID="group-topic-button"
|
||||
color={colors.pri}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
name="plus"
|
||||
onPress={PLACEHOLDER_DATA.action}
|
||||
testID="add-topic-button"
|
||||
color={colors.pri}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={collapsed ? "chevron-up" : "chevron-down"}
|
||||
onPress={() => {
|
||||
if (ref.current?.currentSnapIndex() !== 0) {
|
||||
setCollapsed(true);
|
||||
ref.current?.snapToIndex(0);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
ref.current?.snapToIndex(1);
|
||||
}
|
||||
}}
|
||||
color={colors.pri}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<SelectionContext.Provider value={selectionContext}>
|
||||
<FlatList
|
||||
data={topics}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={false}
|
||||
onRefresh={() => {
|
||||
onRequestUpdate();
|
||||
}}
|
||||
colors={[colors.accent]}
|
||||
progressBackgroundColor={colors.bg}
|
||||
/>
|
||||
}
|
||||
keyExtractor={(item) => (item as TopicType).id}
|
||||
renderItem={renderTopic}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 200
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon}>No topics</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
/>
|
||||
</SelectionContext.Provider>
|
||||
</View>
|
||||
</ActionSheet>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectionContext = createContext<{
|
||||
selection: TopicType[];
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
toggleSelection: (item: TopicType) => void;
|
||||
}>({
|
||||
selection: [],
|
||||
enabled: false,
|
||||
setEnabled: (_value: boolean) => {},
|
||||
toggleSelection: (_item: TopicType) => {}
|
||||
});
|
||||
const useSelection = () => useContext(SelectionContext);
|
||||
|
||||
const TopicItem = ({
|
||||
item,
|
||||
index,
|
||||
sheetRef
|
||||
}: {
|
||||
item: TopicType;
|
||||
index: number;
|
||||
sheetRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const screen = useNavigationStore((state) => state.currentScreen);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const selection = useSelection();
|
||||
const isSelected =
|
||||
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
|
||||
const isFocused = screen.id === item.id;
|
||||
const notesCount = getTotalNotes(item);
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
type={isSelected || isFocused ? "grayBg" : "transparent"}
|
||||
onLongPress={() => {
|
||||
if (selection.enabled) return;
|
||||
selection.setEnabled(true);
|
||||
selection.toggleSelection(item);
|
||||
}}
|
||||
testID={`topic-sheet-item-${index}`}
|
||||
onPress={() => {
|
||||
if (selection.enabled) {
|
||||
selection.toggleSelection(item);
|
||||
return;
|
||||
}
|
||||
TopicNotes.navigate(item, true);
|
||||
}}
|
||||
customStyle={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 0
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{selection.enabled ? (
|
||||
<IconButton
|
||||
size={SIZE.lg}
|
||||
color={isSelected ? colors.accent : colors.icon}
|
||||
name={
|
||||
isSelected
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Paragraph size={SIZE.sm}>
|
||||
{item.title}{" "}
|
||||
{notesCount ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{notesCount}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<IconButton
|
||||
name="dots-horizontal"
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
Properties.present(item);
|
||||
}}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
top={0}
|
||||
color={colors.pri}
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
@@ -19,10 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Linking, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { checkVersion } from "react-native-check-version";
|
||||
import Config from "react-native-config";
|
||||
import deviceInfoModule from "react-native-device-info";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { STORE_LINK } from "../../../utils/constants";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
@@ -148,6 +148,9 @@ export const Update = ({ version: appVersion, fwdRef }) => {
|
||||
<Seperator />
|
||||
<ScrollView
|
||||
nestedScrollEnabled={true}
|
||||
onMomentumScrollEnd={() => {
|
||||
fwdRef?.current?.handleChildScrollEnd();
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useCallback } from "react";
|
||||
import { FlatList, View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import umami from "../../common/analytics";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
@@ -75,6 +76,7 @@ export const SideMenu = React.memo(
|
||||
name: "Notesnook Pro",
|
||||
icon: "crown",
|
||||
func: () => {
|
||||
umami.pageView("/pro-screen", "/sidemenu");
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -164,6 +164,7 @@ export const PinItem = React.memo(
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
width="95%"
|
||||
height={50}
|
||||
customStyle={{
|
||||
marginBottom: 30
|
||||
}}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
@@ -34,25 +34,24 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
let toastMessages = [];
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [keyboard, setKeyboard] = useState(false);
|
||||
const [data, setData] = useState({});
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const hideTimeout = useRef();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef([]);
|
||||
|
||||
const show = useCallback(
|
||||
const showToastFunc = useCallback(
|
||||
async (data) => {
|
||||
if (!data) return;
|
||||
if (data.context !== context) return;
|
||||
if (
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) >= 0
|
||||
) {
|
||||
if (toastMessages.findIndex((m) => m.message === data.message) >= 0) {
|
||||
return;
|
||||
}
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
toastMessages.push(data);
|
||||
if (toastMessages?.length > 1) return;
|
||||
setData(data);
|
||||
|
||||
setVisible(true);
|
||||
@@ -60,16 +59,16 @@ export const Toast = ({ context = "global" }) => {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
hideToastFunc();
|
||||
}, data.duration);
|
||||
},
|
||||
[context, hide]
|
||||
[context, hideToastFunc]
|
||||
);
|
||||
|
||||
const next = useCallback(
|
||||
const showNext = useCallback(
|
||||
(data) => {
|
||||
if (!data) {
|
||||
hide();
|
||||
hideToastFunc();
|
||||
return;
|
||||
}
|
||||
setData(data);
|
||||
@@ -77,28 +76,27 @@ export const Toast = ({ context = "global" }) => {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
hideToastFunc();
|
||||
}, data?.duration);
|
||||
},
|
||||
[hide]
|
||||
[hideToastFunc]
|
||||
);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
const hideToastFunc = useCallback(() => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
let msg =
|
||||
toastMessages.current.length > 1 ? toastMessages.current.shift() : null;
|
||||
let msg = toastMessages.length > 1 ? toastMessages.shift() : null;
|
||||
|
||||
if (msg) {
|
||||
setVisible(false);
|
||||
next(msg);
|
||||
showNext(msg);
|
||||
setTimeout(() => {
|
||||
setVisible(true);
|
||||
}, 300);
|
||||
} else {
|
||||
setVisible(false);
|
||||
toastMessages.current.shift();
|
||||
toastMessages.shift();
|
||||
setTimeout(() => {
|
||||
setData({});
|
||||
if (hideTimeout.current) {
|
||||
@@ -106,22 +104,44 @@ export const Toast = ({ context = "global" }) => {
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [next]);
|
||||
}, [showNext]);
|
||||
|
||||
const _onKeyboardShow = () => {
|
||||
setKeyboard(true);
|
||||
};
|
||||
|
||||
const _onKeyboardHide = () => {
|
||||
setKeyboard(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowToast, show);
|
||||
eSubscribeEvent(eHideToast, hide);
|
||||
toastMessages = [];
|
||||
let sub1 = Keyboard.addListener("keyboardDidShow", _onKeyboardShow);
|
||||
let sub2 = Keyboard.addListener("keyboardDidHide", _onKeyboardHide);
|
||||
eSubscribeEvent(eShowToast, showToastFunc);
|
||||
eSubscribeEvent(eHideToast, hideToastFunc);
|
||||
return () => {
|
||||
toastMessages.current = [];
|
||||
eUnSubscribeEvent(eShowToast, show);
|
||||
eUnSubscribeEvent(eHideToast, hide);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
|
||||
toastMessages = [];
|
||||
sub1?.remove();
|
||||
sub2?.remove();
|
||||
eUnSubscribeEvent(eShowToast, showToastFunc);
|
||||
eUnSubscribeEvent(eHideToast, hideToastFunc);
|
||||
};
|
||||
}, [hide, show]);
|
||||
}, [hideToastFunc, keyboard, showToastFunc]);
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<TouchableOpacity
|
||||
onPress={hide}
|
||||
onPress={() => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideToastFunc();
|
||||
}}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? 400 : "100%",
|
||||
@@ -189,7 +209,7 @@ export const Toast = ({ context = "global" }) => {
|
||||
color={colors.pri}
|
||||
size={SIZE.md}
|
||||
onPress={() => {
|
||||
hide();
|
||||
hideToastFunc();
|
||||
}}
|
||||
>
|
||||
{data.heading}
|
||||
@@ -204,7 +224,7 @@ export const Toast = ({ context = "global" }) => {
|
||||
paddingRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
hide();
|
||||
hideToastFunc();
|
||||
}}
|
||||
>
|
||||
{data.message}
|
||||
|
||||
@@ -73,7 +73,6 @@ interface InputProps extends TextInputProps {
|
||||
buttonLeft?: React.ReactNode;
|
||||
inputStyle?: TextInputProps["style"];
|
||||
containerStyle?: ViewStyle;
|
||||
wrapperStyle?: ViewStyle;
|
||||
flexGrow?: number;
|
||||
}
|
||||
|
||||
@@ -92,7 +91,7 @@ const Input = ({
|
||||
button,
|
||||
onBlurInput,
|
||||
onPress,
|
||||
height = 45,
|
||||
height = 50,
|
||||
fontSize = SIZE.md,
|
||||
onFocusInput,
|
||||
buttons,
|
||||
@@ -101,7 +100,6 @@ const Input = ({
|
||||
flexGrow = 1,
|
||||
inputStyle = {},
|
||||
containerStyle = {},
|
||||
wrapperStyle = {},
|
||||
...restProps
|
||||
}: InputProps) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -238,8 +236,7 @@ const Input = ({
|
||||
marginBottom: marginBottom,
|
||||
flexGrow: flexGrow,
|
||||
maxHeight: height,
|
||||
marginRight: marginRight,
|
||||
...wrapperStyle
|
||||
marginRight: marginRight
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
|
||||
@@ -37,9 +37,7 @@ const SheetWrapper = ({
|
||||
onHasReachedTop,
|
||||
keyboardMode,
|
||||
overlay,
|
||||
overlayOpacity = 0.3,
|
||||
enableGesturesInScrollView = false,
|
||||
bottomPadding = true
|
||||
overlayOpacity = 0.3
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
@@ -61,8 +59,8 @@ const SheetWrapper = ({
|
||||
zIndex: 10,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 0,
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15,
|
||||
borderTopRightRadius: 10,
|
||||
borderTopLeftRadius: 10,
|
||||
alignSelf: "center",
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0
|
||||
@@ -86,8 +84,7 @@ const SheetWrapper = ({
|
||||
backdrop: "sheet-backdrop"
|
||||
}}
|
||||
indicatorStyle={{
|
||||
width: 100,
|
||||
backgroundColor: colors.nav
|
||||
width: 100
|
||||
}}
|
||||
drawUnderStatusBar={false}
|
||||
containerStyle={style}
|
||||
@@ -101,35 +98,32 @@ const SheetWrapper = ({
|
||||
indicatorColor={colors.nav}
|
||||
onOpen={_onOpen}
|
||||
keyboardDismissMode="none"
|
||||
enableGesturesInScrollView={enableGesturesInScrollView}
|
||||
defaultOverlayOpacity={overlayOpacity}
|
||||
overlayColor={pitchBlack ? "#585858" : "#2b2b2b"}
|
||||
overlayColor={pitchBlack ? "#585858" : "#000000"}
|
||||
keyboardShouldPersistTaps="always"
|
||||
ExtraOverlayComponent={
|
||||
<>
|
||||
{overlay}
|
||||
<Toast context="local" />
|
||||
<PremiumToast
|
||||
context="sheet"
|
||||
close={() => fwdRef?.current?.hide()}
|
||||
offset={50}
|
||||
/>
|
||||
<Toast context="local" />
|
||||
</>
|
||||
}
|
||||
onClose={_onClose}
|
||||
>
|
||||
<BouncingView>
|
||||
{children}
|
||||
{bottomPadding ? (
|
||||
<View
|
||||
style={{
|
||||
height:
|
||||
Platform.OS === "ios" && insets.bottom !== 0
|
||||
? insets.bottom + 5
|
||||
: 20
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
height:
|
||||
Platform.OS === "ios" && insets.bottom !== 0
|
||||
? insets.bottom + 5
|
||||
: 20
|
||||
}}
|
||||
/>
|
||||
</BouncingView>
|
||||
</ActionSheet>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,6 @@ export default function Tag({
|
||||
marginLeft: 2,
|
||||
marginTop: -10,
|
||||
height: 20,
|
||||
justifyContent: "center",
|
||||
...style
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,23 @@ import { FeatureType } from "./components/sheets/new-feature";
|
||||
|
||||
export const features: FeatureType[] = [
|
||||
{
|
||||
title: "Default font size & font family",
|
||||
body: "Now you can set default font size and font family in editor that will be used across all your new and old notes."
|
||||
title: "Cross platform Reminders 🔔",
|
||||
body: "Finally reminders are here. You can set reminders on notes or independently. Go to Side Menu > Reminders to set your first reminder!"
|
||||
},
|
||||
{
|
||||
title: "Multi-factor auth by default",
|
||||
body: "Multi-factor auth is enabled by default for all users to improve login security."
|
||||
},
|
||||
{
|
||||
title: "Change account email",
|
||||
body: "Now you can go to Settings > Account Settings to change your email."
|
||||
},
|
||||
{
|
||||
title: "Sync settings",
|
||||
body: "Added options to disable sync, auto-sync & real-time sync from settings."
|
||||
},
|
||||
{
|
||||
title: "Improved PDF exports styling",
|
||||
body: "Tables, checklists, codeblocks, quotes are now properly formatted in PDF exports."
|
||||
}
|
||||
];
|
||||
|
||||
@@ -21,16 +21,12 @@ import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import Share from "react-native-share";
|
||||
import { notesnook } from "../../e2e/test.ids";
|
||||
import { db } from "../common/database";
|
||||
import { AttachmentDialog } from "../components/attachments";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import NoteHistory from "../components/note-history";
|
||||
import { AddNotebookSheet } from "../components/sheets/add-notebook";
|
||||
import MoveNoteSheet from "../components/sheets/add-to";
|
||||
import ExportNotesSheet from "../components/sheets/export-notes";
|
||||
import { MoveNotes } from "../components/sheets/move-notes/movenote";
|
||||
import PublishNoteSheet from "../components/sheets/publish-note";
|
||||
import { RelationsList } from "../components/sheets/relations-list/index";
|
||||
import ReminderSheet from "../components/sheets/reminder";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -45,7 +41,6 @@ import Notifications from "../services/notifications";
|
||||
import { useEditorStore } from "../stores/use-editor-store";
|
||||
import { useMenuStore } from "../stores/use-menu-store";
|
||||
import useNavigationStore from "../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
import { useSelectionStore } from "../stores/use-selection-store";
|
||||
import { useTagStore } from "../stores/use-tag-store";
|
||||
import { useThemeStore } from "../stores/use-theme-store";
|
||||
@@ -53,12 +48,17 @@ import { useUserStore } from "../stores/use-user-store";
|
||||
import { toTXT } from "../utils";
|
||||
import { toggleDarkMode } from "../utils/color-scheme/utils";
|
||||
import {
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddNotebookDialog,
|
||||
eOpenAddTopicDialog,
|
||||
eOpenLoginDialog
|
||||
eOpenAttachmentsDialog,
|
||||
eOpenLoginDialog,
|
||||
eOpenMoveNoteDialog,
|
||||
eOpenPublishNoteDialog
|
||||
} from "../utils/events";
|
||||
import { deleteItems } from "../utils/functions";
|
||||
import { sleep } from "../utils/time";
|
||||
import { RelationsList } from "../components/sheets/relations-list/index";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
|
||||
export const useActions = ({ close = () => null, item }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -106,15 +106,6 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isNoteInNotebook = () => {
|
||||
const currentScreen = useNavigationStore.getState().currentScreen;
|
||||
if (item.type !== "note" || currentScreen.name !== "Notebook") return;
|
||||
|
||||
return !!db.relations
|
||||
.to(item, "notebook")
|
||||
.find((notebook) => notebook.id === currentScreen.id);
|
||||
};
|
||||
|
||||
const onUpdate = useCallback(
|
||||
async (type) => {
|
||||
if (type === "unpin") {
|
||||
@@ -139,16 +130,25 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
|
||||
function addTo() {
|
||||
close();
|
||||
clearSelection(true);
|
||||
setSelectedItem(item);
|
||||
MoveNoteSheet.present(item);
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenMoveNoteDialog, item);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
async function addToFavorites() {
|
||||
if (!item.id) return;
|
||||
close();
|
||||
await db.notes.note(item.id).favorite();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
}
|
||||
|
||||
async function pinItem() {
|
||||
@@ -156,7 +156,14 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
close();
|
||||
let type = item.type;
|
||||
await db[`${type}s`][type](item.id).pin();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebooks"
|
||||
);
|
||||
}
|
||||
|
||||
async function pinToNotifications() {
|
||||
@@ -170,17 +177,9 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
checkNotifPinned();
|
||||
return;
|
||||
}
|
||||
if (item.locked) {
|
||||
ToastEvent.show({
|
||||
heading: "Note is locked",
|
||||
type: "error",
|
||||
message: "Locked notes cannot be pinned to notifications",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
let text = await toTXT(item, false);
|
||||
let html = text.replace(/\n/g, "<br />");
|
||||
if (item.locked) return;
|
||||
let html = await db.notes.note(item.id).content();
|
||||
let text = await toTXT(item);
|
||||
Notifications.displayNotification({
|
||||
title: item.title,
|
||||
message: item.headline || text,
|
||||
@@ -199,7 +198,15 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
if (!checkNoteSynced()) return;
|
||||
close();
|
||||
await db.trash.restore(item.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebooks",
|
||||
"Trash"
|
||||
);
|
||||
let type = item.type === "trash" ? item.itemType : item.type;
|
||||
ToastEvent.show({
|
||||
heading:
|
||||
@@ -264,7 +271,9 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
PublishNoteSheet.present(item);
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPublishNoteDialog, item);
|
||||
}
|
||||
|
||||
const checkNoteSynced = () => {
|
||||
@@ -315,7 +324,13 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
let note = db.notes.note(item.id).data;
|
||||
if (note.locked) {
|
||||
close();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
close();
|
||||
@@ -381,11 +396,16 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
positivePress: async (value) => {
|
||||
if (!value || value === "" || value.trimStart().length == 0) return;
|
||||
await db.tags.rename(item.id, db.tags.sanitize(value));
|
||||
setImmediate(() => {
|
||||
useTagStore.getState().setTags();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
});
|
||||
useTagStore.getState().setTags();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Tags"
|
||||
);
|
||||
},
|
||||
input: true,
|
||||
defaultValue: alias,
|
||||
@@ -428,16 +448,24 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
? "This reminder will be removed"
|
||||
: "This tag will be removed from all notes.",
|
||||
positivePress: async () => {
|
||||
const routes = [];
|
||||
routes.push(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"Notes",
|
||||
"NotesPage",
|
||||
"Reminders",
|
||||
"Favorites"
|
||||
);
|
||||
if (item.type === "reminder") {
|
||||
await db.reminders.remove(item.id);
|
||||
} else {
|
||||
await db.tags.remove(item.id);
|
||||
}
|
||||
setImmediate(() => {
|
||||
useTagStore.getState().setTags();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
});
|
||||
routes.push("Tags");
|
||||
}
|
||||
Navigation.queueRoutesForUpdate(...routes);
|
||||
useRelationStore.getState().update();
|
||||
},
|
||||
positiveText: "Delete",
|
||||
positiveType: "errorShade"
|
||||
@@ -475,16 +503,15 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
},
|
||||
item.id
|
||||
);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
close();
|
||||
}
|
||||
|
||||
async function removeNoteFromNotebook() {
|
||||
const currentScreen = useNavigationStore.getState().currentScreen;
|
||||
if (currentScreen.name !== "Notebook") return;
|
||||
await db.relations.unlink({ type: "notebook", id: currentScreen.id }, item);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -499,14 +526,12 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
await db.trash.delete(item.id);
|
||||
setImmediate(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
Navigation.queueRoutesForUpdate("Trash");
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
@@ -514,32 +539,35 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
|
||||
async function openHistory() {
|
||||
close();
|
||||
await sleep(300);
|
||||
presentSheet({
|
||||
component: (ref) => <NoteHistory fwdRef={ref} note={item} />
|
||||
});
|
||||
}
|
||||
|
||||
async function showAttachments() {
|
||||
AttachmentDialog.present();
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenAttachmentsDialog, item);
|
||||
}
|
||||
|
||||
async function exportNote() {
|
||||
if (item.locked) {
|
||||
ToastEvent.show({
|
||||
heading: "Note is locked",
|
||||
type: "error",
|
||||
message: "Locked notes cannot be exported",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
close();
|
||||
await sleep(300);
|
||||
ExportNotesSheet.present([item]);
|
||||
}
|
||||
|
||||
async function toggleLocalOnly() {
|
||||
if (!checkNoteSynced() || !user) return;
|
||||
db.notes.note(item.id).localOnly();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -550,109 +578,101 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
useEditorStore.getState().setReadonly(current);
|
||||
// tiny.call(EditorWebView, tiny.toogleReadMode(current ? 'readonly' : 'design'));
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
close();
|
||||
};
|
||||
|
||||
const duplicateNote = async () => {
|
||||
if (!checkNoteSynced()) return;
|
||||
await db.notes.note(item.id).duplicate();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes"
|
||||
);
|
||||
close();
|
||||
};
|
||||
const actions = [
|
||||
{
|
||||
id: "notebooks",
|
||||
title: "Link Notebooks",
|
||||
name: "Add to notebook",
|
||||
title: "Add to notebook",
|
||||
icon: "book-outline",
|
||||
func: addTo
|
||||
},
|
||||
{
|
||||
id: "add-tag",
|
||||
title: "Add tags",
|
||||
icon: "pound",
|
||||
func: addTo
|
||||
},
|
||||
{
|
||||
id: "add-reminder",
|
||||
title: "Remind me",
|
||||
icon: "clock-plus-outline",
|
||||
name: "Add-Reminder",
|
||||
title: "Add reminder",
|
||||
icon: "bell-plus",
|
||||
func: () => {
|
||||
ReminderSheet.present(null, { id: item.id, type: "note" });
|
||||
},
|
||||
close: true
|
||||
},
|
||||
{
|
||||
id: "lock-unlock",
|
||||
title: item.locked ? "Unlock" : "Lock",
|
||||
icon: item.locked ? "lock-open-outline" : "key-outline",
|
||||
func: addToVault,
|
||||
on: item.locked
|
||||
},
|
||||
{
|
||||
id: "publish",
|
||||
title: isPublished ? "Published" : "Publish",
|
||||
icon: "cloud-upload-outline",
|
||||
on: isPublished,
|
||||
func: publishNote
|
||||
},
|
||||
|
||||
{
|
||||
id: "export",
|
||||
title: "Export",
|
||||
icon: "export",
|
||||
func: exportNote
|
||||
},
|
||||
{
|
||||
id: "move-notes",
|
||||
name: "Move notes",
|
||||
title: "Add notes",
|
||||
icon: "plus",
|
||||
func: async () => {
|
||||
close();
|
||||
await sleep(500);
|
||||
MoveNotes.present(db.notebooks.notebook(item.notebookId).data, item);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "pin",
|
||||
title: "Pin",
|
||||
name: "Pin",
|
||||
title: item.pinned ? "Unpin" : "Pin to top",
|
||||
icon: item.pinned ? "pin-off-outline" : "pin-outline",
|
||||
func: pinItem,
|
||||
close: false,
|
||||
check: true,
|
||||
on: item.pinned,
|
||||
pro: true
|
||||
nopremium: true,
|
||||
id: notesnook.ids.dialogs.actionsheet.pin
|
||||
},
|
||||
{
|
||||
id: "favorite",
|
||||
title: "Favorite",
|
||||
name: "Favorite",
|
||||
title: !item.favorite ? "Favorite" : "Unfavorite",
|
||||
icon: item.favorite ? "star-off" : "star-outline",
|
||||
func: addToFavorites,
|
||||
close: false,
|
||||
check: true,
|
||||
on: item.favorite,
|
||||
pro: true,
|
||||
nopremium: true,
|
||||
id: notesnook.ids.dialogs.actionsheet.favorite,
|
||||
color: "orange"
|
||||
},
|
||||
{
|
||||
id: "pin-to-notifications",
|
||||
name: "PinToNotif",
|
||||
title:
|
||||
notifPinned !== null
|
||||
? "Unpin from notifications"
|
||||
: "Pin to notifications",
|
||||
icon: "message-badge-outline",
|
||||
? "Unpin from Notifications"
|
||||
: "Pin to Notifications",
|
||||
icon: "bell",
|
||||
on: notifPinned !== null,
|
||||
func: pinToNotifications
|
||||
},
|
||||
|
||||
{
|
||||
id: "edit-notebook",
|
||||
name: "Edit Notebook",
|
||||
title: "Edit notebook",
|
||||
icon: "square-edit-outline",
|
||||
func: async () => {
|
||||
AddNotebookSheet.present(item);
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenAddNotebookDialog, item);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "edit-topic",
|
||||
name: "Edit Topic",
|
||||
title: "Edit topic",
|
||||
icon: "square-edit-outline",
|
||||
func: async () => {
|
||||
@@ -665,85 +685,146 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "copy",
|
||||
name: "Copy",
|
||||
title: "Copy",
|
||||
icon: "content-copy",
|
||||
func: copyContent
|
||||
},
|
||||
{
|
||||
id: "restore",
|
||||
name: "Restore",
|
||||
title: "Restore " + item.itemType,
|
||||
icon: "delete-restore",
|
||||
func: restoreTrashItem
|
||||
},
|
||||
|
||||
{
|
||||
id: "add-shortcut",
|
||||
name: "Publish",
|
||||
title: isPublished ? "Published" : "Publish",
|
||||
icon: "cloud-upload-outline",
|
||||
on: isPublished,
|
||||
func: publishNote
|
||||
},
|
||||
{
|
||||
name: "Vault",
|
||||
title: item.locked ? "Remove from vault" : "Add to vault",
|
||||
icon: item.locked ? "shield-off-outline" : "shield-outline",
|
||||
func: addToVault,
|
||||
on: item.locked
|
||||
},
|
||||
|
||||
{
|
||||
name: "Add Shortcut",
|
||||
title: isPinnedToMenu ? "Remove Shortcut" : "Add Shortcut",
|
||||
icon: isPinnedToMenu ? "link-variant-remove" : "link-variant",
|
||||
func: createMenuShortcut,
|
||||
close: false,
|
||||
check: true,
|
||||
on: isPinnedToMenu,
|
||||
pro: true
|
||||
nopremium: true,
|
||||
id: notesnook.ids.dialogs.actionsheet.pinMenu
|
||||
},
|
||||
{
|
||||
id: "rename-tag",
|
||||
name: "Rename Tag",
|
||||
title: "Rename tag",
|
||||
icon: "square-edit-outline",
|
||||
func: renameTag
|
||||
},
|
||||
{
|
||||
id: "share",
|
||||
name: "Share",
|
||||
title: "Share",
|
||||
icon: "share-variant",
|
||||
func: shareNote
|
||||
},
|
||||
{
|
||||
id: "read-only",
|
||||
name: "Attachments",
|
||||
title: "Attachments",
|
||||
icon: "attachment",
|
||||
func: showAttachments
|
||||
},
|
||||
{
|
||||
name: "Export",
|
||||
title: "Export",
|
||||
icon: "export",
|
||||
func: exportNote
|
||||
},
|
||||
{
|
||||
name: "RemoveTopic",
|
||||
title: "Remove from topic",
|
||||
hidden: !isNoteInTopic(),
|
||||
icon: "minus-circle-outline",
|
||||
func: removeNoteFromTopic
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
title:
|
||||
item.type !== "notebook" && item.type !== "note"
|
||||
? "Delete " + item.type
|
||||
: "Move to trash",
|
||||
icon: "delete-outline",
|
||||
type: "error",
|
||||
func: deleteItem
|
||||
},
|
||||
{
|
||||
name: "PermDelete",
|
||||
title: "Delete " + item.itemType,
|
||||
icon: "delete",
|
||||
func: deleteTrashItem
|
||||
},
|
||||
{
|
||||
name: "ReadOnly",
|
||||
title: "Read only",
|
||||
icon: "pencil-lock",
|
||||
func: toggleReadyOnlyMode,
|
||||
on: item.readonly
|
||||
},
|
||||
{
|
||||
id: "local-only",
|
||||
name: "Local only",
|
||||
title: "Local only",
|
||||
icon: "sync-off",
|
||||
func: toggleLocalOnly,
|
||||
on: item.localOnly
|
||||
},
|
||||
|
||||
{
|
||||
id: "duplicate",
|
||||
name: "History",
|
||||
title: "History",
|
||||
icon: "history",
|
||||
func: openHistory
|
||||
},
|
||||
{
|
||||
name: "Duplicate",
|
||||
title: "Duplicate",
|
||||
icon: "content-duplicate",
|
||||
func: duplicateNote
|
||||
},
|
||||
{
|
||||
id: "dark-mode",
|
||||
name: "Dark Mode",
|
||||
title: "Dark mode",
|
||||
icon: "theme-light-dark",
|
||||
func: switchTheme,
|
||||
switch: true,
|
||||
on: colors.night ? true : false,
|
||||
close: false,
|
||||
pro: true
|
||||
nopremium: true,
|
||||
id: notesnook.ids.dialogs.actionsheet.night
|
||||
},
|
||||
{
|
||||
id: "edit-reminder",
|
||||
name: "Edit reminder",
|
||||
title: "Edit reminder",
|
||||
icon: "pencil",
|
||||
func: async () => {
|
||||
close();
|
||||
await sleep(300);
|
||||
ReminderSheet.present(item);
|
||||
},
|
||||
close: false
|
||||
},
|
||||
|
||||
{
|
||||
id: "reminders",
|
||||
name: "Reminders",
|
||||
title: "Reminders",
|
||||
icon: "clock-outline",
|
||||
icon: "bell",
|
||||
func: async () => {
|
||||
close();
|
||||
RelationsList.present({
|
||||
reference: item,
|
||||
referenceType: "reminder",
|
||||
@@ -761,20 +842,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
close: false
|
||||
},
|
||||
{
|
||||
id: "attachments",
|
||||
title: "Attachments",
|
||||
icon: "attachment",
|
||||
func: showAttachments
|
||||
},
|
||||
{
|
||||
id: "history",
|
||||
title: "History",
|
||||
icon: "history",
|
||||
func: openHistory
|
||||
},
|
||||
|
||||
{
|
||||
id: "disable-reminder",
|
||||
name: "ReminderOnOff",
|
||||
title: !item.disabled ? "Turn off reminder" : "Turn on reminder",
|
||||
icon: !item.disabled ? "bell-off-outline" : "bell",
|
||||
func: async () => {
|
||||
@@ -785,41 +853,19 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
});
|
||||
Notifications.scheduleNotification(item);
|
||||
useRelationStore.getState().update();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"NotesPage",
|
||||
"Reminders"
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "remove-from-topic",
|
||||
title: "Remove from topic",
|
||||
hidden: !isNoteInTopic(),
|
||||
icon: "minus-circle-outline",
|
||||
func: removeNoteFromTopic
|
||||
},
|
||||
{
|
||||
id: "remove-from-notebook",
|
||||
title: "Remove from notebook",
|
||||
hidden: !isNoteInNotebook(),
|
||||
icon: "minus-circle-outline",
|
||||
func: removeNoteFromNotebook
|
||||
},
|
||||
{
|
||||
id: "trash",
|
||||
title:
|
||||
item.type !== "notebook" && item.type !== "note"
|
||||
? "Delete " + item.type
|
||||
: "Move to trash",
|
||||
icon: "delete-outline",
|
||||
type: "error",
|
||||
func: deleteItem
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
title: "Delete " + item.itemType,
|
||||
icon: "delete",
|
||||
func: deleteTrashItem
|
||||
}
|
||||
// {
|
||||
// id: "ReferencedIn",
|
||||
// name: "ReferencedIn",
|
||||
// title: "References",
|
||||
// icon: "link",
|
||||
// func: async () => {
|
||||
|
||||
@@ -26,13 +26,12 @@ import {
|
||||
Linking,
|
||||
NativeEventEmitter,
|
||||
NativeModules,
|
||||
Platform,
|
||||
Keyboard
|
||||
Platform
|
||||
} from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { Walkthrough } from "../components/walkthroughs";
|
||||
import { editorController, editorState } from "../screens/editor/tiptap/utils";
|
||||
import { editorState } from "../screens/editor/tiptap/utils";
|
||||
import {
|
||||
clearMessage,
|
||||
setEmailVerifyMessage,
|
||||
@@ -70,7 +69,6 @@ import { clearAppState } from "../screens/editor/tiptap/utils";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import BackupService from "../services/backup";
|
||||
import { sleep } from "../utils/time";
|
||||
import notifee from "@notifee/react-native";
|
||||
|
||||
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
|
||||
export const useAppEvents = () => {
|
||||
@@ -318,10 +316,6 @@ export const useAppEvents = () => {
|
||||
|
||||
const onLogout = async (reason) => {
|
||||
console.log("Logged out", reason);
|
||||
DatabaseLogger.log("User Logged Out" + reason);
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
};
|
||||
|
||||
const unsubIAP = () => {
|
||||
@@ -402,9 +396,11 @@ export const useAppEvents = () => {
|
||||
|
||||
const checkAutoBackup = useCallback(async () => {
|
||||
if (verify || syncing) {
|
||||
console.log("backup is waiting");
|
||||
refValues.current.backupDidWait = true;
|
||||
return;
|
||||
}
|
||||
console.log("backup running immediate");
|
||||
const user = await db.user.getUser();
|
||||
if (PremiumService.get() && user) {
|
||||
if (SettingsService.get().reminder === "off") {
|
||||
@@ -425,6 +421,7 @@ export const useAppEvents = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!verify && !syncing && refValues.current.backupDidWait) {
|
||||
console.log("backup run after wait");
|
||||
refValues.current.backupDidWait = false;
|
||||
checkAutoBackup();
|
||||
}
|
||||
@@ -447,7 +444,6 @@ export const useAppEvents = () => {
|
||||
const onAppStateChanged = useCallback(
|
||||
async (state) => {
|
||||
if (state === "active") {
|
||||
notifee.setBadgeCount(0);
|
||||
updateStatusBarColor();
|
||||
if (
|
||||
SettingsService.get().appLockMode !== "background" &&
|
||||
@@ -492,10 +488,6 @@ export const useAppEvents = () => {
|
||||
!useUserStore.getState().verifyUser
|
||||
) {
|
||||
useUserStore.getState().setVerifyUser(true);
|
||||
if (Platform.OS === "ios") {
|
||||
editorController.current?.commands.blur();
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}
|
||||
if (
|
||||
SettingsService.get().privacyScreen ||
|
||||
@@ -558,7 +550,7 @@ export const useAppEvents = () => {
|
||||
let shareExtensionOpened = MMKV.getString("shareExtensionOpened");
|
||||
if (notesAddedFromIntent) {
|
||||
if (Platform.OS === "ios") {
|
||||
await db.initCollections();
|
||||
await db.init();
|
||||
await db.notes.init();
|
||||
}
|
||||
useNoteStore.getState().setNotes();
|
||||
@@ -571,8 +563,8 @@ export const useAppEvents = () => {
|
||||
if (notesAddedFromIntent || shareExtensionOpened) {
|
||||
let id = useEditorStore.getState().currentEditingNote;
|
||||
let note = id && db.notes.note(id).data;
|
||||
eSendEvent("loadingNote", note);
|
||||
eSendEvent("webview_reset");
|
||||
setTimeout(() => eSendEvent("loadingNote", note), 1);
|
||||
MMKV.removeItem("shareExtensionOpened");
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -17,46 +17,35 @@ 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 { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
|
||||
type AttachmentProgress = {
|
||||
type: string;
|
||||
value?: number;
|
||||
percent?: string;
|
||||
};
|
||||
|
||||
export const useAttachmentProgress = (
|
||||
attachment: any,
|
||||
encryption?: boolean
|
||||
) => {
|
||||
export const useAttachmentProgress = (attachment, encryption) => {
|
||||
const progress = useAttachmentStore((state) => state.progress);
|
||||
const [currentProgress, setCurrentProgress] = useState<
|
||||
AttachmentProgress | undefined
|
||||
>(
|
||||
const [currentProgress, setCurrentProgress] = useState(
|
||||
encryption
|
||||
? {
|
||||
type: "encrypt"
|
||||
}
|
||||
: undefined
|
||||
: null
|
||||
);
|
||||
|
||||
const attachmentProgress = progress?.[attachment.metadata.hash];
|
||||
if (attachmentProgress) {
|
||||
const type = attachmentProgress.type;
|
||||
const loaded =
|
||||
attachmentProgress.type === "download"
|
||||
? attachmentProgress.recieved
|
||||
: attachmentProgress.sent;
|
||||
const value = loaded / attachmentProgress.total;
|
||||
setCurrentProgress({
|
||||
value: value * 100,
|
||||
percent: (value * 100).toFixed(0) + "%",
|
||||
type: type
|
||||
});
|
||||
} else {
|
||||
setCurrentProgress(undefined);
|
||||
}
|
||||
useEffect(() => {
|
||||
let prog = progress[attachment.metadata.hash];
|
||||
if (prog) {
|
||||
let type = prog.type;
|
||||
let loaded = prog.type === "download" ? prog.recieved : prog.sent;
|
||||
prog = loaded / prog.total;
|
||||
prog = (prog * 100).toFixed(0);
|
||||
setCurrentProgress({
|
||||
value: prog,
|
||||
percent: prog + "%",
|
||||
type: type
|
||||
});
|
||||
} else {
|
||||
setCurrentProgress(null);
|
||||
}
|
||||
}, [attachment.metadata.hash, progress]);
|
||||
|
||||
return [currentProgress, setCurrentProgress];
|
||||
};
|
||||
61
apps/mobile/app/hooks/use-vault-status.js
Normal file
61
apps/mobile/app/hooks/use-vault-status.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import BiometicService from "../services/biometrics";
|
||||
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
|
||||
import { db } from "../common/database";
|
||||
|
||||
const VaultStatusCache = {
|
||||
exists: false,
|
||||
biometryEnrolled: false,
|
||||
isBiometryAvailable: false
|
||||
};
|
||||
|
||||
export const useVaultStatus = () => {
|
||||
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusCache);
|
||||
|
||||
const checkVaultStatus = useCallback(() => {
|
||||
db.vault.exists().then(async (exists) => {
|
||||
let available = await BiometicService.isBiometryAvailable();
|
||||
let fingerprint = await BiometicService.hasInternetCredentials();
|
||||
if (
|
||||
VaultStatusCache.exists === exists &&
|
||||
VaultStatusCache.biometryEnrolled === fingerprint &&
|
||||
VaultStatusCache.isBiometryAvailable === available
|
||||
)
|
||||
return;
|
||||
setVaultStatus({
|
||||
exists: exists,
|
||||
biometryEnrolled: fingerprint,
|
||||
isBiometryAvailable: available ? true : false
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkVaultStatus();
|
||||
eSubscribeEvent("vaultUpdated", () => checkVaultStatus());
|
||||
return () => {
|
||||
eUnSubscribeEvent("vaultUpdated", () => checkVaultStatus());
|
||||
};
|
||||
}, [checkVaultStatus]);
|
||||
|
||||
return vaultStatus;
|
||||
};
|
||||
@@ -24,7 +24,6 @@ import { SafeAreaView } from "react-native";
|
||||
import Container from "../components/container";
|
||||
import DelayLayout from "../components/delay-layout";
|
||||
import Intro from "../components/intro";
|
||||
import { TopicsSheet } from "../components/sheets/topic-sheet";
|
||||
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
|
||||
import { hideAllTooltips } from "../hooks/use-tooltip";
|
||||
import Favorites from "../screens/favorites";
|
||||
@@ -199,7 +198,6 @@ const _NavigationStack = () => {
|
||||
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
|
||||
<Tabs />
|
||||
</NavigationContainer>
|
||||
<TopicsSheet />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,7 +69,6 @@ import {
|
||||
import { editorRef, tabBarRef } from "../utils/global-refs";
|
||||
import { sleep } from "../utils/time";
|
||||
import { NavigationStack } from "./navigation-stack";
|
||||
import changeNavigationBarColor from "react-native-navigation-bar-color";
|
||||
|
||||
const _TabsHolder = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -236,17 +235,14 @@ const _TabsHolder = () => {
|
||||
let needsUpdate = current !== deviceMode;
|
||||
|
||||
if (fullscreen && current !== "mobile") {
|
||||
// Runs after size is set via state.
|
||||
setTimeout(() => {
|
||||
editorRef.current?.setNativeProps({
|
||||
style: {
|
||||
width: size.width,
|
||||
zIndex: 999,
|
||||
paddingHorizontal:
|
||||
current === "smallTablet" ? size.width * 0 : size.width * 0.15
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
editorRef.current?.setNativeProps({
|
||||
style: {
|
||||
width: size.width,
|
||||
zIndex: 999,
|
||||
paddingHorizontal:
|
||||
current === "smallTablet" ? size.width * 0 : size.width * 0.15
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (fullscreen) eSendEvent(eCloseFullscreenEditor, current);
|
||||
editorRef.current?.setNativeProps({
|
||||
@@ -285,8 +281,10 @@ const _TabsHolder = () => {
|
||||
!editorState().movedAway &&
|
||||
useEditorStore.getState().currentEditingNote
|
||||
) {
|
||||
console.log("editor");
|
||||
tabBarRef.current?.goToIndex(2, false);
|
||||
} else {
|
||||
console.log("home");
|
||||
tabBarRef.current?.goToIndex(1, false);
|
||||
}
|
||||
break;
|
||||
@@ -387,10 +385,6 @@ const _TabsHolder = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
changeNavigationBarColor(colors.bg, !colors.night, true);
|
||||
}, [colors.night, colors.bg]);
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={_onLayout}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user