mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
1 Commits
fix/share-
...
add-oss-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33509ca25e |
@@ -20,7 +20,6 @@ const SCOPES = [
|
||||
"logger",
|
||||
"theme",
|
||||
"core",
|
||||
"fs",
|
||||
"clipper",
|
||||
"config",
|
||||
"ci",
|
||||
|
||||
@@ -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
|
||||
27
.github/actions/setup-node-with-cache/action.yml
vendored
27
.github/actions/setup-node-with-cache/action.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: 'Setup node with cache action'
|
||||
description: 'Setups node with cache'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
apps/mobile/package-lock.json
|
||||
apps/desktop/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
extensions/web-clipper/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/sodium/package-lock.json
|
||||
packages/clipper/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
|
||||
4
.github/dco.yml
vendored
4
.github/dco.yml
vendored
@@ -1,4 +0,0 @@
|
||||
allowRemediationCommits:
|
||||
individual: true
|
||||
require:
|
||||
members: false
|
||||
17
.github/workflows/android.publish.internal.yml
vendored
17
.github/workflows/android.publish.internal.yml
vendored
@@ -11,8 +11,21 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
- 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
|
||||
|
||||
43
.github/workflows/android.publish.yml
vendored
43
.github/workflows/android.publish.yml
vendored
@@ -11,8 +11,21 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
- 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
|
||||
@@ -37,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
|
||||
@@ -89,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.1.1
|
||||
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/
|
||||
16
.github/workflows/core.tests.yml
vendored
16
.github/workflows/core.tests.yml
vendored
@@ -24,7 +24,21 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
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: Prepare environment
|
||||
run: |
|
||||
|
||||
111
.github/workflows/desktop.publish.yml
vendored
111
.github/workflows/desktop.publish.yml
vendored
@@ -28,7 +28,21 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
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: |
|
||||
@@ -50,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Collect app metadata
|
||||
id: app_metadata
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
run: |
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
|
||||
@@ -77,9 +91,6 @@ jobs:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup notarization
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
@@ -87,7 +98,7 @@ jobs:
|
||||
|
||||
- name: Collect app metadata
|
||||
id: app_metadata
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
run: |
|
||||
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
|
||||
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
|
||||
@@ -104,20 +115,26 @@ jobs:
|
||||
private-key-raw: ${{ secrets.api_key }}
|
||||
platform: "MAC_OS"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/desktop/build
|
||||
path: ./apps/web/desktop/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
npm i
|
||||
npm i dmg-license
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build app for Mac App Store
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
@@ -125,9 +142,9 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.mac_certs }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
npm run builder -- --mac mas --universal -p never
|
||||
working-directory: ./apps/desktop
|
||||
npm run build:mas
|
||||
npx electron-builder --mac mas --universal -p never
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build zip and dmg
|
||||
env:
|
||||
@@ -138,13 +155,13 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop
|
||||
npm run build
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
npm run builder -- --mac zip dmg --arm64 --x64 -p always
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
else
|
||||
npm run builder -- --mac zip dmg --arm64 --x64 -p never
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Deploy to Testflight
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
@@ -152,12 +169,12 @@ jobs:
|
||||
API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
package=$(find ./output/mas-universal/notesnook*.pkg)
|
||||
package=$(find ./dist/mas-universal/notesnook*.pkg)
|
||||
|
||||
echo "Uploading ${package} using altool..."
|
||||
|
||||
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
@@ -168,9 +185,6 @@ jobs:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Snapcraft Auth
|
||||
if: inputs.publish-snap
|
||||
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
|
||||
@@ -178,44 +192,48 @@ jobs:
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
if: inputs.publish-snap
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/desktop/build
|
||||
path: ./apps/web/desktop/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run build
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
npm run builder -- --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
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
|
||||
npm run builder -- --linux AppImage:x64 AppImage:arm64 -p always
|
||||
npx electron-builder --linux AppImage deb rpm -p always
|
||||
else
|
||||
npm run builder -- --linux AppImage:x64 AppImage:arm64 -p never
|
||||
npx electron-builder --linux AppImage deb rpm -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Publish on Snapcraft
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/desktop
|
||||
snapcraft upload --release=stable ./dist/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
@@ -227,30 +245,31 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/desktop/build
|
||||
path: ./apps/web/desktop/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run build
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build app
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --win --publish always
|
||||
npx electron-builder --win --publish always
|
||||
} else {
|
||||
yarn electron-builder --win --publish never
|
||||
npx electron-builder --win --publish never
|
||||
}
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
43
.github/workflows/editor.tests.yml
vendored
43
.github/workflows/editor.tests.yml
vendored
@@ -1,43 +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: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- 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
|
||||
16
.github/workflows/web.publish.yml
vendored
16
.github/workflows/web.publish.yml
vendored
@@ -11,7 +11,21 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
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: |
|
||||
|
||||
32
.github/workflows/web.tests.yml
vendored
32
.github/workflows/web.tests.yml
vendored
@@ -25,7 +25,21 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
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: |
|
||||
@@ -62,7 +76,21 @@ jobs:
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
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: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,5 +6,4 @@ dist
|
||||
nx-cloud.env
|
||||
.idea
|
||||
.eslintcache
|
||||
.env.local
|
||||
.nyc_output
|
||||
.env.local
|
||||
@@ -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).
|
||||
|
||||
@@ -30,7 +30,7 @@ Before you open a new feature request, please make sure it's not a duplicate. **
|
||||
|
||||
### Helping out in the issue tracker
|
||||
|
||||
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 +62,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
|
||||
|
||||
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
|
||||
2. Run `npm run bootstrap` in the repository root.
|
||||
3. If you've fixed a bug or added code that should be tested, add tests!
|
||||
3. If you’ve fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`npm run test`).
|
||||
5. Format your code with prettier (`npm run prettier`).
|
||||
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
|
||||
@@ -131,11 +131,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
|
||||
|
||||
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
|
||||
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb’s Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
|
||||
## Git Branch Organization
|
||||
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don’t use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
|
||||
1. All tests must be passing at all times
|
||||
2. There should be as few breaking changes as possible
|
||||
|
||||
39
README.md
39
README.md
@@ -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
|
||||
|
||||
@@ -25,30 +25,25 @@ Notesnook is built using the following technologies:
|
||||
1. JavaScript/Typescript — this repo is in a hybrid state. A lot of the newer code is being written in Typescript & the old code is slowly being ported over.
|
||||
2. React — the whole front-end across all platforms is built using React.
|
||||
3. React Native — For mobile apps we are using React Native
|
||||
4. Electron — For desktop app
|
||||
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
6. Lerna & Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
4. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
5. Lerna & Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
|
||||
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
|
||||
|
||||
### Monorepo structure
|
||||
|
||||
| Name | Path | Description |
|
||||
| -------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `@notesnook/web` | [/apps/web](/apps/web) | Web client |
|
||||
| `@notesnook/desktop` | [/apps/desktop](/apps/desktop) | Desktop client |
|
||||
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
|
||||
| `@notesnook/web-clipper` | [/extensions/web-clipper](/extensions/web-clipper) | Web clipper |
|
||||
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
|
||||
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
|
||||
| `@notesnook/clipper` | [/packages/clipper](/packages/clipper) | Web clipper core handling everything related to actual page clipping |
|
||||
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/sodium` | [/packages/sodium](/packages/sodium) | Wrapper around libsodium to support Node.js & Browser |
|
||||
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
| Name | Path | Description |
|
||||
| -------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `@notesnook/web` | [/apps/web](/apps/web) | Web/Desktop clients |
|
||||
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
|
||||
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
|
||||
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
|
||||
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
|
||||
### Contributing guidelines
|
||||
|
||||
|
||||
4
apps/desktop/.gitignore
vendored
4
apps/desktop/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
output
|
||||
dist
|
||||
@@ -1,58 +0,0 @@
|
||||
<p align="center">
|
||||
<img style="align:center;" src="/resources/screenshots/web.jpg" alt="Notesnook desktop screenshot" width="600" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Notesnook Desktop</h1>
|
||||
<h3 align="center">The desktop app is built using Electron & Typescript.</h3>
|
||||
<p align="center">
|
||||
<a href="https://notesnook.com/downloads">Downloads</a> | <a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
|
||||
</p>
|
||||
|
||||
## Getting started
|
||||
|
||||
## Build instructions
|
||||
|
||||
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
|
||||
### Setting up the development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/download/)
|
||||
2. [git](https://git-scm.com/downloads)
|
||||
3. NPM (not yarn or pnpm)
|
||||
|
||||
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
|
||||
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
|
||||
# change directory
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
## Developer guide
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible:
|
||||
|
||||
1. Electron
|
||||
2. tRPC: for cross-communication between the web & desktop parts
|
||||
3. zod: for runtime typechecking
|
||||
4. yargs: for CLI argument parsing
|
||||
52
apps/desktop/global.d.ts
vendored
52
apps/desktop/global.d.ts
vendored
@@ -1,52 +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/>.
|
||||
*/
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { BrowserWindow } from "electron";
|
||||
import * as undici_types from "undici";
|
||||
|
||||
declare global {
|
||||
export const {
|
||||
fetch,
|
||||
FormData,
|
||||
Headers,
|
||||
Request,
|
||||
Response
|
||||
}: typeof import("undici");
|
||||
|
||||
type FormData = undici_types.FormData;
|
||||
type Headers = undici_types.Headers;
|
||||
type HeadersInit = undici_types.HeadersInit;
|
||||
type BodyInit = undici_types.BodyInit;
|
||||
type Request = undici_types.Request;
|
||||
type RequestInit = undici_types.RequestInit;
|
||||
type RequestInfo = undici_types.RequestInfo;
|
||||
type RequestMode = undici_types.RequestMode;
|
||||
type RequestRedirect = undici_types.RequestRedirect;
|
||||
type RequestCredentials = undici_types.RequestCredentials;
|
||||
type RequestDestination = undici_types.RequestDestination;
|
||||
type ReferrerPolicy = undici_types.ReferrerPolicy;
|
||||
type Response = undici_types.Response;
|
||||
type ResponseInit = undici_types.ResponseInit;
|
||||
type ResponseType = undici_types.ResponseType;
|
||||
|
||||
var window: BrowserWindow | null;
|
||||
var RELEASE: boolean;
|
||||
var MAC_APP_STORE: boolean;
|
||||
}
|
||||
4879
apps/desktop/package-lock.json
generated
4879
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,72 +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 path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { $, argv, os } from "zx";
|
||||
|
||||
$.env = process.env;
|
||||
|
||||
const sodiumNativePrebuildPath = path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`,
|
||||
`${os.platform()}-x64`
|
||||
);
|
||||
|
||||
if (argv.rebuild) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await $`cd ../web/ && npm run build:desktop`;
|
||||
|
||||
await fs.cp(path.join("..", "web", "build"), "build", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (argv.variant === "mas") {
|
||||
await $`npm run bundle:mas`;
|
||||
} else {
|
||||
await $`npm run bundle`;
|
||||
}
|
||||
|
||||
await $`tsc`;
|
||||
await $`npm rebuild`;
|
||||
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath,
|
||||
path.join("build", "prebuilds", `${process.platform}-x64`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
|
||||
if (argv.run) {
|
||||
await $`npm run builder -- --linux AppImage:x64`;
|
||||
|
||||
await $`./output/notesnook_linux_x86_64.AppImage`;
|
||||
}
|
||||
@@ -1,117 +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 path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { watch } from "turbowatch";
|
||||
import { $, within } from "zx";
|
||||
|
||||
const sodiumNativePrebuildPath = path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`
|
||||
);
|
||||
let isServerRunning = false;
|
||||
|
||||
async function main() {
|
||||
const { shutdown } = await watch({
|
||||
project: path.join(__dirname, ".."),
|
||||
cwd: path.join(__dirname, ".."),
|
||||
triggers: [
|
||||
{
|
||||
expression: [
|
||||
"allof",
|
||||
["not", ["dirname", "node_modules"]],
|
||||
["match", "*.ts", "basename"]
|
||||
],
|
||||
name: "dev",
|
||||
// retry: { retries: 0 },
|
||||
onTeardown: async () => {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
},
|
||||
onChange: async ({ spawn: $, first, log }) => {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
}
|
||||
|
||||
await $`npm run build:electron`;
|
||||
await $`tsc`;
|
||||
|
||||
if (first) {
|
||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!isServerRunning) {
|
||||
await spawnAndWaitUntil(
|
||||
path.join(__dirname, "..", "..", "web"),
|
||||
"npm run start:desktop",
|
||||
(data) => data.includes("Compiled successfully!")
|
||||
);
|
||||
isServerRunning = true;
|
||||
}
|
||||
|
||||
log("Starting desktop app!");
|
||||
|
||||
await $`npx electron ${path.join("build", "electron.js")}`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// SIGINT is the signal sent when we press Ctrl+C
|
||||
process.once("SIGINT", () => {
|
||||
void shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
function spawnAndWaitUntil(
|
||||
cwd: string,
|
||||
cmd: string,
|
||||
predicate: (data: string) => boolean
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
within(async () => {
|
||||
$.env = process.env;
|
||||
$.quote = (c) => c;
|
||||
|
||||
try {
|
||||
const s = $`cd ${cwd} && ${cmd}`;
|
||||
s.stdout.on("data", (data) => {
|
||||
if (predicate(data)) resolve(undefined); //
|
||||
});
|
||||
await s;
|
||||
} catch (e) {
|
||||
//ignore
|
||||
} finally {
|
||||
isServerRunning = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,58 +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 { initTRPC } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { EventEmitter } from "events";
|
||||
import TypedEventEmitter from "typed-emitter";
|
||||
|
||||
export type AppEvents = {
|
||||
onCreateItem(name: "note" | "notebook" | "reminder"): void;
|
||||
};
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
const typedEmitter = emitter as TypedEventEmitter<AppEvents>;
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const bridgeRouter = t.router({
|
||||
onCreateItem: createSubscription("onCreateItem")
|
||||
});
|
||||
|
||||
export const bridge: AppEvents = new Proxy({} as AppEvents, {
|
||||
get(_t, name) {
|
||||
if (typeof name === "symbol") return;
|
||||
return (...args: unknown[]) => {
|
||||
emitter.emit(name, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function createSubscription<TName extends keyof AppEvents>(eventName: TName) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<Parameters<AppEvents[TName]>[0]>((emit) => {
|
||||
const listener: AppEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
typedEmitter.addListener(eventName, listener);
|
||||
return () => {
|
||||
typedEmitter.removeListener(eventName, listener);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,39 +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 zlib from "node:zlib";
|
||||
import utils from "node:util";
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const gzipAsync = utils.promisify(zlib.gzip);
|
||||
const gunzipAsync = utils.promisify(zlib.gunzip);
|
||||
|
||||
export const compressionRouter = t.router({
|
||||
gzip: t.procedure
|
||||
.input(z.object({ data: z.string(), level: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { data, level } = input;
|
||||
return (await gzipAsync(data, { level })).toString("base64");
|
||||
}),
|
||||
gunzip: t.procedure.input(z.string()).query(async ({ input }) => {
|
||||
return (await gunzipAsync(Buffer.from(input, "base64"))).toString("utf-8");
|
||||
})
|
||||
});
|
||||
@@ -1,41 +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 { initTRPC } from "@trpc/server";
|
||||
import { compressionRouter } from "./compression";
|
||||
import { osIntegrationRouter } from "./os-integration";
|
||||
import { spellCheckerRouter } from "./spell-checker";
|
||||
import { updaterRouter } from "./updater";
|
||||
import { bridgeRouter } from "./bridge";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const router = t.router({
|
||||
compress: compressionRouter,
|
||||
integration: osIntegrationRouter,
|
||||
spellChecker: spellCheckerRouter,
|
||||
updater: updaterRouter,
|
||||
bridge: bridgeRouter
|
||||
});
|
||||
|
||||
export const api = router.createCaller({});
|
||||
|
||||
// Export type router type signature,
|
||||
// NOT the router itself.
|
||||
export type AppRouter = typeof router;
|
||||
@@ -1,160 +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 { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
import { getTheme, setTheme, Theme } from "../utils/theme";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { platform } from "os";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const NotificationOptions = z.object({
|
||||
title: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
silent: z.boolean().optional(),
|
||||
timeoutType: z.union([z.literal("default"), z.literal("never")]).optional(),
|
||||
urgency: z
|
||||
.union([z.literal("normal"), z.literal("critical"), z.literal("low")])
|
||||
.optional(),
|
||||
tag: z.string()
|
||||
});
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
globalThis.window?.webContents.setZoomFactor(factor);
|
||||
config.zoomFactor = factor;
|
||||
}),
|
||||
|
||||
privacyMode: t.procedure.query(() => config.privacyMode),
|
||||
setPrivacyMode: t.procedure
|
||||
.input(z.boolean())
|
||||
.mutation(({ input: privacyMode }) => {
|
||||
if (!globalThis.window || !["win32", "darwin"].includes(process.platform))
|
||||
return;
|
||||
|
||||
globalThis.window.setContentProtection(privacyMode);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
globalThis.window.setThumbnailClip(
|
||||
privacyMode
|
||||
? { x: 0, y: 0, width: 1, height: 1 }
|
||||
: { x: 0, y: 0, width: 0, height: 0 }
|
||||
);
|
||||
}
|
||||
config.privacyMode = privacyMode;
|
||||
}),
|
||||
|
||||
desktopIntegration: t.procedure.query(() => config.desktopSettings),
|
||||
setDesktopIntegration: t.procedure
|
||||
.input(DesktopIntegration)
|
||||
.mutation(({ input: settings }) => {
|
||||
if (settings.autoStart) {
|
||||
AutoLaunch.enable(settings.startMinimized);
|
||||
} else {
|
||||
AutoLaunch.disable();
|
||||
}
|
||||
config.desktopSettings = settings;
|
||||
}),
|
||||
|
||||
selectDirectory: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
buttonLabel: z.string().optional(),
|
||||
defaultPath: z.string().optional()
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (!globalThis.window) return undefined;
|
||||
|
||||
const { title, buttonLabel, defaultPath } = input;
|
||||
|
||||
const result = await dialog.showOpenDialog(globalThis.window, {
|
||||
title,
|
||||
buttonLabel,
|
||||
properties: ["openDirectory"],
|
||||
defaultPath: defaultPath && resolvePath(defaultPath)
|
||||
});
|
||||
if (result.canceled) return undefined;
|
||||
|
||||
return result.filePaths[0];
|
||||
}),
|
||||
|
||||
saveFile: t.procedure
|
||||
.input(z.object({ data: z.string(), filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { data, filePath } = input;
|
||||
if (!data || !filePath) return;
|
||||
|
||||
const resolvedPath = resolvePath(filePath);
|
||||
|
||||
mkdirSync(dirname(resolvedPath), { recursive: true });
|
||||
writeFileSync(resolvedPath, data);
|
||||
}),
|
||||
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
const notification = new Notification({
|
||||
...input,
|
||||
icon: join(
|
||||
__dirname,
|
||||
platform() === "win32" ? "app.ico" : "favicon-72x72.png"
|
||||
)
|
||||
});
|
||||
notification.show();
|
||||
if (input.urgency === "critical") {
|
||||
shell.beep();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
notification.once("close", () => resolve(undefined));
|
||||
notification.once("click", () => resolve(input.tag));
|
||||
});
|
||||
}),
|
||||
openPath: t.procedure
|
||||
.input(z.object({ type: z.literal("path"), link: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { type, link } = input;
|
||||
if (type === "path") return shell.openPath(resolvePath(link));
|
||||
}),
|
||||
bringToFront: t.procedure.query(() => bringToFront()),
|
||||
changeTheme: t.procedure
|
||||
.input(Theme)
|
||||
.mutation(({ input }) => setTheme(input)),
|
||||
|
||||
onThemeChanged: t.procedure.subscription(() =>
|
||||
observable<"dark" | "light">((emit) => {
|
||||
nativeTheme.on("updated", () => {
|
||||
if (getTheme() === "system") {
|
||||
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
});
|
||||
@@ -1,71 +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 { initTRPC } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { CancellationToken, autoUpdater } from "electron-updater";
|
||||
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
|
||||
|
||||
type UpdateInfo = { version: string };
|
||||
type Progress = { percent: number };
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
const cancellationToken = new CancellationToken();
|
||||
await autoUpdater.downloadUpdate(cancellationToken);
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
await autoUpdater.checkForUpdates();
|
||||
}),
|
||||
|
||||
onChecking: createSubscription("checking-for-update"),
|
||||
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
|
||||
"update-downloaded"
|
||||
),
|
||||
onDownloadProgress: createSubscription<"download-progress", Progress>(
|
||||
"download-progress"
|
||||
),
|
||||
onNotAvailable: createSubscription<"update-not-available", UpdateInfo>(
|
||||
"update-not-available"
|
||||
),
|
||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||
"update-available"
|
||||
),
|
||||
onError: createSubscription("error")
|
||||
});
|
||||
|
||||
function createSubscription<
|
||||
TName extends keyof AppUpdaterEvents,
|
||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||
>(eventName: TName) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<TReturnType>((emit) => {
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.addListener(eventName, listener);
|
||||
return () => {
|
||||
autoUpdater.removeListener(eventName, listener);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,44 +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/>.
|
||||
*/
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
import { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
declare global {
|
||||
var os: string;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var NativeNNCrypto: new () => NNCrypto;
|
||||
}
|
||||
console.log("HELLO", process);
|
||||
process.once("loaded", async () => {
|
||||
console.log("HELLO!");
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
onMessage: (callback) =>
|
||||
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
|
||||
};
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto = NNCrypto;
|
||||
globalThis.os = MAC_APP_STORE ? "mas" : process.platform;
|
||||
@@ -1,55 +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 { nativeTheme } from "electron";
|
||||
import { JSONStorage } from "./json-storage";
|
||||
import { z } from "zod";
|
||||
|
||||
export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean(),
|
||||
startMinimized: z.boolean(),
|
||||
minimizeToSystemTray: z.boolean(),
|
||||
closeToSystemTray: z.boolean()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
|
||||
export const config = {
|
||||
desktopSettings: <DesktopIntegration>{
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: false,
|
||||
zoomFactor: 0,
|
||||
theme: nativeTheme.themeSource
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
for (const key in config) {
|
||||
const defaultValue = config[<ConfigKey>key];
|
||||
if (Object.hasOwn(config, key)) {
|
||||
Object.defineProperty(config, key, {
|
||||
get: () => JSONStorage.get(key, defaultValue),
|
||||
set: (value) => JSONStorage.set(key, value)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"lib": ["ESNext"]
|
||||
},
|
||||
"include": ["src", "global.d.ts"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -31,6 +31,14 @@ import { TipManager } from "./services/tip-manager";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { View } from "react-native";
|
||||
import { useState } from "react";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
NetInfo.configure({
|
||||
reachabilityUrl: "https://notesnook.com",
|
||||
reachabilityTest: (response) => {
|
||||
if (!response) return false;
|
||||
return response?.status >= 200 && response?.status < 300;
|
||||
}
|
||||
});
|
||||
|
||||
SettingsService.init();
|
||||
SettingsService.checkOrientation();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -22,17 +22,10 @@ import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
@@ -46,27 +39,12 @@ export async function deriveCryptoKey(name, data) {
|
||||
credentials.key,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeIOSKeychain(username, password) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
username,
|
||||
password,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
@@ -74,9 +52,6 @@ export async function getCryptoKey(_name) {
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// upgrades ios keychain to use accessGroups
|
||||
// so we have access to keychain in share extension.
|
||||
await upgradeIOSKeychain(credentials.username, credentials.password);
|
||||
return credentials.password;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -68,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();
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import {
|
||||
decrypt,
|
||||
deriveCryptoKey,
|
||||
|
||||
@@ -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 "react-native-blob-util";
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -17,171 +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 Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/utils/filename";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
||||
import { presentSheet, ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { cacheDir } from "./utils";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
Fail: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export function downloadAllAttachments() {
|
||||
const attachments = db.attachments.all;
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||
: `${cacheDir}/notesnook-attachments.zip`;
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
let attachment = attachments[i];
|
||||
const hash = attachment.metadata.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Success
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
console.log("Error downloading attachment", e);
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (result?.size) {
|
||||
let sub;
|
||||
try {
|
||||
onProgress?.(0, `Zipping... Please wait`);
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||
);
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
onProgress(1, `Saving zip file... Please wait`);
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
|
||||
onProgress?.(1, `Done`);
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
console.log("Error zipping attachments", e);
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hash,
|
||||
global = true,
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined
|
||||
}
|
||||
) {
|
||||
export default async function downloadAttachment(hash, global = true) {
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
@@ -189,23 +37,16 @@ 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 {
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db.fs.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
@@ -213,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,
|
||||
@@ -227,51 +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 &&
|
||||
!isImage(attachment.metadata?.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
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);
|
||||
@@ -281,8 +112,5 @@ export default async function downloadAttachment(
|
||||
.catch(console.log);
|
||||
}
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hosts from "@notesnook/core/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,54 +19,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { cacheDir, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists =
|
||||
(await RNFetchBlob.fs.exists(path)) ||
|
||||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
|
||||
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
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,
|
||||
{
|
||||
...cipherData,
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
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);
|
||||
@@ -75,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,
|
||||
|
||||
@@ -17,13 +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 RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
@@ -38,19 +35,9 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||
const uploadUrl = await res.text();
|
||||
if (!uploadUrl) throw new Error("Unable to resolve upload url");
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
uploadFilePath = appGroupPath;
|
||||
}
|
||||
let request = RNFetchBlob.config({
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
IOSBackgroundTask: true
|
||||
})
|
||||
.fetch(
|
||||
"PUT",
|
||||
@@ -58,7 +45,7 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
{
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(uploadFilePath)
|
||||
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
|
||||
)
|
||||
.uploadProgress((sent, total) => {
|
||||
useAttachmentStore
|
||||
@@ -76,10 +63,7 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
if (result) {
|
||||
let attachment = db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (
|
||||
!isImage(attachment.metadata.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
if (!attachment.metadata.type.startsWith("image/")) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +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 * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
|
||||
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
@@ -64,22 +62,3 @@ export function cancelable(operation) {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
console.log(e, r);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (let uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
type="gray"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
right: 0
|
||||
}}
|
||||
iconSize={24}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingVertical: 0,
|
||||
|
||||
@@ -102,21 +102,15 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
ToastEvent.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
setFailed(null);
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
setAttachments([...db.attachments.all]);
|
||||
setLoading({
|
||||
name: null
|
||||
@@ -215,7 +209,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
@@ -227,7 +221,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
>
|
||||
{formatBytes(attachment.length)}
|
||||
@@ -238,7 +232,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
>
|
||||
{attachment.noteIds.length} note
|
||||
@@ -254,7 +248,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
>
|
||||
{attachment.metadata.hash}
|
||||
@@ -310,7 +304,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
}}
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
|
||||
</PressableButton>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { formatBytes } from "../../utils";
|
||||
import { SIZE } from "../../utils/size";
|
||||
@@ -36,26 +37,20 @@ function getFileExtension(filename) {
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
|
||||
export const AttachmentItem = ({
|
||||
attachment,
|
||||
encryption,
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading
|
||||
}) => {
|
||||
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
encryption
|
||||
);
|
||||
const encryptionProgress = useAttachmentStore(
|
||||
(state) => state.encryptionProgress
|
||||
);
|
||||
|
||||
const onPress = () => {
|
||||
if (!pressable) return;
|
||||
Actions.present(attachment, setAttachments, attachment.metadata.hash);
|
||||
};
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -118,22 +113,20 @@ export const AttachmentItem = ({
|
||||
{attachment.metadata.filename}
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentProgress ? (
|
||||
{currentProgress || encryptionProgress || encryption ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
if (encryption) return;
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
@@ -146,7 +139,13 @@ export const AttachmentItem = ({
|
||||
>
|
||||
<ProgressCircleComponent
|
||||
size={SIZE.xxl}
|
||||
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
|
||||
progress={
|
||||
encryptionProgress
|
||||
? encryptionProgress
|
||||
: currentProgress?.value
|
||||
? currentProgress?.value / 100
|
||||
: 0
|
||||
}
|
||||
showsText
|
||||
textStyle={{
|
||||
fontSize: 10
|
||||
|
||||
@@ -1,273 +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 { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
|
||||
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
value: 0,
|
||||
statusText: "Download started... Please wait"
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef();
|
||||
|
||||
const onDownload = async () => {
|
||||
update({
|
||||
disableClosing: true
|
||||
});
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
attachments,
|
||||
(progress, statusText) => setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update({
|
||||
disableClosing: false
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update({
|
||||
disableClosing: false
|
||||
});
|
||||
canceled.current = true;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs.cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
groupId.current = null;
|
||||
};
|
||||
|
||||
const successResults = () => {
|
||||
const results = [];
|
||||
for (let [key, value] of result.entries()) {
|
||||
if (value.status === 1) results.push(db.attachments.attachment(key));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
const failedResults = () => {
|
||||
const results = [];
|
||||
for (let [key, value] of result.entries()) {
|
||||
if (value.status === 0) results.push(db.attachments.attachment(key));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>
|
||||
{downloading
|
||||
? "Downloading attachments"
|
||||
: result?.size
|
||||
? "Downloaded attachments"
|
||||
: "Download attachments"}
|
||||
</Heading>
|
||||
|
||||
{downloading ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{progress.statusText}
|
||||
</Paragraph>
|
||||
) : result?.size ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{getResultText()}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Are you sure you want to download all attachments
|
||||
{isNote ? " of this note?" : "?"}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{downloading ? (
|
||||
<View
|
||||
style={{
|
||||
width: 200,
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={progress.value ? progress.value / attachments.length : 0}
|
||||
unfilledColor={colors.nav}
|
||||
color={colors.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
width: "100%",
|
||||
minHeight: 60,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments : []}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 60
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon}>No downloads in progress.</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
attachment={item}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{result?.size ? (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={close}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
) : !downloading ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={close}
|
||||
type="grayBg"
|
||||
title="No"
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginLeft: 5
|
||||
}}
|
||||
onPress={onDownload}
|
||||
type="accent"
|
||||
title="Yes"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={cancel}
|
||||
type="error"
|
||||
title="Cancel"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (context, attachments, isNote) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
<DownloadAttachments
|
||||
close={close}
|
||||
attachments={attachments}
|
||||
isNote={isNote}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default DownloadAttachments;
|
||||
@@ -17,61 +17,82 @@ 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 { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
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 SheetProvider from "../sheet-provider";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Toast } from "../toast";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import SheetWrapper from "../ui/sheet";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
isAudio,
|
||||
isDocument,
|
||||
isImage,
|
||||
isVideo
|
||||
} from "@notesnook/core/utils/filename";
|
||||
|
||||
export const AttachmentDialog = ({ note }) => {
|
||||
export const AttachmentDialog = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
|
||||
const [attachments, setAttachments] = useState(
|
||||
note
|
||||
? db.attachments.ofNote(note.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);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
|
||||
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) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments([...attachments]);
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => {
|
||||
let results = db.lookup.attachments(
|
||||
attachments,
|
||||
db.attachments.all,
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
@@ -83,239 +104,107 @@ export const AttachmentDialog = ({ note }) => {
|
||||
<AttachmentItem setAttachments={setAttachments} attachment={item} />
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
setLoading(true);
|
||||
const checkedAttachments = [];
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
if (result.failed) {
|
||||
await db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
checkedAttachments.push(
|
||||
db.attachments.attachment(attachment.metadata.hash)
|
||||
);
|
||||
setAttachments([...checkedAttachments]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Documents",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
filterBy: "audio"
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = (type) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
isDocument;
|
||||
switch (type) {
|
||||
case "all":
|
||||
return attachments;
|
||||
case "images":
|
||||
return attachments.filter((attachment) =>
|
||||
isImage(attachment.metadata.type)
|
||||
);
|
||||
case "video":
|
||||
return attachments.filter((attachment) =>
|
||||
isVideo(attachment.metadata.type)
|
||||
);
|
||||
case "audio":
|
||||
return attachments.filter((attachment) =>
|
||||
isAudio(attachment.metadata.type)
|
||||
);
|
||||
case "documents":
|
||||
return attachments.filter((attachment) =>
|
||||
isDocument(attachment.metadata.type)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
onClose={async () => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>Attachments</Heading>
|
||||
<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}
|
||||
|
||||
<View
|
||||
<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={{
|
||||
flexDirection: "row"
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
name="check-all"
|
||||
customStyle={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
color={colors.pri}
|
||||
size={SIZE.lg}
|
||||
onPress={onCheck}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
customStyle={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
color={colors.pri}
|
||||
onPress={() => {
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
!!note
|
||||
);
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={10}
|
||||
initialNumToRender={10}
|
||||
windowSize={5}
|
||||
stickyHeaderIndices={[0]}
|
||||
ListHeaderComponent={
|
||||
<ScrollView
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.bg
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minWidth: "100%"
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) => (
|
||||
<Button
|
||||
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
||||
key={item.title}
|
||||
title={
|
||||
item.title +
|
||||
` (${filterAttachments(item.filterBy)?.length || 0})`
|
||||
}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
flexGrow: 1,
|
||||
borderBottomColor:
|
||||
currentFilter !== item.filterBy
|
||||
? "transparent"
|
||||
: colors.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(filterAttachments(item.filterBy));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
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 note={note} />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Platform, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
@@ -75,7 +75,7 @@ const AuthModal = () => {
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
overlayOpacity={0}
|
||||
statusBarTranslucent={true}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
|
||||
visible={true}
|
||||
onClose={close}
|
||||
@@ -84,7 +84,6 @@ const AuthModal = () => {
|
||||
background={colors.bg}
|
||||
transparent={false}
|
||||
animated={false}
|
||||
avoidKeyboardResize
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
@@ -102,7 +101,7 @@ const AuthModal = () => {
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: insets.top,
|
||||
top: Platform.OS === "ios" ? insets.top : 0,
|
||||
zIndex: 999,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
@@ -127,11 +126,11 @@ const AuthModal = () => {
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
title="Skip for now"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
iconSize={20}
|
||||
type="gray"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
|
||||
@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
@@ -29,12 +30,13 @@ import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SVG } from "./background";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -62,7 +64,6 @@ export const Login = ({ changeMode }) => {
|
||||
await sleep(500);
|
||||
Progress.present();
|
||||
});
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
@@ -81,70 +82,62 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 0.8,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.nav,
|
||||
borderBottomWidth: 1,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
src={SVG(colors.night ? colors.icon : "black")}
|
||||
height={700}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
textAlign: "center"
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
size={30}
|
||||
color={colors.heading}
|
||||
>
|
||||
Login to your {"\n"}account
|
||||
Welcome back!
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
textAlign: "center",
|
||||
marginTop: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{"Don't have an account? Sign up"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
@@ -154,10 +147,10 @@ export const Login = ({ changeMode }) => {
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.bg,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20,
|
||||
flexGrow: 1
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -224,22 +217,28 @@ export const Login = ({ changeMode }) => {
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
marginTop: 25,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
title={
|
||||
loading
|
||||
? null
|
||||
: step === LoginSteps.emailAuth
|
||||
? "Login"
|
||||
: "Continue"
|
||||
}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
@@ -262,34 +261,9 @@ export const Login = ({ changeMode }) => {
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.accent }}
|
||||
>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 += "*";
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
@@ -31,10 +32,11 @@ import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SVG } from "./background";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -48,7 +50,6 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
@@ -95,77 +96,69 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.nav,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Create your {"\n"}account
|
||||
</Heading>
|
||||
<SvgView
|
||||
src={SVG(colors.night ? colors.icon : "black")}
|
||||
height={700}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.bg,
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
flexGrow: 0.5
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: Dimensions.get("window").height < 700 ? -75 : 15
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={30}
|
||||
color={colors.heading}
|
||||
>
|
||||
Create your account
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
textAlign: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
Already have an account? Log in
|
||||
</Paragraph>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.bg,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -224,13 +217,37 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={12}
|
||||
marginBottom={5}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
signup();
|
||||
}}
|
||||
type="accent"
|
||||
title={loading ? null : "Agree and continue"}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
textAlign: "center",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
alignSelf: "center",
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
@@ -246,7 +263,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}}
|
||||
color={colors.accent}
|
||||
>
|
||||
Terms of Service{" "}
|
||||
terms of service{" "}
|
||||
</Paragraph>
|
||||
and{" "}
|
||||
<Paragraph
|
||||
@@ -259,46 +276,11 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}}
|
||||
color={colors.accent}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Paragraph>{" "}
|
||||
You also agree to recieve marketing emails from us which you can
|
||||
opt-out of from app settings.
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
Already have an account?{" "}
|
||||
<Paragraph size={SIZE.xs + 1} style={{ color: colors.accent }}>
|
||||
Login
|
||||
</Paragraph>
|
||||
privacy policy.
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,10 +34,13 @@ 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";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -47,6 +51,7 @@ const DialogProvider = () => {
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<AddTopicDialog colors={colors} />
|
||||
<AddNotebookSheet colors={colors} />
|
||||
<PremiumDialog colors={colors} />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
@@ -56,12 +61,15 @@ const DialogProvider = () => {
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<AddToNotebookSheet colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<PublishNoteSheet />
|
||||
<ManageTagsSheet />
|
||||
<AttachmentDialog />
|
||||
{loading ? null : <Expiring />}
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,8 +46,7 @@ const BaseDialog = ({
|
||||
animated = true,
|
||||
bounce = true,
|
||||
closeOnTouch = true,
|
||||
useSafeArea = true,
|
||||
avoidKeyboardResize = false
|
||||
useSafeArea = true
|
||||
}) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
|
||||
@@ -95,7 +94,7 @@ const BaseDialog = ({
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
enabled={!floating && Platform.OS === "ios" && !avoidKeyboardResize}
|
||||
enabled={!floating && Platform.OS === "ios"}
|
||||
behavior="padding"
|
||||
>
|
||||
<BouncingView
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,324 +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, useRef, useState } from "react";
|
||||
import { Dimensions, TextInput, View } from "react-native";
|
||||
import {
|
||||
addOrientationListener,
|
||||
removeOrientationListener
|
||||
} from "react-native-orientation";
|
||||
import Pdf from "react-native-pdf";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import downloadAttachment from "../../../common/filesystem/download-attachment";
|
||||
import { cacheDir } from "../../../common/filesystem/utils";
|
||||
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import SheetProvider from "../../sheet-provider";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
|
||||
const attachmentSnapshotsKey = "___attachmentsnapshots";
|
||||
const usePDFSnapshot = (attachment) => {
|
||||
const snapshots = useRef(MMKV.getMap(attachmentSnapshotsKey) || {});
|
||||
const snapshot = useRef(snapshots[attachment?.id]);
|
||||
|
||||
function saveSnapshot(ss) {
|
||||
if (!attachment) return;
|
||||
snapshots.current[attachment.id] = ss;
|
||||
MMKV.setMap(attachmentSnapshotsKey, snapshots.current);
|
||||
snapshot.current = snapshots.current[attachment.id];
|
||||
}
|
||||
|
||||
return [snapshot, saveSnapshot];
|
||||
};
|
||||
|
||||
const PDFPreview = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pdfSource, setPDFSource] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [width, setWidth] = useState(WIN_WIDTH);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const inputRef = useRef();
|
||||
const pdfRef = useRef();
|
||||
const [attachment, setAttachment] = useState(null);
|
||||
const [password, setPassword] = useState("");
|
||||
const [progress] = useAttachmentProgress(attachment);
|
||||
const [snapshot, saveSnapshot] = usePDFSnapshot(attachment);
|
||||
const snapshotValue = useRef(snapshot.current);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("PDFPreview", open);
|
||||
return () => {
|
||||
eUnSubscribeEvent("PDFPreview", open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onOrientationChange = (o) => {
|
||||
if (o.includes("LANDSCAPE")) {
|
||||
setWidth(WIN_HEIGHT);
|
||||
} else {
|
||||
setWidth(WIN_WIDTH);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
addOrientationListener(onOrientationChange);
|
||||
return () => {
|
||||
removeOrientationListener(onOrientationChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = async (attachment) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
setAttachment(attachment);
|
||||
let hash = attachment.metadata.hash;
|
||||
if (!hash) return;
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
snapshotValue.current = snapshot.current;
|
||||
setPDFSource("file://" + path);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setPDFSource(null);
|
||||
setVisible(false);
|
||||
setPassword("");
|
||||
};
|
||||
|
||||
const onError = async (error) => {
|
||||
if (error?.message === "Password required or incorrect password.") {
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
context: attachment?.metadata?.hash,
|
||||
input: true,
|
||||
inputPlaceholder: "Enter password",
|
||||
positiveText: "Unlock",
|
||||
title: "Decrypt",
|
||||
paragraph: "Please input password to view pdf.",
|
||||
positivePress: (value) => {
|
||||
setTimeout(() => {
|
||||
setPassword(value);
|
||||
});
|
||||
},
|
||||
onClose: () => {
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
|
||||
<SheetProvider context={attachment?.metadata?.hash} />
|
||||
<Dialog context={attachment?.metadata?.hash} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "black"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Animated.View
|
||||
exiting={FadeOut}
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
indeterminate={!progress}
|
||||
color={colors.accent}
|
||||
borderColor="transparent"
|
||||
progress={parseInt(progress?.value || "100") / 100}
|
||||
/>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
color={colors.light}
|
||||
>
|
||||
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
|
||||
... Please wait
|
||||
</Paragraph>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
marginTop: insets.top,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 12,
|
||||
paddingLeft: 6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color={colors.light}
|
||||
name="arrow-left"
|
||||
onPress={close}
|
||||
customStyle={{
|
||||
marginRight: 12
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: 12
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
defaultValue={currentPage + ""}
|
||||
style={{
|
||||
color: colors.pri,
|
||||
padding: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
paddingVertical: 0,
|
||||
height: 25,
|
||||
backgroundColor: colors.nav,
|
||||
width: 40,
|
||||
textAlign: "center",
|
||||
marginRight: 4,
|
||||
borderRadius: 3,
|
||||
fontFamily: "OpenSans-Regular"
|
||||
}}
|
||||
selectTextOnFocus
|
||||
keyboardType="decimal-pad"
|
||||
onSubmitEditing={(event) => {
|
||||
setCurrentPage(event.nativeEvent.text);
|
||||
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
|
||||
}}
|
||||
blurOnSubmit
|
||||
/>
|
||||
<Paragraph color={colors.light}>/{numPages}</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color={colors.light}
|
||||
name="download"
|
||||
onPress={() => {
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{pdfSource ? (
|
||||
<Animated.View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
entering={FadeIn}
|
||||
>
|
||||
<Pdf
|
||||
source={{
|
||||
uri: pdfSource
|
||||
}}
|
||||
ref={pdfRef}
|
||||
onLoadComplete={(numberOfPages) => {
|
||||
setNumPages(numberOfPages);
|
||||
}}
|
||||
onPageChanged={(page) => {
|
||||
setCurrentPage(page);
|
||||
inputRef.current?.setNativeProps({
|
||||
text: page + ""
|
||||
});
|
||||
saveSnapshot({
|
||||
currentPage: page,
|
||||
scale: snapshot?.current?.scale
|
||||
});
|
||||
}}
|
||||
// scale={snapshotValue.current?.scale}
|
||||
// onScaleChanged={(scale) => {
|
||||
// saveSnapshot({
|
||||
// currentPage: snapshot?.current?.currentPage,
|
||||
// scale: scale
|
||||
// });
|
||||
// }}
|
||||
page={snapshotValue?.current?.currentPage}
|
||||
password={password}
|
||||
maxScale={6}
|
||||
onError={onError}
|
||||
onPressLink={(uri) => {
|
||||
console.log(`Link pressed: ${uri}`);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width,
|
||||
height: Dimensions.get("window").height
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</BaseDialog>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFPreview;
|
||||
@@ -135,6 +135,7 @@ const ResultDialog = () => {
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
onPress={close}
|
||||
height={50}
|
||||
fontSize={SIZE.md + 2}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -86,7 +86,7 @@ export const ProFeatures = ({ count = 6 }) => {
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.icon
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,14 +17,12 @@ 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";
|
||||
@@ -37,11 +35,10 @@ import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { AndroidModule } from "../../utils";
|
||||
import { eOpenAnnouncementDialog } from "../../utils/events";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { SVG } from "../auth/background";
|
||||
import Migrate from "../sheets/migrate";
|
||||
import NewFeature from "../sheets/new-feature/index";
|
||||
import { Update } from "../sheets/update";
|
||||
@@ -49,9 +46,14 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import Config from "react-native-config";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
import notifee from "@notifee/react-native";
|
||||
|
||||
|
||||
const Launcher = React.memo(
|
||||
function Launcher() {
|
||||
@@ -68,8 +70,7 @@ const Launcher = React.memo(
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const verifying = useRef(false);
|
||||
|
||||
const dbInitCompleted = useRef(false);
|
||||
const loadNotes = useCallback(async () => {
|
||||
if (verifyUser) {
|
||||
return;
|
||||
@@ -80,25 +81,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) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
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
|
||||
@@ -117,6 +121,9 @@ const Launcher = React.memo(
|
||||
if (!loading) {
|
||||
doAppLoadActions();
|
||||
}
|
||||
return () => {
|
||||
dbInitCompleted.current = false;
|
||||
};
|
||||
}, [doAppLoadActions, loading]);
|
||||
|
||||
const doAppLoadActions = useCallback(async () => {
|
||||
@@ -130,6 +137,7 @@ const Launcher = React.memo(
|
||||
if (NewFeature.present()) return;
|
||||
if (await checkAppUpdateAvailable()) return;
|
||||
if (await checkForRateAppRequest()) return;
|
||||
if (await checkNeedsBackup()) return;
|
||||
if (await PremiumService.getRemainingTrialDaysStatus()) return;
|
||||
|
||||
if (introCompleted) {
|
||||
@@ -143,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"
|
||||
@@ -172,13 +180,25 @@ const Launcher = React.memo(
|
||||
return false;
|
||||
};
|
||||
|
||||
const checkNeedsBackup = async () => {
|
||||
return false;
|
||||
// let { nextBackupRequestTime, reminder } = SettingsService.get();
|
||||
// if (reminder === 'off' || !reminder) {
|
||||
// if (nextBackupRequestTime < Date.now()) {
|
||||
// presentSheet({
|
||||
// title: 'Backup & restore',
|
||||
// paragraph: 'Please enable automatic backups to keep your data safe',
|
||||
// component: <SettingsBackupAndRestore isSheet={true} />
|
||||
// });
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
};
|
||||
|
||||
const onUnlockBiometrics = useCallback(async () => {
|
||||
if (!(await BiometricService.isBiometryAvailable())) return;
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await AndroidModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
}
|
||||
|
||||
let verified = await BiometricService.validateUser(
|
||||
"Unlock to access your notes",
|
||||
""
|
||||
@@ -187,9 +207,6 @@ const Launcher = React.memo(
|
||||
setVerifyUser(false);
|
||||
enabled(false);
|
||||
password.current = null;
|
||||
setTimeout(() => {
|
||||
verifying.current = false;
|
||||
}, 1);
|
||||
}
|
||||
}, [setVerifyUser]);
|
||||
|
||||
@@ -198,10 +215,7 @@ const Launcher = React.memo(
|
||||
}, [init, verifyUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (verifying.current || useUserStore.getState().shouldBlockVerifyUser)
|
||||
return;
|
||||
if (verifyUser && appState === "active") {
|
||||
verifying.current = true;
|
||||
onUnlockBiometrics();
|
||||
}
|
||||
}, [appState, onUnlockBiometrics, verifyUser]);
|
||||
@@ -227,10 +241,18 @@ const Launcher = React.memo(
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
zIndex: 999
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<SvgView src={SVG(colors.night ? "white" : "black")} height={700} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -266,13 +288,14 @@ const Launcher = React.memo(
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Unlock your notes
|
||||
Unlock to access your notes
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
fontSize: SIZE.md,
|
||||
maxWidth: "90%"
|
||||
}}
|
||||
>
|
||||
@@ -283,7 +306,8 @@ const Launcher = React.memo(
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.bg
|
||||
backgroundColor: colors.bg,
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
@@ -323,9 +347,14 @@ const Launcher = React.memo(
|
||||
<Button
|
||||
title="Unlock with Biometrics"
|
||||
width={250}
|
||||
height={45}
|
||||
style={{
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={onUnlockBiometrics}
|
||||
icon={"fingerprint"}
|
||||
type={user ? "grayAccent" : "accent"}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -178,29 +149,16 @@ const NoteItem = ({
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{compactMode ? (
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
)}
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
|
||||
{item.headline && !compactMode ? (
|
||||
<Paragraph
|
||||
@@ -215,223 +173,161 @@ const NoteItem = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{compactMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: 5,
|
||||
height: SIZE.md + 2
|
||||
}}
|
||||
>
|
||||
{!isTrash ? (
|
||||
<>
|
||||
{item.conflicted ? (
|
||||
<Icon
|
||||
name="alert-circle"
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
color={colors.red}
|
||||
/>
|
||||
) : null}
|
||||
<TimeSince
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: 5,
|
||||
height: SIZE.md + 2
|
||||
}}
|
||||
>
|
||||
{!isTrash ? (
|
||||
<>
|
||||
{item.conflicted ? (
|
||||
<Icon
|
||||
name="alert-circle"
|
||||
style={{
|
||||
fontSize: SIZE.xs,
|
||||
color: colors.icon,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={item[dateBy]}
|
||||
updateFrequency={
|
||||
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
|
||||
size={SIZE.sm}
|
||||
color={colors.red}
|
||||
/>
|
||||
) : null}
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: SIZE.xs,
|
||||
color: colors.icon,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={item[dateBy]}
|
||||
updateFrequency={
|
||||
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
|
||||
}
|
||||
/>
|
||||
|
||||
{attachmentCount > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={SIZE.md} color={colors.icon} />
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{attachmentCount}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{item.pinned ? (
|
||||
<Icon
|
||||
testID="icon-pinned"
|
||||
name="pin-outline"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={
|
||||
COLORS_NOTE[item.color?.toLowerCase()] || colors.accent
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{attachmentCount > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={SIZE.md}
|
||||
color={colors.icon}
|
||||
/>
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{attachmentCount}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{item.pinned ? (
|
||||
<Icon
|
||||
testID="icon-pinned"
|
||||
name="pin-outline"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={
|
||||
COLORS_NOTE[item.color?.toLowerCase()] || colors.accent
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.locked ? (
|
||||
<Icon
|
||||
name="lock"
|
||||
testID="note-locked-icon"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={colors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.favorite ? (
|
||||
<Icon
|
||||
testID="icon-star"
|
||||
name="star-outline"
|
||||
size={SIZE.md}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color="orange"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isTrash && !compactMode && tags
|
||||
? tags.map((item) =>
|
||||
item.id ? (
|
||||
<Button
|
||||
title={"#" + item.alias}
|
||||
key={item.id}
|
||||
height={23}
|
||||
type="gray"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
paddingHorizontal: 6,
|
||||
marginRight: 4,
|
||||
zIndex: 10,
|
||||
maxWidth: tags.length > 1 ? 130 : null
|
||||
}}
|
||||
onPress={() => navigateToTag(item)}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Paragraph
|
||||
{item.locked ? (
|
||||
<Icon
|
||||
name="lock"
|
||||
testID="note-locked-icon"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
Deleted on{" "}
|
||||
{item && item.dateDeleted
|
||||
? new Date(item.dateDeleted).toISOString().slice(0, 10)
|
||||
: null}
|
||||
</Paragraph>
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Paragraph
|
||||
color={colors.accent}
|
||||
size={SIZE.xs}
|
||||
{item.favorite ? (
|
||||
<Icon
|
||||
testID="icon-star"
|
||||
name="star"
|
||||
size={SIZE.md}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
color="orange"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isTrash && !compactMode && tags
|
||||
? tags.map((item) =>
|
||||
item.id ? (
|
||||
<Button
|
||||
title={"#" + item.alias}
|
||||
key={item.id}
|
||||
height={23}
|
||||
type="gray"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
paddingHorizontal: 6,
|
||||
marginRight: 4,
|
||||
zIndex: 10,
|
||||
maxWidth: tags.length > 1 ? 130 : null
|
||||
}}
|
||||
onPress={() => navigateToTag(item)}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
Deleted on{" "}
|
||||
{item && item.dateDeleted
|
||||
? new Date(item.dateDeleted).toISOString().slice(0, 10)
|
||||
: null}
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph
|
||||
color={colors.accent}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
<IconButton
|
||||
testID={notesnook.listitem.menu}
|
||||
color={colors.pri}
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => !noOpen && showActionSheet(item, isTrash)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
borderRadius: 100,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{compactMode ? (
|
||||
<>
|
||||
{item.conflicted ? (
|
||||
<Icon
|
||||
name="alert-circle"
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
color={colors.red}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.locked ? (
|
||||
<Icon
|
||||
name="lock"
|
||||
testID="note-locked-icon"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={colors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.favorite ? (
|
||||
<Icon
|
||||
testID="icon-star"
|
||||
name="star-outline"
|
||||
size={SIZE.md}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color="orange"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: SIZE.xs,
|
||||
color: colors.icon,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={item[dateBy]}
|
||||
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
testID={notesnook.listitem.menu}
|
||||
color={colors.pri}
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => !noOpen && showActionSheet(item, isTrash)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
borderRadius: 100,
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "../../../utils/time";
|
||||
|
||||
const showActionSheet = (item) => {
|
||||
Properties.present(item);
|
||||
@@ -64,28 +63,15 @@ export const NotebookItem = ({
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{compactMode ? (
|
||||
<Paragraph
|
||||
size={SIZE.sm}
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Heading
|
||||
size={SIZE.md}
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
size={SIZE.md}
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
{isTopic || !item.description || compactMode ? null : (
|
||||
<Paragraph
|
||||
size={SIZE.sm}
|
||||
@@ -136,51 +122,40 @@ export const NotebookItem = ({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!compactMode ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
marginTop: 5,
|
||||
height: SIZE.md + 2
|
||||
}}
|
||||
>
|
||||
{isTrash ? (
|
||||
<>
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlignVertical: "center",
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{"Deleted on " +
|
||||
new Date(item.dateDeleted).toISOString().slice(0, 10)}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
color={colors.accent}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlignVertical: "center",
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
marginTop: 5,
|
||||
height: SIZE.md + 2
|
||||
}}
|
||||
>
|
||||
{isTrash ? (
|
||||
<>
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlignVertical: "center",
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{getFormattedDate(item[dateBy], "date")}
|
||||
{"Deleted on " +
|
||||
new Date(item.dateDeleted).toISOString().slice(0, 10)}
|
||||
</Paragraph>
|
||||
)}
|
||||
<Paragraph
|
||||
color={colors.accent}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlignVertical: "center",
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
@@ -188,66 +163,50 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: "0 notes"}
|
||||
{new Date(item[dateBy]).toDateString().substring(4)}
|
||||
</Paragraph>
|
||||
)}
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: "0 notes"}
|
||||
</Paragraph>
|
||||
|
||||
{item.pinned ? (
|
||||
<Icon
|
||||
name="pin-outline"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
marginTop: 2
|
||||
}}
|
||||
color={colors.accent}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
{item.pinned ? (
|
||||
<Icon
|
||||
name="pin-outline"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
marginTop: 2
|
||||
}}
|
||||
color={colors.accent}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
<IconButton
|
||||
color={colors.heading}
|
||||
name="dots-horizontal"
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
size={SIZE.xl}
|
||||
onPress={() => showActionSheet(item)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
borderRadius: 100,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{compactMode ? (
|
||||
<>
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: "0 notes"}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
color={colors.heading}
|
||||
name="dots-horizontal"
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
size={SIZE.xl}
|
||||
onPress={() => showActionSheet(item)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
borderRadius: 100,
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -108,7 +108,7 @@ const ReminderItem = React.memo(
|
||||
color={colors.errorText}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
@@ -133,7 +133,7 @@ const ReminderItem = React.memo(
|
||||
>
|
||||
<Icon name="reload" size={SIZE.md} color={colors.accent} />
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
@@ -147,7 +147,7 @@ const ReminderItem = React.memo(
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
checkIsActive={false}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
|
||||
@@ -42,16 +42,11 @@ const SelectionWrapper = ({
|
||||
const notesListMode = useSettingStore(
|
||||
(state) => state.settings.notesListMode
|
||||
);
|
||||
const listMode =
|
||||
item.type === "notebook" || item.itemType === "notebook"
|
||||
? notebooksListMode
|
||||
: notesListMode;
|
||||
const listMode = item.type === "notebook" ? notebooksListMode : notesListMode;
|
||||
const compactMode =
|
||||
(item.type === "notebook" ||
|
||||
item.itemType === "notebook" ||
|
||||
item.itemType === "note" ||
|
||||
item.type === "note") &&
|
||||
(item.type === "notebook" || item.type === "note") &&
|
||||
listMode === "compact";
|
||||
|
||||
if (item.id !== itemId.current) {
|
||||
itemId.current = item.id;
|
||||
}
|
||||
@@ -82,7 +77,7 @@ const SelectionWrapper = ({
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: compactMode ? 4 : 12,
|
||||
paddingVertical: compactMode ? 8 : 12,
|
||||
borderRadius: isSheet ? 10 : 0,
|
||||
marginBottom: isSheet ? 12 : undefined
|
||||
}}
|
||||
|
||||
@@ -23,7 +23,6 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
|
||||
export const SelectionIcon = ({ item }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -33,17 +32,6 @@ export const SelectionIcon = ({ item }) => {
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const [selected, setSelected] = useState(false);
|
||||
const notebooksListMode = useSettingStore(
|
||||
(state) => state.settings.notebooksListMode
|
||||
);
|
||||
const notesListMode = useSettingStore(
|
||||
(state) => state.settings.notesListMode
|
||||
);
|
||||
const listMode = item.type === "notebook" ? notebooksListMode : notesListMode;
|
||||
|
||||
const compactMode =
|
||||
(item.type === "notebook" || item.type === "note") &&
|
||||
listMode === "compact";
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
@@ -66,8 +54,8 @@ export const SelectionIcon = ({ item }) => {
|
||||
return selectionMode ? (
|
||||
<View
|
||||
style={{
|
||||
width: compactMode ? 30 : 40,
|
||||
height: compactMode ? 30 : 40,
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 10,
|
||||
@@ -79,7 +67,7 @@ export const SelectionIcon = ({ item }) => {
|
||||
>
|
||||
{selected ? (
|
||||
<Icon
|
||||
size={compactMode ? SIZE.xl - 2 : SIZE.xl}
|
||||
size={SIZE.xl}
|
||||
color={selected ? colors.accent : colors.icon}
|
||||
name={"check"}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { dHeight } from "../../utils";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { getFormattedDate, sleep } from "../../utils/time";
|
||||
import { sleep, timeConverter } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import DialogButtons from "../dialog/dialog-buttons";
|
||||
import DialogContainer from "../dialog/dialog-container";
|
||||
@@ -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, {
|
||||
@@ -166,7 +172,7 @@ const MergeConflicts = () => {
|
||||
{isCurrent ? "(This Device)" : "(Incoming)"}
|
||||
</Text>
|
||||
{"\n"}
|
||||
{getFormattedDate(contentToKeep?.dateEdited)}
|
||||
{timeConverter(contentToKeep?.dateEdited)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { presentSheet } from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { getFormattedDate, timeSince } from "../../utils/time";
|
||||
import { timeConverter, timeSince } from "../../utils/time";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
@@ -63,15 +63,15 @@ export default function NoteHistory({ note, fwdRef }) {
|
||||
}, []);
|
||||
|
||||
const getDate = (start, end) => {
|
||||
let _start_date = getFormattedDate(start, "date");
|
||||
let _end_date = getFormattedDate(end + 60000, "date");
|
||||
|
||||
let _start_time = getFormattedDate(start, "time");
|
||||
let _end_time = getFormattedDate(end + 60000, "time");
|
||||
|
||||
return `${_start_date} ${_start_time} - ${
|
||||
_end_date === _start_date ? " " : _end_date + " "
|
||||
}${_end_time}`;
|
||||
let _start = timeConverter(start);
|
||||
let _end = timeConverter(end + 60000);
|
||||
if (_start === _end) return _start;
|
||||
let final = _end.lastIndexOf(",");
|
||||
let part = _end.slice(0, final + 1);
|
||||
if (_start.includes(part)) {
|
||||
return _start + " —" + _end.replace(part, "");
|
||||
}
|
||||
return _start + " — " + _end;
|
||||
};
|
||||
|
||||
const renderItem = useCallback(
|
||||
|
||||
@@ -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,13 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { ScrollView } from "react-native";
|
||||
import { FeatureBlock } from "./feature";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
export const CompactFeatures = ({
|
||||
vertical,
|
||||
features = [],
|
||||
maxHeight = 600,
|
||||
maxHeight = 500,
|
||||
scrollRef
|
||||
}) => {
|
||||
let data = vertical
|
||||
@@ -80,11 +80,14 @@ export const CompactFeatures = ({
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal={!vertical}
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={() => {
|
||||
scrollRef?.current?.handleChildScrollEnd();
|
||||
}}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxHeight: maxHeight,
|
||||
paddingHorizontal: 12
|
||||
maxHeight: maxHeight
|
||||
}}
|
||||
>
|
||||
{data.map((item) => (
|
||||
|
||||
@@ -143,7 +143,7 @@ export const Expiring = () => {
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog, promo);
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.icon,
|
||||
|
||||
@@ -41,19 +41,18 @@ export const FeatureBlock = ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 10,
|
||||
backgroundColor: colors.nav,
|
||||
borderRadius: 10,
|
||||
paddingVertical: 12
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Icon color={colors.accent} name="check" size={SIZE.lg} />
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginLeft: 5,
|
||||
flexShrink: 1
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{content}
|
||||
</Paragraph>
|
||||
|
||||
@@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { editorState } from "../../screens/editor/tiptap/utils";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -31,7 +31,6 @@ import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { getElevation } from "../../utils";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseSheet,
|
||||
eOpenPremiumDialog,
|
||||
eShowGetPremium
|
||||
} from "../../utils/events";
|
||||
@@ -40,12 +39,12 @@ import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [msg, setMsg] = useState(null);
|
||||
const timer = useRef();
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const open = useCallback(
|
||||
(event) => {
|
||||
@@ -73,6 +72,7 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowGetPremium, open);
|
||||
return () => {
|
||||
clearTimeout(timer.current);
|
||||
eUnSubscribeEvent(eShowGetPremium, open);
|
||||
};
|
||||
}, [open]);
|
||||
@@ -80,7 +80,9 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
const onPress = async () => {
|
||||
open(null);
|
||||
eSendEvent(eCloseActionSheet);
|
||||
eSendEvent(eCloseSheet);
|
||||
if (editorState().isFocused) {
|
||||
//tiny.call(EditorWebView, tiny.blur);
|
||||
}
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
};
|
||||
@@ -100,13 +102,9 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
top: offset + keyboard.keyboardHeight,
|
||||
top: offset,
|
||||
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setMsg(null);
|
||||
clearTimeout(timer.current);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -232,7 +232,8 @@ export const PricingPlans = ({
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -397,7 +398,8 @@ export const PricingPlans = ({
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 30,
|
||||
marginBottom: 10
|
||||
marginBottom: 10,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
{Platform.OS !== "ios" &&
|
||||
|
||||
@@ -43,7 +43,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);
|
||||
};
|
||||
|
||||
@@ -83,7 +89,6 @@ export const ColorTags = ({ item }) => {
|
||||
flexWrap: "wrap",
|
||||
flexGrow: isTablet ? undefined : 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingRight: 0,
|
||||
alignItems: "center",
|
||||
justifyContent: isTablet ? "center" : "space-between"
|
||||
}}
|
||||
|
||||
@@ -21,7 +21,7 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { getFormattedDate } from "../../utils/time";
|
||||
import { timeConverter } from "../../utils/time";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const DateMeta = ({ item }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -62,11 +62,11 @@ export const DateMeta = ({ item }) => {
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
|
||||
{timeConverter(item[key])}
|
||||
</Paragraph>
|
||||
</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";
|
||||
@@ -36,6 +37,7 @@ 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 (
|
||||
@@ -51,11 +53,20 @@ const Line = ({ top = 6, bottom = 6 }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
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 onScrollEnd = () => {
|
||||
getRef().current?.handleChildScrollEnd();
|
||||
};
|
||||
|
||||
if (!item || !item.id) {
|
||||
return (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
@@ -64,92 +75,89 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
);
|
||||
}
|
||||
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>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 5,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{item.type === "tag" && !isColor ? (
|
||||
<Heading size={SIZE.xl} color={colors.accent}>
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{alias}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 5,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{item.type === "tag" && !isColor ? (
|
||||
<Heading size={SIZE.xl} color={colors.accent}>
|
||||
#
|
||||
</Heading>
|
||||
|
||||
{item.type === "note" ? (
|
||||
<TagStrip close={close} item={item} />
|
||||
) : null}
|
||||
|
||||
{item.type === "reminder" ? (
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<Line top={12} />
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
{alias}
|
||||
</Heading>
|
||||
|
||||
{item.type === "note" ? <TagStrip close={close} item={item} /> : null}
|
||||
|
||||
{item.type === "reminder" ? (
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
fontSize={SIZE.xs + 1}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<Line top={12} />
|
||||
|
||||
<DateMeta item={item} />
|
||||
<Line bottom={0} />
|
||||
{item.type === "note" ? <Tags close={close} item={item} /> : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{item.notebooks ? <Notebooks note={item} close={close} /> : null}
|
||||
</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" />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -179,7 +187,6 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
"lock-unlock",
|
||||
"trash",
|
||||
"remove-from-topic",
|
||||
"remove-from-notebook",
|
||||
"history",
|
||||
"read-only",
|
||||
"reminders",
|
||||
@@ -191,25 +198,13 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
break;
|
||||
case "notebook":
|
||||
props[0] = db.notebooks.notebook(item.id).data;
|
||||
props.push([
|
||||
"edit-notebook",
|
||||
"pin",
|
||||
"add-shortcut",
|
||||
"trash",
|
||||
"default-notebook"
|
||||
]);
|
||||
props.push(["edit-notebook", "pin", "add-shortcut", "trash"]);
|
||||
break;
|
||||
case "topic":
|
||||
props[0] = db.notebooks
|
||||
.notebook(item.notebookId)
|
||||
.topics.topic(item.id)._topic;
|
||||
props.push([
|
||||
"move-notes",
|
||||
"edit-topic",
|
||||
"add-shortcut",
|
||||
"trash",
|
||||
"default-topic"
|
||||
]);
|
||||
props.push(["move-notes", "edit-topic", "add-shortcut", "trash"]);
|
||||
break;
|
||||
case "tag":
|
||||
props[0] = db.tags.tag(item.id);
|
||||
@@ -229,7 +224,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";
|
||||
@@ -84,9 +85,8 @@ export const Items = ({ item, buttons, close }) => {
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderColumnItem = (item) => (
|
||||
const renderColumnItem = ({ item }) => (
|
||||
<Button
|
||||
key={item.name + item.title}
|
||||
buttonType={{
|
||||
text: item.on
|
||||
? colors.accent
|
||||
@@ -108,19 +108,18 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
const renderTopBarItem = (item, index) => {
|
||||
const isLast = index === topBarItems.length;
|
||||
const renderTopBarItem = ({ item }) => {
|
||||
return (
|
||||
<PressableButton
|
||||
onPress={item.func}
|
||||
key={item.id}
|
||||
testID={"icon-" + item.id}
|
||||
activeOpacity={1}
|
||||
customStyle={{
|
||||
alignSelf: "flex-start",
|
||||
marginRight: isLast ? 0 : 10,
|
||||
paddingHorizontal: 0,
|
||||
width: topBarItemWidth
|
||||
alignItems: "center",
|
||||
width: topBarItemWidth,
|
||||
marginBottom: 10,
|
||||
marginRight: 10,
|
||||
backgroundColor: "transparent"
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
@@ -150,7 +149,11 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
|
||||
<Paragraph
|
||||
size={SIZE.xxs + 1}
|
||||
style={{ textAlign: "center" }}
|
||||
textBreakStrategy="simple"
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
@@ -166,7 +169,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
"lock-unlock",
|
||||
"publish"
|
||||
];
|
||||
const topBarItems = data.filter(
|
||||
const bottomBarItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) > -1
|
||||
);
|
||||
|
||||
@@ -175,19 +178,21 @@ export const Items = ({ item, buttons, close }) => {
|
||||
);
|
||||
|
||||
const topBarItemWidth =
|
||||
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
|
||||
(width - (bottomBarItems.length * 10 + 14)) / bottomBarItems.length;
|
||||
|
||||
return item.type === "note" ? (
|
||||
<>
|
||||
<ScrollView
|
||||
<FlatList
|
||||
data={bottomBarItems}
|
||||
keyExtractor={(item) => item.title}
|
||||
horizontal
|
||||
disableVirtualization={true}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
{topBarItems.map(renderTopBarItem)}
|
||||
</ScrollView>
|
||||
renderItem={renderTopBarItem}
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={bottomGridItems}
|
||||
@@ -197,8 +202,10 @@ export const Items = ({ item, buttons, close }) => {
|
||||
disableVirtualization={true}
|
||||
style={{
|
||||
marginTop: item.type !== "note" ? 10 : 0,
|
||||
paddingTop: 10,
|
||||
marginLeft: 6
|
||||
paddingTop: 10
|
||||
}}
|
||||
columnWrapperStyle={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignSelf: "center",
|
||||
@@ -209,6 +216,11 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<View data={data}>{data.map(renderColumnItem)}</View>
|
||||
<FlatList
|
||||
data={data}
|
||||
keyExtractor={(item) => item.title}
|
||||
renderItem={renderColumnItem}
|
||||
disableVirtualization={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,33 +23,20 @@ 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 {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
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 Heading from "../ui/typography/heading";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
|
||||
export default function Notebooks({ note, close, full }) {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const notebooks = useNotebookStore((state) => state.notebooks);
|
||||
function getNotebooks(item) {
|
||||
if (!item.notebooks || item.notebooks.length < 1) return [];
|
||||
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) || {})
|
||||
@@ -58,14 +45,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
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);
|
||||
}
|
||||
filteredNotebooks.push(notebook);
|
||||
}
|
||||
}
|
||||
return filteredNotebooks;
|
||||
@@ -83,6 +63,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
if (!item) return;
|
||||
TopicNotes.navigate(item, true);
|
||||
};
|
||||
|
||||
const renderItem = (item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
@@ -97,8 +78,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
borderWidth: full ? 0 : 1,
|
||||
borderColor: colors.nav,
|
||||
borderRadius: 10,
|
||||
backgroundColor: full ? "transparent" : colors.nav,
|
||||
minHeight: 42
|
||||
backgroundColor: full ? "transparent" : colors.nav
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -117,7 +97,6 @@ export default function Notebooks({ note, close, full }) {
|
||||
size={SIZE.sm}
|
||||
onPress={() => {
|
||||
navigateNotebook(item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
@@ -140,7 +119,6 @@ export default function Notebooks({ note, close, full }) {
|
||||
key={topic.id}
|
||||
onPress={() => {
|
||||
navigateTopic(topic.id, item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
onLongPress={async () => {
|
||||
@@ -152,7 +130,13 @@ export default function Notebooks({ note, close, full }) {
|
||||
note
|
||||
);
|
||||
useNotebookStore.getState().setNotebooks();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
ToastEvent.show({
|
||||
heading: "Note removed from topic",
|
||||
context: "local",
|
||||
@@ -162,7 +146,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
title={topic.title}
|
||||
type="gray"
|
||||
height={30}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
icon="bookmark-outline"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
@@ -176,7 +160,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
</View>
|
||||
);
|
||||
|
||||
return noteNotebooks.length === 0 ? null : (
|
||||
return !note.notebooks || note.notebooks.length === 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -191,7 +175,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
{noteNotebooks.length > 1 && !full ? (
|
||||
<Button
|
||||
title={`See all linked notebooks`}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
marginRight: 12,
|
||||
|
||||
@@ -91,10 +91,10 @@ export const Synced = ({ item, close }) => {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
title="Learn more"
|
||||
height={30}
|
||||
type="grayAccent"
|
||||
type="transparent"
|
||||
/>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
@@ -21,10 +21,11 @@ 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 }) => {
|
||||
@@ -40,14 +41,14 @@ export const Tags = ({ item, close }) => {
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTagsSheet.present([item]);
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenTagsDialog, item);
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.accent
|
||||
@@ -57,7 +58,7 @@ export const Tags = ({ item, close }) => {
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
height={30}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
@@ -78,8 +79,8 @@ export const TagStrip = ({ item, close }) => {
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{item.tags.map((tag) =>
|
||||
tag ? <TagItem key={tag} tag={tag} close={close} /> : null
|
||||
{item.tags.map((item) =>
|
||||
item ? <TagItem key={item} tag={item} close={close} /> : null
|
||||
)}
|
||||
</View>
|
||||
) : null;
|
||||
@@ -101,13 +102,14 @@ const TagItem = ({ tag, close }) => {
|
||||
marginTop: 0,
|
||||
backgroundColor: "transparent"
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={"#" + tag}
|
||||
type="grayBg"
|
||||
height={20}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={style}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
|
||||
@@ -39,7 +39,7 @@ export const Topics = ({ item, close }) => {
|
||||
height={30}
|
||||
onPress={() => open(topic)}
|
||||
icon="bookmark-outline"
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
paddingHorizontal: 8,
|
||||
|
||||
@@ -21,22 +21,21 @@ 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";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
|
||||
export const SelectionHeader = React.memo(() => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -65,7 +64,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();
|
||||
}
|
||||
};
|
||||
@@ -77,7 +82,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({
|
||||
@@ -104,7 +118,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();
|
||||
}
|
||||
},
|
||||
@@ -178,7 +201,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md} color={colors.accent}>
|
||||
{selectedItemsList.length}
|
||||
{selectedItemsList.length + " Selected"}
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
@@ -210,25 +233,11 @@ export const SelectionHeader = React.memo(() => {
|
||||
screen === "Notebook" ||
|
||||
screen === "Reminders" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await sleep(100);
|
||||
ManageTagsSheet.present(selectedItemsList);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.pri}
|
||||
tooltipText="Manage tags"
|
||||
tooltipPosition={4}
|
||||
name="pound"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
//setSelectionMode(false);
|
||||
await sleep(100);
|
||||
MoveNoteSheet.present();
|
||||
eSendEvent(eOpenMoveNoteDialog);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
@@ -239,7 +248,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
name="plus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
@@ -256,40 +264,36 @@ 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"
|
||||
}`}
|
||||
tooltipText="Remove from topic"
|
||||
tooltipPosition={4}
|
||||
testID="select-minus"
|
||||
color={colors.pri}
|
||||
|
||||
@@ -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(0);
|
||||
}
|
||||
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,30 +197,32 @@ 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={40}
|
||||
height={45}
|
||||
width={250}
|
||||
style={{
|
||||
marginBottom: 25
|
||||
borderRadius: 100
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
) : 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
|
||||
}}
|
||||
@@ -220,7 +231,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{data?.learnMore ? (
|
||||
{dialogData?.learnMore ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
@@ -228,7 +239,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
onPress={data.learnMorePress}
|
||||
onPress={dialogData.learnMorePress}
|
||||
color={colors.icon}
|
||||
>
|
||||
<Icon
|
||||
@@ -236,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,165 +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);
|
||||
|
||||
|
||||
@@ -20,9 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export const SelectionContext = createContext({
|
||||
enabled: false,
|
||||
selected: [],
|
||||
toggleSelection: (item) => null,
|
||||
deselect: (item) => null,
|
||||
select: (item) => null,
|
||||
isSelected: (item) => null,
|
||||
setMultiSelect: () => null,
|
||||
deselectAll: () => null
|
||||
});
|
||||
export const SelectionProvider = SelectionContext.Provider;
|
||||
|
||||
@@ -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 { FlatList } from "react-native-actions-sheet";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { db } from "../../../common/database";
|
||||
import { ListHeaderInputItem } from "./list-header-item.js";
|
||||
|
||||
@@ -27,7 +27,6 @@ export const FilteredList = ({
|
||||
itemType,
|
||||
onAddItem,
|
||||
hasHeaderSearch,
|
||||
listRef,
|
||||
...restProps
|
||||
}) => {
|
||||
const [filtered, setFiltered] = useState(data);
|
||||
@@ -57,7 +56,6 @@ export const FilteredList = ({
|
||||
<FlatList
|
||||
{...restProps}
|
||||
data={filtered}
|
||||
ref={listRef}
|
||||
ListHeaderComponent={
|
||||
hasHeaderSearch ? (
|
||||
<ListHeaderInputItem
|
||||
@@ -71,6 +69,7 @@ export const FilteredList = ({
|
||||
}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
nestedScrollEnabled
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,45 +17,90 @@ 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,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
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 { eOpenMoveNoteDialog } from "../../../utils/events";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
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 { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
const actionSheetRef = createRef();
|
||||
const AddToNotebookSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
|
||||
function open(note) {
|
||||
setNote(note);
|
||||
setVisible(true);
|
||||
actionSheetRef.current?.setModalVisible(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenMoveNoteDialog, open);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenMoveNoteDialog, open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const _onClose = () => {
|
||||
setVisible(false);
|
||||
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 [multiSelect, setMultiSelect] = useState(false);
|
||||
const notebooks = useNotebookStore((state) =>
|
||||
state.notebooks.filter((n) => n?.type === "notebook")
|
||||
);
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const [edited, setEdited] = useState(false);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
|
||||
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
const [itemState, setItemState] = useState({});
|
||||
|
||||
const onAddNotebook = async (title) => {
|
||||
if (!title || title.trim().length === 0) {
|
||||
@@ -114,13 +159,13 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
(item) => {
|
||||
switch (item.type) {
|
||||
case "notebook": {
|
||||
const notes = db.relations.from(item, "note");
|
||||
if (notes.length === 0) return 0;
|
||||
const noteIds = [];
|
||||
for (let topic of item.topics) {
|
||||
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
|
||||
}
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
notes.findIndex((note) => note.id === item.id) > -1
|
||||
? count++
|
||||
: undefined
|
||||
noteIds.indexOf(item.id) > -1 ? count++ : undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
@@ -139,55 +184,42 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
|
||||
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
|
||||
setItemState(() => {
|
||||
const itemState = {};
|
||||
const notebooks = db.notebooks.all;
|
||||
for (let notebook of notebooks) {
|
||||
itemState[notebook.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
|
||||
getSelectedNotesCountInItem(topic, selectedItemsList)
|
||||
: areAllSelectedItemsInAllTopics(notebook, selectedItemsList) &&
|
||||
getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
|
||||
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
: "deselected";
|
||||
if (itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(topic);
|
||||
} else {
|
||||
contextValue.deselect(topic);
|
||||
if (itemState[notebook.id] === "selected") {
|
||||
contextValue.select(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") {
|
||||
contextValue.select(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 1) {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
} else {
|
||||
useItemSelectionStore.getState().setMultiSelect(false);
|
||||
}
|
||||
useItemSelectionStore.getState().setItemState(itemState);
|
||||
return itemState;
|
||||
});
|
||||
},
|
||||
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
|
||||
);
|
||||
@@ -199,11 +231,11 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
};
|
||||
|
||||
function areAllSelectedItemsInNotebook(notebook, items) {
|
||||
const notes = db.relations.from(notebook, "note");
|
||||
if (notes.length === 0) return false;
|
||||
function areAllSelectedItemsInAllTopics(notebook, items) {
|
||||
return items.every((item) => {
|
||||
return notes.find((note) => note.id === item.id);
|
||||
return notebook.topics.every((topic) => {
|
||||
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -214,26 +246,51 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
|
||||
const updateItemState = useCallback(function (item, state) {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
useItemSelectionStore.getState().setItemState({
|
||||
...itemState,
|
||||
...mergeState
|
||||
setItemState((itemState) => {
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
const notebooks = db.notebooks.all;
|
||||
const notebook =
|
||||
item.type === "notebook"
|
||||
? item
|
||||
: notebooks.find((n) => n.id === item.notebookId);
|
||||
const intermediate = notebook.topics.some((topic) => {
|
||||
return topic.id === item.id
|
||||
? state === "selected"
|
||||
: itemState[topic.id] === "selected";
|
||||
});
|
||||
if (intermediate) mergeState[notebook.id] = "intermediate";
|
||||
const selected = notebook.topics.every((topic) => {
|
||||
return topic.id === item.id
|
||||
? state === "selected"
|
||||
: itemState[topic.id] === "selected";
|
||||
});
|
||||
if (selected) mergeState[notebook.id] = "selected";
|
||||
if (!selected && !intermediate) mergeState[notebook.id] = "deselected";
|
||||
|
||||
return {
|
||||
...itemState,
|
||||
...mergeState
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
enabled: multiSelect,
|
||||
toggleSelection: (item) => {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
if (itemState[item.id] === "selected") {
|
||||
updateItemState(item, "deselected");
|
||||
} else {
|
||||
updateItemState(item, "selected");
|
||||
}
|
||||
setItemState((itemState) => {
|
||||
if (itemState[item.id] === "selected") {
|
||||
updateItemState(item, "deselected");
|
||||
} else {
|
||||
updateItemState(item, "selected");
|
||||
}
|
||||
|
||||
return itemState;
|
||||
});
|
||||
},
|
||||
setMultiSelect: setMultiSelect,
|
||||
deselect: (item) => {
|
||||
updateItemState(item, "deselected");
|
||||
},
|
||||
@@ -244,7 +301,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
resetItemState(state);
|
||||
}
|
||||
}),
|
||||
[resetItemState, updateItemState]
|
||||
[multiSelect, resetItemState, updateItemState]
|
||||
);
|
||||
|
||||
const getItemFromId = (id) => {
|
||||
@@ -257,47 +314,39 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
};
|
||||
|
||||
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 (item.type === "notebook") continue;
|
||||
const noteIds = selectedItemsList.map((n) => n.id);
|
||||
if (itemState[id] === "selected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
await db.relations.add(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.addToNotebook(
|
||||
{
|
||||
topic: item.id,
|
||||
id: item.notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
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) {
|
||||
await db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
setNotebooks();
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
SearchService.updateAndSearch();
|
||||
useRelationStore.getState().update();
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
@@ -348,32 +397,34 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title="Reset selection"
|
||||
height={30}
|
||||
{multiSelect ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
type="grayAccent"
|
||||
onPress={() => {
|
||||
resetItemState();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
>
|
||||
<Button
|
||||
title="Reset selection"
|
||||
height={30}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
resetItemState();
|
||||
setMultiSelect(false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<SelectionProvider value={contextValue}>
|
||||
<FilteredList
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: dimensions.height * 0.85
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
notebooks.length > 0 ? null : (
|
||||
@@ -399,8 +450,12 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
item={item}
|
||||
key={item.id}
|
||||
index={index}
|
||||
hasNotes={getSelectedNotesCountInItem(item) > 0}
|
||||
sheetRef={actionSheetRef}
|
||||
intermediate={itemState[item.id] === "intermediate"}
|
||||
removed={
|
||||
itemState[item.id] === "deselected" &&
|
||||
getSelectedNotesCountInItem(item) > 0
|
||||
}
|
||||
isSelected={itemState[item.id] === "selected"}
|
||||
infoText={
|
||||
<>
|
||||
{item.topics.length === 1
|
||||
@@ -410,18 +465,25 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
getListItems={getItemsForItem}
|
||||
getSublistItemProps={(topic) => ({
|
||||
hasNotes: getSelectedNotesCountInItem(topic) > 0,
|
||||
selected: itemState[topic.id] === "selected",
|
||||
intermediate: itemState[topic.id] === "intermediate",
|
||||
isSelected: itemState[topic.id] === "selected",
|
||||
removed:
|
||||
itemState[topic.id] === "deselected" &&
|
||||
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);
|
||||
updateItemState(
|
||||
notebooks.find((n) => n.id === item.notebookId),
|
||||
"intermediate"
|
||||
);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
@@ -446,35 +508,22 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
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 }} />}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SelectionProvider>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MoveNoteSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
|
||||
enableGesturesInScrollView: false,
|
||||
noBottomPadding: true
|
||||
});
|
||||
};
|
||||
export default MoveNoteSheet;
|
||||
|
||||
@@ -17,10 +17,8 @@ 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 React, { 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";
|
||||
@@ -29,78 +27,13 @@ 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 = ({
|
||||
const _ListItem = ({
|
||||
item,
|
||||
index,
|
||||
icon,
|
||||
infoText,
|
||||
intermediate,
|
||||
hasSubList,
|
||||
onPress,
|
||||
onScrollEnd,
|
||||
@@ -110,49 +43,29 @@ export const ListItem = ({
|
||||
sublistItemType,
|
||||
onAddItem,
|
||||
getSublistItemProps,
|
||||
removed,
|
||||
isSelected,
|
||||
hasHeaderSearch,
|
||||
onAddSublistItem,
|
||||
hasNotes,
|
||||
onChange,
|
||||
sheetRef
|
||||
onAddSublistItem
|
||||
}) => {
|
||||
const { toggleSelection } = useSelectionContext();
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
|
||||
const { enabled, toggleSelection, setMultiSelect, select, deselect } =
|
||||
useSelectionContext();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
function selectItem() {
|
||||
const currentState = isSelected;
|
||||
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++;
|
||||
}
|
||||
if (item.type === "notebook") {
|
||||
item.topics.forEach((item) => {
|
||||
if (currentState) {
|
||||
deselect(item);
|
||||
} else {
|
||||
select(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
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={{
|
||||
@@ -164,12 +77,12 @@ export const ListItem = ({
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
if (hasSubList) return setExpanded(!expanded);
|
||||
if (multiSelect) return selectItem();
|
||||
if (enabled) return selectItem();
|
||||
onPress?.(item);
|
||||
}}
|
||||
type={type}
|
||||
onLongPress={() => {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
setMultiSelect(true);
|
||||
selectItem();
|
||||
}}
|
||||
customStyle={{
|
||||
@@ -194,12 +107,46 @@ export const ListItem = ({
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<SelectionIndicator
|
||||
hasNotes={hasNotes}
|
||||
onPress={onPress}
|
||||
item={item}
|
||||
onChange={onChange}
|
||||
selectItem={selectItem}
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={
|
||||
removed
|
||||
? colors.red
|
||||
: intermediate || isSelected
|
||||
? colors.accent
|
||||
: colors.icon
|
||||
}
|
||||
onPress={() => {
|
||||
if (item.type === "notebook") {
|
||||
setMultiSelect(true);
|
||||
}
|
||||
selectItem();
|
||||
if (enabled) return;
|
||||
onPress?.(item);
|
||||
}}
|
||||
testID={
|
||||
removed
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: intermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
removed
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: intermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
<View>
|
||||
{hasSubList && expanded ? (
|
||||
@@ -218,27 +165,14 @@ export const ListItem = ({
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{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}
|
||||
color={colors}
|
||||
size={SIZE.xl}
|
||||
onPress={() => {
|
||||
onAddSublistItem(item);
|
||||
@@ -271,7 +205,7 @@ export const ListItem = ({
|
||||
style={{
|
||||
width: "95%",
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 250
|
||||
maxHeight: 500
|
||||
}}
|
||||
itemType={sublistItemType}
|
||||
hasHeaderSearch={hasHeaderSearch}
|
||||
@@ -280,7 +214,6 @@ export const ListItem = ({
|
||||
item={item}
|
||||
{...getSublistItemProps(item)}
|
||||
index={index}
|
||||
onChange={onChangeSubItem}
|
||||
onScrollEnd={onScrollEnd}
|
||||
/>
|
||||
)}
|
||||
@@ -290,3 +223,11 @@ export const ListItem = ({
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListItem = React.memo(_ListItem, (prev, next) => {
|
||||
if (prev.selected === undefined) return false;
|
||||
if (prev.isSelected !== next.isSelected) return false;
|
||||
if (prev.selected !== next.selected) return false;
|
||||
if (prev.intermediate !== next.intermediate) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,6 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
|
||||
const ExportNotesSheet = ({ notes, update }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -73,7 +72,6 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
update({ disableClosing: false });
|
||||
setComplete(true);
|
||||
setExporting(false);
|
||||
requestInAppReview();
|
||||
};
|
||||
|
||||
const actions = [
|
||||
@@ -279,6 +277,7 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
});
|
||||
});
|
||||
}}
|
||||
height={50}
|
||||
/>
|
||||
<Button
|
||||
title="Share"
|
||||
@@ -302,6 +301,7 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
}).catch(console.log);
|
||||
}
|
||||
}}
|
||||
height={50}
|
||||
/>
|
||||
<Button
|
||||
title="Export in another format"
|
||||
@@ -317,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,33 +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 { ToastEvent, presentSheet } 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";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
const ManageTagsSheet = (props) => {
|
||||
import { useCallback } from "react";
|
||||
const ManageTagsSheet = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [notes, setNotes] = useState(props.notes || []);
|
||||
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, notes, 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];
|
||||
@@ -52,14 +70,13 @@ const ManageTagsSheet = (props) => {
|
||||
if (query) {
|
||||
_tags = _tags.filter((t) => t.title.startsWith(query));
|
||||
}
|
||||
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
|
||||
|
||||
if (!tagsMerged || !tagsMerged.length) {
|
||||
if (!note || !note.tags) {
|
||||
setTags(_tags);
|
||||
return;
|
||||
}
|
||||
let noteTags = [];
|
||||
for (let tag of tagsMerged) {
|
||||
for (let tag of note.tags) {
|
||||
let index = _tags.findIndex((t) => t.title === tag);
|
||||
if (index !== -1) {
|
||||
noteTags.push(_tags[index]);
|
||||
@@ -69,11 +86,28 @@ const ManageTagsSheet = (props) => {
|
||||
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
|
||||
let combinedTags = [...noteTags, ..._tags];
|
||||
setTags(combinedTags);
|
||||
}, [allTags, notes, query]);
|
||||
}, [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;
|
||||
@@ -87,23 +121,15 @@ const ManageTagsSheet = (props) => {
|
||||
}
|
||||
|
||||
let tag = _query;
|
||||
setNotes(
|
||||
notes.map((note) => ({
|
||||
...note,
|
||||
tags: note.tags ? [...note.tags, tag] : [tag]
|
||||
}))
|
||||
);
|
||||
|
||||
setNote({ ...note, tags: note.tags ? [...note.tags, tag] : [tag] });
|
||||
setQuery(null);
|
||||
inputRef.current?.setNativeProps({
|
||||
text: ""
|
||||
});
|
||||
try {
|
||||
for (let note of notes) {
|
||||
await db.notes.note(note.id).tag(tag);
|
||||
}
|
||||
await db.notes.note(note.id).tag(tag);
|
||||
useTagStore.getState().setTags();
|
||||
setNotes(notes.map((note) => db.notes.note(note.id).data));
|
||||
setNote(db.notes.note(note.id).data);
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Cannot add tag",
|
||||
@@ -113,168 +139,184 @@ 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}
|
||||
notes={notes}
|
||||
setNotes={setNotes}
|
||||
/>
|
||||
))}
|
||||
</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 = (notes) => {
|
||||
presentSheet({
|
||||
component: (ref) => {
|
||||
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default ManageTagsSheet;
|
||||
|
||||
const TagItem = ({ tag, notes, setNotes }) => {
|
||||
const TagItem = ({ tag, note, setNote }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const someNotesTagged = notes.some(
|
||||
(note) => note.tags?.indexOf(tag.title) !== -1
|
||||
);
|
||||
const allNotesTagged = notes.every(
|
||||
(note) => note.tags?.indexOf(tag.title) !== -1
|
||||
);
|
||||
|
||||
const onPress = async () => {
|
||||
for (let note of notes) {
|
||||
try {
|
||||
if (someNotesTagged) {
|
||||
await db.notes
|
||||
.note(note.id)
|
||||
.untag(note.tags[note.tags.indexOf(tag.title)]);
|
||||
} else {
|
||||
await db.notes.note(note.id).tag(tag.title);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let prevNote = { ...note };
|
||||
try {
|
||||
if (prevNote.tags.indexOf(tag.title) !== -1) {
|
||||
await db.notes
|
||||
.note(note.id)
|
||||
.untag(prevNote.tags[prevNote.tags.indexOf(tag.title)]);
|
||||
} else {
|
||||
await db.notes.note(note.id).tag(tag.title);
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
setNote(db.notes.note(note.id).data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
setNotes(notes.map((note) => db.notes.note(note.id).data));
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
}, 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "flex-start",
|
||||
height: 40
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
onPress={onPress}
|
||||
type="gray"
|
||||
type={
|
||||
note && note.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? "shade"
|
||||
: "grayBg"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={someNotesTagged || allNotesTagged ? colors.accent : colors.icon}
|
||||
testID={
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
<Heading
|
||||
size={SIZE.sm}
|
||||
color={
|
||||
note && note?.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? colors.accent
|
||||
: colors.pri
|
||||
}
|
||||
>
|
||||
{"#" + tag.title}
|
||||
</Heading>
|
||||
<Icon
|
||||
name={
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
note && note?.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? "minus"
|
||||
: "plus"
|
||||
}
|
||||
color={
|
||||
note && note?.tags.findIndex((t) => t === tag.title) !== -1
|
||||
? colors.accent
|
||||
: colors.accent
|
||||
}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
<Paragraph size={SIZE.sm}>{"#" + tag.alias}</Paragraph>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user