mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 11:08:47 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f691d55a7 |
@@ -14,8 +14,6 @@ runs:
|
||||
apps/mobile/package-lock.json
|
||||
apps/desktop/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
apps/monograph/package-lock.json
|
||||
apps/theme-builder/package-lock.json
|
||||
extensions/web-clipper/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
@@ -26,4 +24,3 @@ runs:
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
packages/intl/package-lock.json
|
||||
|
||||
11
.github/workflows/android.publish.internal.yml
vendored
11
.github/workflows/android.publish.internal.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
java-version: "11"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: beta
|
||||
track: alpha
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
|
||||
repository: streetwriters/notesnook
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
@@ -146,7 +146,6 @@ jobs:
|
||||
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
|
||||
${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
|
||||
2
.github/workflows/core.tests.yml
vendored
2
.github/workflows/core.tests.yml
vendored
@@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
50
.github/workflows/desktop.publish.yml
vendored
50
.github/workflows/desktop.publish.yml
vendored
@@ -33,19 +33,11 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
release-track:
|
||||
type: choice
|
||||
required: true
|
||||
default: stable
|
||||
description: "Select the release track"
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -57,30 +49,14 @@ jobs:
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate desktop build (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
- name: Generate desktop build
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Generate desktop build (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
run: BETA=true npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Build desktop bundle
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run bundle
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -95,8 +71,7 @@ jobs:
|
||||
|
||||
- name: Package desktop build
|
||||
run: |
|
||||
cp -r ./apps/web/build ./apps/desktop/
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/desktop/build/
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/web/build/
|
||||
echo "Build folder archived to ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip"
|
||||
|
||||
- name: Upload desktop build
|
||||
@@ -112,8 +87,7 @@ jobs:
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
|
||||
sudo apt-get install -y flatpak flatpak-builder pipx
|
||||
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
|
||||
cd /tmp/flatpak-builder-tools/node
|
||||
pipx install .
|
||||
@@ -199,7 +173,7 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
|
||||
yarn electron-builder --mac mas --universal -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build zip and dmg
|
||||
@@ -213,9 +187,9 @@ jobs:
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p always
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p never
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
@@ -277,7 +251,7 @@ jobs:
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
|
||||
yarn electron-builder --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
@@ -285,9 +259,9 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -p always
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -p never
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
@@ -347,8 +321,8 @@ jobs:
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --config=electron-builder.config.js --win --publish always
|
||||
yarn electron-builder --win --publish always
|
||||
} else {
|
||||
yarn electron-builder --config=electron-builder.config.js --win --publish never
|
||||
yarn electron-builder --win --publish never
|
||||
}
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
24
.github/workflows/desktop.tests.yml
vendored
24
.github/workflows/desktop.tests.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -69,11 +69,12 @@ jobs:
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
|
||||
yarn electron-builder --mac --dir --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
run: npm run test
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
@@ -113,11 +114,12 @@ jobs:
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
|
||||
yarn electron-builder --mac --dir --arm64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
run: npm run test
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
@@ -131,7 +133,7 @@ jobs:
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -163,11 +165,12 @@ jobs:
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
|
||||
yarn electron-builder --linux --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/linux-unpacked/notesnook xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
@@ -213,11 +216,12 @@ jobs:
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
run: |
|
||||
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
|
||||
2
.github/workflows/editor.tests.yml
vendored
2
.github/workflows/editor.tests.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/help.publish.yml
vendored
2
.github/workflows/help.publish.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
4
.github/workflows/ios.publish.yml
vendored
4
.github/workflows/ios.publish.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v2
|
||||
id: pods-cache
|
||||
with:
|
||||
path: apps/mobile/native/ios/Pods
|
||||
|
||||
2
.github/workflows/monograph.publish.yml
vendored
2
.github/workflows/monograph.publish.yml
vendored
@@ -14,7 +14,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/theme-builder.publish.yml
vendored
2
.github/workflows/theme-builder.publish.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/vericrypt.publish.yml
vendored
2
.github/workflows/vericrypt.publish.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/web.benchmarks.yml
vendored
2
.github/workflows/web.benchmarks.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
bench:
|
||||
name: Bench
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
32
.github/workflows/web.publish.yml
vendored
32
.github/workflows/web.publish.yml
vendored
@@ -1,21 +1,11 @@
|
||||
name: Publish @notesnook/web
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-track:
|
||||
type: choice
|
||||
required: true
|
||||
default: stable
|
||||
description: "Select the release track"
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -34,20 +24,8 @@ jobs:
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
- name: Generate build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Publish to Cloudflare Pages (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
working-directory: ./apps/web
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./build/
|
||||
|
||||
- name: Generate build (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
working-directory: ./apps/web
|
||||
run: npx --yes wrangler pages deploy --branch=beta --project-name=notesnook-app-beta ./build/
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/
|
||||
|
||||
4
.github/workflows/web.tests.yml
vendored
4
.github/workflows/web.tests.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
shard: [1, 2, 3, 4]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,4 +11,3 @@ nx-cloud.env
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
*.patch
|
||||
|
||||
4
apps/desktop/.gitignore
vendored
4
apps/desktop/.gitignore
vendored
@@ -2,6 +2,4 @@ node_modules
|
||||
build
|
||||
output
|
||||
dist
|
||||
_catalog
|
||||
test-results
|
||||
test-artifacts
|
||||
_catalog
|
||||
@@ -58,13 +58,13 @@ This will compile and run the app in production mode but it won't generate any p
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 -publish never
|
||||
npx electron-builder --mac dmg --arm64 --x64 -publish never
|
||||
|
||||
# For Linux (AppImage)
|
||||
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
|
||||
# For Windows
|
||||
npx electron-builder --config=electron-builder.config.js --win --publish never
|
||||
npx electron-builder --win --publish never
|
||||
```
|
||||
|
||||
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
|
||||
|
||||
@@ -1,246 +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 { test } from "vitest";
|
||||
import { harness } from "./utils.js";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { Page } from "playwright";
|
||||
import { gt, lt } from "semver";
|
||||
|
||||
test("update starts downloading if version is outdated", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async ({ page }) => {
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /updating/i })
|
||||
.waitFor({ state: "attached" });
|
||||
},
|
||||
{ version: "3.0.0" }
|
||||
);
|
||||
});
|
||||
|
||||
test("update is only shown if version is outdated and auto updates are disabled", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.waitFor({ state: "attached" });
|
||||
},
|
||||
{ version: "3.0.0" }
|
||||
);
|
||||
});
|
||||
|
||||
test("update to stable if it is newer", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
t.expect(gt(version, "3.0.0-beta.0")).toBe(true);
|
||||
},
|
||||
{ version: "3.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
|
||||
test("update is not available if it latest stable version is older", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
t.expect(
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.isHidden()
|
||||
).toBe(true);
|
||||
},
|
||||
{ version: "99.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
|
||||
test("downgrade to stable on switching to stable release track", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "stable"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
t.expect(lt(version, "99.0.0-beta.0")).toBe(true);
|
||||
},
|
||||
{ version: "99.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
|
||||
async function skipDialog(page: Page) {
|
||||
await page
|
||||
.waitForSelector(".ReactModal__Content", {
|
||||
timeout: 1000
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(async () => {
|
||||
const positiveButton = page.locator(
|
||||
"button[data-role='positive-button']"
|
||||
);
|
||||
const negativeButton = page.locator(
|
||||
"button[data-role='negative-button']"
|
||||
);
|
||||
if (await positiveButton.isVisible()) await positiveButton.click();
|
||||
else if (await negativeButton.isVisible()) await negativeButton.click();
|
||||
});
|
||||
}
|
||||
@@ -17,12 +17,16 @@ 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 { test } from "vitest";
|
||||
import { harness } from "./utils.js";
|
||||
import test from "node:test";
|
||||
import { launchApp } from "./utils.mjs";
|
||||
import assert from "assert";
|
||||
import slugify from "slugify";
|
||||
import path from "path";
|
||||
import { mkdir } from "fs/promises";
|
||||
|
||||
test("make sure app loads", async (t) => {
|
||||
await harness(t, async ({ page }) => {
|
||||
const { app, page } = await launchApp();
|
||||
try {
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
assert.ok(
|
||||
@@ -34,5 +38,16 @@ test("make sure app loads", async (t) => {
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
});
|
||||
} catch (e) {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(t.name)}-${process.platform}-${process.arch}-error.png`
|
||||
)
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
@@ -17,14 +17,22 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { db } from "../common/database";
|
||||
import createDBCollectionStore from "./create-db-collection-store";
|
||||
import { _electron as electron } from "playwright";
|
||||
|
||||
const { useStore: useMonographStore, useCollection: useMonographs } =
|
||||
createDBCollectionStore({
|
||||
getCollection: () =>
|
||||
db.monographs.all.grouped(db.settings.getGroupOptions("notes")),
|
||||
eagerlyFetchFirstBatch: true
|
||||
const executablePath = process.env.EXECUTABLE_PATH;
|
||||
|
||||
// process.platform === "linux"
|
||||
// ? "output/linux-unpacked/Notesnook"
|
||||
// : process.platform === "darwin"
|
||||
// ? "output/mac/Notesnook.app/Contents/MacOS/Notesnook"
|
||||
// : "output/win-unpacked/Notesnook.exe";
|
||||
|
||||
export async function launchApp() {
|
||||
const app = await electron.launch({
|
||||
executablePath
|
||||
});
|
||||
|
||||
export { useMonographStore, useMonographs };
|
||||
const page = await app.firstWindow();
|
||||
|
||||
return { app, page };
|
||||
}
|
||||
@@ -1,172 +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 { execSync } from "child_process";
|
||||
import { mkdir } from "fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "path";
|
||||
import { _electron as electron } from "playwright";
|
||||
import slugify from "slugify";
|
||||
import { TaskContext } from "vitest";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
|
||||
|
||||
interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
page: import("playwright").Page;
|
||||
configPath: string;
|
||||
relaunch: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface TestOptions {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export async function harness(
|
||||
t: TaskContext,
|
||||
cb: (ctx: AppContext) => Promise<void>,
|
||||
options?: TestOptions
|
||||
) {
|
||||
const ctx = await buildAndLaunchApp(options);
|
||||
|
||||
t.onTestFinished(async (result) => {
|
||||
if (result.state === "fail") {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await ctx.page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(t.task.name)}-${process.platform}-${
|
||||
process.arch
|
||||
}-error.png`
|
||||
)
|
||||
});
|
||||
}
|
||||
await ctx.app.close();
|
||||
});
|
||||
|
||||
await cb(ctx);
|
||||
}
|
||||
|
||||
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
|
||||
const productName = makeid(10);
|
||||
const executablePath = await buildApp({ ...options, productName });
|
||||
const { app, page, configPath } = await launchApp(executablePath);
|
||||
const ctx: AppContext = {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
relaunch: async () => {
|
||||
const { app, page, configPath } = await launchApp(executablePath);
|
||||
ctx.app = app;
|
||||
ctx.page = page;
|
||||
ctx.configPath = configPath;
|
||||
}
|
||||
};
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async function launchApp(executablePath: string) {
|
||||
const app = await electron.launch({
|
||||
executablePath,
|
||||
args: IS_DEBUG ? [] : ["--hidden"],
|
||||
env:
|
||||
process.platform === "linux"
|
||||
? {
|
||||
...(process.env as Record<string, string>),
|
||||
APPIMAGE: "true"
|
||||
}
|
||||
: (process.env as Record<string, string>)
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
|
||||
const userDataDirectory = await app.evaluate((a) => {
|
||||
return a.app.getPath("userData");
|
||||
});
|
||||
const configPath = path.join(userDataDirectory, "config.json");
|
||||
return {
|
||||
app,
|
||||
page,
|
||||
configPath
|
||||
};
|
||||
}
|
||||
|
||||
async function buildApp({
|
||||
version,
|
||||
productName
|
||||
}: {
|
||||
version?: string;
|
||||
productName: string;
|
||||
}) {
|
||||
const buildRoot = path.join("test-artifacts", `${productName}-build`);
|
||||
const output = path.join("test-artifacts", `${productName}-output`);
|
||||
execSync(`npm run release -- --root ${buildRoot} --skip-tsc-build`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore"
|
||||
});
|
||||
|
||||
const args = [
|
||||
`--config electron-builder.config.js`,
|
||||
`--c.extraMetadata.productName=${productName}`,
|
||||
"--publish=never"
|
||||
];
|
||||
if (version) args.push(`--c.extraMetadata.version=${version}`);
|
||||
|
||||
execSync(`npx electron-builder --dir --${process.arch} ${args.join(" ")}`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
NOTESNOOK_STAGING: "true",
|
||||
NN_BUILD_ROOT: buildRoot,
|
||||
NN_PRODUCT_NAME: productName,
|
||||
NN_APP_ID: `com.notesnook.test.${productName}`,
|
||||
NN_OUTPUT_DIR: output
|
||||
}
|
||||
});
|
||||
|
||||
return path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
output,
|
||||
process.platform === "linux"
|
||||
? process.arch === "arm64"
|
||||
? "linux-arm64-unpacked"
|
||||
: "linux-unpacked"
|
||||
: process.platform === "darwin"
|
||||
? process.arch === "arm64"
|
||||
? `mac-arm64/${productName}.app/Contents/MacOS/`
|
||||
: `mac/${productName}.app/Contents/MacOS/`
|
||||
: "win-unpacked",
|
||||
process.platform === "win32" ? `${productName}.exe` : productName
|
||||
);
|
||||
}
|
||||
|
||||
function makeid(length: number) {
|
||||
let result = "";
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result.toLowerCase();
|
||||
}
|
||||
Binary file not shown.
@@ -1,203 +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/>.
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const pkg = require("./package.json");
|
||||
|
||||
const buildRoot = process.env.NN_BUILD_ROOT || ".";
|
||||
const buildFiles = [
|
||||
`${buildRoot}/build/`,
|
||||
`!${buildRoot}/build/screenshots\${/*}`,
|
||||
`!${buildRoot}/build/banner.jpg`,
|
||||
`!${buildRoot}/build/*.ico`,
|
||||
`!${buildRoot}/build/*.png`
|
||||
];
|
||||
|
||||
const productName = process.env.NN_PRODUCT_NAME || "Notesnook";
|
||||
const appId = process.env.NN_APP_ID || "org.streetwriters.notesnook";
|
||||
const outputDir = process.env.NN_OUTPUT_DIR || "output";
|
||||
const linuxExecutableName = process.env.NN_PRODUCT_NAME
|
||||
? process.env.NN_PRODUCT_NAME.toLowerCase().replace(/\s+/g, "-")
|
||||
: "notesnook";
|
||||
const year = new Date().getFullYear();
|
||||
const isBeta = pkg.version.includes("-beta");
|
||||
|
||||
module.exports = {
|
||||
appId: appId,
|
||||
productName: productName,
|
||||
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
|
||||
artifactName: "notesnook_${os}_${arch}.${ext}",
|
||||
generateUpdatesFilesForAllChannels: true,
|
||||
asar: false,
|
||||
files: [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
...buildFiles,
|
||||
"!node_modules${/*}",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/lib",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/package.json",
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/bindings",
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/sqlite-better-trigram",
|
||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-linux-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-linux-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-darwin-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-windows-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-windows-${arch}"
|
||||
},
|
||||
"node_modules/sodium-native/index.js",
|
||||
"node_modules/sodium-native/package.json"
|
||||
],
|
||||
afterPack: "./scripts/removeLocales.js",
|
||||
mac: {
|
||||
bundleVersion: "240",
|
||||
minimumSystemVersion: "10.12.0",
|
||||
target: [
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["arm64", "x64"]
|
||||
},
|
||||
{
|
||||
target: "zip",
|
||||
arch: ["arm64", "x64"]
|
||||
}
|
||||
],
|
||||
category: "public.app-category.productivity",
|
||||
darkModeSupport: true,
|
||||
type: "distribution",
|
||||
hardenedRuntime: true,
|
||||
entitlements: "assets/entitlements.mac.plist",
|
||||
entitlementsInherit: "assets/entitlements.mac.plist",
|
||||
gatekeeperAssess: false,
|
||||
icon: "assets/icons/app.icns",
|
||||
notarize: true
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 130,
|
||||
y: 220
|
||||
},
|
||||
{
|
||||
x: 410,
|
||||
y: 220,
|
||||
type: "link",
|
||||
path: "/Applications"
|
||||
}
|
||||
],
|
||||
icon: "assets/icons/app.icns",
|
||||
title: "Install Notesnook"
|
||||
},
|
||||
mas: {
|
||||
entitlements: "assets/entitlements.mas.plist",
|
||||
entitlementsInherit: "assets/entitlements.mas.inherit.plist",
|
||||
entitlementsLoginHelper: "assets/entitlements.mas.loginhelper.plist",
|
||||
hardenedRuntime: true
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
target: "portable",
|
||||
arch: ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
signtoolOptions: {
|
||||
signingHashAlgorithms: ["sha256"],
|
||||
sign: "./scripts/sign.js"
|
||||
},
|
||||
icon: "assets/icons/app.ico"
|
||||
},
|
||||
portable: {
|
||||
artifactName: "notesnook_${os}_${arch}_portable.${ext}"
|
||||
},
|
||||
nsis: {
|
||||
oneClick: true,
|
||||
createDesktopShortcut: "always",
|
||||
deleteAppDataOnUninstall: true
|
||||
},
|
||||
linux: {
|
||||
target: [
|
||||
{
|
||||
target: "AppImage",
|
||||
arch: ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
target: "snap",
|
||||
arch: ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
category: "Office",
|
||||
icon: "assets/icons/app.icns",
|
||||
description: "Your private note taking space",
|
||||
executableName: linuxExecutableName,
|
||||
desktop: {
|
||||
actions: [
|
||||
{
|
||||
id: "new-note",
|
||||
name: "New note",
|
||||
args: "new note"
|
||||
},
|
||||
{
|
||||
id: "new-notebook",
|
||||
name: "New notebook",
|
||||
args: "new notebook"
|
||||
},
|
||||
{
|
||||
id: "new-reminder",
|
||||
name: "New reminder",
|
||||
args: "new reminder"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
snap: {
|
||||
autoStart: false,
|
||||
confinement: "strict",
|
||||
allowNativeWayland: true
|
||||
},
|
||||
extraResources: ["app-update.yml", "./assets/**"],
|
||||
extraMetadata: {
|
||||
main: path.join(buildRoot, "build", "electron.js")
|
||||
},
|
||||
directories: {
|
||||
buildResources: "assets",
|
||||
output: outputDir
|
||||
},
|
||||
publish: [
|
||||
{
|
||||
provider: "github",
|
||||
repo: "notesnook",
|
||||
owner: "streetwriters",
|
||||
channel: isBeta ? "beta" : "latest"
|
||||
}
|
||||
]
|
||||
};
|
||||
1140
apps/desktop/package-lock.json
generated
1140
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.1.0",
|
||||
"version": "3.0.22",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -25,18 +25,18 @@
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@lingui/core": "5.1.2",
|
||||
"@lingui/core": "4.11.4",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"better-sqlite3-multiple-ciphers": "11.5.0",
|
||||
"electron-trpc": "0.7.1",
|
||||
"electron-trpc": "0.6.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"icojs": "^0.19.4",
|
||||
"sqlite-better-trigram": "0.0.2",
|
||||
"sqlite-better-trigram": "^0.0.2",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "3.23.8"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
@@ -45,15 +45,15 @@
|
||||
"chokidar": "^4.0.1",
|
||||
"electron": "^31.7.4",
|
||||
"electron-builder": "^25.1.8",
|
||||
"esbuild": "0.21.5",
|
||||
"esbuild": "^0.24.0",
|
||||
"node-abi": "^3.68.0",
|
||||
"node-gyp-build": "^4.8.2",
|
||||
"playwright": "^1.48.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"slugify": "^1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^2.1.8"
|
||||
"vitest": "^2.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -63,14 +63,203 @@
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "node scripts/build.mjs",
|
||||
"build": "node ../../scripts/build.mjs",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package",
|
||||
"test": "vitest run"
|
||||
"test": "node --test --test-timeout 30000 --test-force-exit"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
"email": "support@streetwriters.co",
|
||||
"url": "https://streetwriters.co"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.streetwriters.notesnook",
|
||||
"productName": "Notesnook",
|
||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
"build/",
|
||||
"!build/screenshots${/*}",
|
||||
"!build/banner.jpg",
|
||||
"!build/*.ico",
|
||||
"!build/*.png",
|
||||
"!node_modules${/*}",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/lib",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/package.json",
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/bindings",
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/sqlite-better-trigram",
|
||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-linux-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-linux-${arch}"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-darwin-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-windows-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-windows-${arch}"
|
||||
},
|
||||
"node_modules/sodium-native/index.js",
|
||||
"node_modules/sodium-native/package.json"
|
||||
],
|
||||
"afterPack": "./scripts/removeLocales.js",
|
||||
"mac": {
|
||||
"bundleVersion": "240",
|
||||
"minimumSystemVersion": "10.12.0",
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "public.app-category.productivity",
|
||||
"darkModeSupport": true,
|
||||
"type": "distribution",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "assets/entitlements.mac.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||
"gatekeeperAssess": false,
|
||||
"icon": "assets/icons/app.icns",
|
||||
"notarize": true
|
||||
},
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
"x": 130,
|
||||
"y": 220
|
||||
},
|
||||
{
|
||||
"x": 410,
|
||||
"y": 220,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"icon": "assets/icons/app.icns",
|
||||
"title": "Install Notesnook"
|
||||
},
|
||||
"mas": {
|
||||
"entitlements": "assets/entitlements.mas.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mas.inherit.plist",
|
||||
"entitlementsLoginHelper": "assets/entitlements.mas.loginhelper.plist",
|
||||
"hardenedRuntime": true
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"signtoolOptions": {
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./scripts/sign.js"
|
||||
},
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "notesnook_${os}_${arch}_portable.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"createDesktopShortcut": "always",
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "snap",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "Office",
|
||||
"icon": "assets/icons/app.icns",
|
||||
"description": "Your private note taking space",
|
||||
"executableName": "notesnook",
|
||||
"desktop": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "new-note",
|
||||
"name": "New note",
|
||||
"args": "new note"
|
||||
},
|
||||
{
|
||||
"id": "new-notebook",
|
||||
"name": "New notebook",
|
||||
"args": "new notebook"
|
||||
},
|
||||
{
|
||||
"id": "new-reminder",
|
||||
"name": "New reminder",
|
||||
"args": "new reminder"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"snap": {
|
||||
"autoStart": false,
|
||||
"confinement": "strict",
|
||||
"allowNativeWayland": true
|
||||
},
|
||||
"extraResources": [
|
||||
"app-update.yml",
|
||||
"./assets/**"
|
||||
],
|
||||
"extraMetadata": {
|
||||
"main": "./build/electron.js"
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "assets",
|
||||
"output": "./output/"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"repo": "notesnook",
|
||||
"owner": "streetwriters"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,14 @@ import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const root = args.root || path.join(__dirname, "..");
|
||||
const skipTscBuild = args.skipTscBuild || false;
|
||||
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
|
||||
await fs.rm(path.join(root, "build"), { force: true, recursive: true });
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(
|
||||
"npx nx build:desktop @notesnook/web",
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
@@ -46,31 +44,25 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
// temporary until there's support for prebuilt binaries for linux ARM
|
||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), path.join(root, "build"), {
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`yarn run bundle:mas --outdir=${path.join(root, "build")}`);
|
||||
await exec(`yarn run bundle:mas`);
|
||||
} else {
|
||||
await exec(`yarn run bundle --outdir=${path.join(root, "build")}`);
|
||||
await exec(`yarn run bundle`);
|
||||
}
|
||||
|
||||
if (!skipTscBuild) {
|
||||
await exec(`yarn run build`);
|
||||
}
|
||||
await exec(`yarn run build`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(
|
||||
`yarn electron-builder --dir --${process.arch} --config=electron-builder.config.js`
|
||||
);
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
if (process.arch === "arm64")
|
||||
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["npm", "run", "start:desktop"],
|
||||
path.join(__dirname, "..", "..", "web"),
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak, isSnap } from "../utils";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
import { rm } from "fs/promises";
|
||||
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
||||
@@ -49,7 +49,6 @@ const NotificationOptions = z.object({
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
isSnap: t.procedure.query(() => isSnap()),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
|
||||
@@ -23,7 +23,6 @@ import { CancellationToken, autoUpdater } from "electron-updater";
|
||||
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
|
||||
import { z } from "zod";
|
||||
import { config } from "../utils/config";
|
||||
import { app } from "electron";
|
||||
|
||||
type UpdateInfo = { version: string };
|
||||
type Progress = { percent: number };
|
||||
@@ -34,7 +33,6 @@ let downloadTimeout: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
releaseTrack: t.procedure.query(() => config.releaseTrack),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
if (cancellationToken) return;
|
||||
@@ -69,13 +67,7 @@ export const updaterRouter = t.router({
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
config.automaticUpdates = enabled;
|
||||
}),
|
||||
changeReleaseTrack: t.procedure
|
||||
.input(z.object({ track: z.string() }))
|
||||
.mutation(({ input: { track } }) => {
|
||||
config.releaseTrack = track;
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
|
||||
onChecking: createSubscription("checking-for-update"),
|
||||
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
|
||||
"update-downloaded"
|
||||
|
||||
@@ -85,8 +85,6 @@ async function createWindow() {
|
||||
const mainWindowState = new WindowState({});
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: !cliOptions.hidden,
|
||||
paintWhenInitiallyHidden: cliOptions.hidden,
|
||||
skipTaskbar: cliOptions.hidden,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
@@ -103,10 +101,7 @@ async function createWindow() {
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle:
|
||||
process.platform === "win32" || process.platform === "darwin"
|
||||
? "hidden"
|
||||
: "default",
|
||||
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
@@ -147,11 +142,6 @@ async function createWindow() {
|
||||
await AssetManager.loadIcons();
|
||||
setupDesktopIntegration(config.desktopSettings);
|
||||
|
||||
mainWindow.webContents.session.setPermissionRequestHandler(
|
||||
(webContents, permission, callback) => {
|
||||
callback(permission === "geolocation" ? false : true);
|
||||
}
|
||||
);
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
|
||||
@@ -20,22 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { config } from "./config";
|
||||
|
||||
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
|
||||
? "beta"
|
||||
: "latest";
|
||||
async function configureAutoUpdater() {
|
||||
const releaseTrack =
|
||||
config.releaseTrack === "stable" ? "latest" : config.releaseTrack;
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${releaseTrack}`,
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
|
||||
useMultipleRangeRequest: false,
|
||||
channel: releaseTrack
|
||||
channel: CHANNEL
|
||||
});
|
||||
|
||||
autoUpdater.autoDownload = config.automaticUpdates;
|
||||
autoUpdater.allowDowngrade =
|
||||
// only allow downgrade if the current version is a prerelease
|
||||
// and the user has changed the release track to stable
|
||||
config.releaseTrack === "stable" &&
|
||||
autoUpdater.currentVersion.prerelease.length > 0;
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
|
||||
@@ -20,7 +20,6 @@ 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";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
@@ -47,9 +46,6 @@ export const config = {
|
||||
automaticUpdates: true,
|
||||
proxyRules: "",
|
||||
customDns: true,
|
||||
releaseTrack: autoUpdater.currentVersion.raw.includes("-beta")
|
||||
? "beta"
|
||||
: "stable",
|
||||
|
||||
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
|
||||
windowControlsIconColor:
|
||||
|
||||
@@ -29,7 +29,3 @@ export function isDevelopment() {
|
||||
export function isFlatpak() {
|
||||
return existsSync("/.flatpak-info");
|
||||
}
|
||||
|
||||
export function isSnap() {
|
||||
return process.env.SNAP !== undefined;
|
||||
}
|
||||
|
||||
@@ -1,40 +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 { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000,
|
||||
sequence: {
|
||||
concurrent: true,
|
||||
shuffle: true
|
||||
},
|
||||
dir: "./__tests__/",
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/cypress/**",
|
||||
"**/.{idea,git,cache,output,temp}/**",
|
||||
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
|
||||
"**/test-results/**",
|
||||
"**/test-artifacts/**"
|
||||
]
|
||||
}
|
||||
});
|
||||
@@ -19,30 +19,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import {
|
||||
ScopedThemeProvider,
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { Appearance, I18nManager, StatusBar } from "react-native";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import DialogProvider from "./components/dialog-provider";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import { Toast } from "./components/toast";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { NotePreviewConfigure } from "./screens/note-preview-configure";
|
||||
import { RootNavigation } from "./navigation/navigation-stack";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import AppLocked from "./components/app-lock";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
@@ -52,13 +47,11 @@ if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
RNBootSplash.hide();
|
||||
const App = (props: { configureMode: "note-preview" }) => {
|
||||
const App = () => {
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
useEffect(() => {
|
||||
changeSystemBarColors();
|
||||
SettingsService.onFirstLaunch();
|
||||
setTimeout(async () => {
|
||||
await Notifications.get();
|
||||
@@ -68,29 +61,39 @@ const App = (props: { configureMode: "note-preview" }) => {
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: -1
|
||||
}}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<SafeAreaProvider>
|
||||
<GlobalSafeAreaProvider />
|
||||
</SafeAreaProvider>
|
||||
</View>
|
||||
|
||||
<GestureHandlerRootView
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<GlobalSafeAreaProvider />
|
||||
{props.configureMode === "note-preview" ? (
|
||||
<NotePreviewConfigure />
|
||||
) : (
|
||||
<RootNavigation />
|
||||
)}
|
||||
<ScopedThemeProvider value="dialog">
|
||||
<Toast />
|
||||
</ScopedThemeProvider>
|
||||
<DialogProvider />
|
||||
<AppLocked />
|
||||
<ApplicationHolder />
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
<AppLockedOverlay />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,8 +103,8 @@ let currTheme =
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
return function AppWithThemeProvider(props: any) {
|
||||
export const withTheme = (Element: () => JSX.Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
state.darkTheme,
|
||||
@@ -120,40 +123,28 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
}, 1000);
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
useThemeStore.getState().setColorScheme(colorScheme);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Element {...props} />
|
||||
<Element />
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
385
apps/mobile/app/common/database/encryption.js
Normal file
385
apps/mobile/app/common/database/encryption.js
Normal file
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
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 Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
const APPLOCK_CIPHER = "applockCipher";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||
const key = getDatabaseKey();
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
try {
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
const hasKey = await Keychain.hasInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
if (hasKey) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = credentials.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
DatabaseLogger.info("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
NOTESNOOK_DB_KEY_SALT
|
||||
);
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
DB_KEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
}
|
||||
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||
}
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.log("getCryptoKey", e);
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey(_name) {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(password, email) {
|
||||
let result = await Sodium.hashPassword(password, email);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function generateCryptoKey(password, salt) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(password, salt || null);
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
let _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
encryptionAlgorithm: enc,
|
||||
kdfAlgorithm: kdf,
|
||||
compressionAlgorithm: compressionAlg,
|
||||
isCompress: compressed === "1",
|
||||
base64_variant: base64variant
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let message = {
|
||||
type: "plain",
|
||||
data: data
|
||||
};
|
||||
let result = await Sodium.encrypt(password, message);
|
||||
|
||||
return {
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let results = await Sodium.encryptMulti(
|
||||
password,
|
||||
data.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
@@ -1,473 +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 Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
|
||||
import { SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
const APPLOCK_CIPHER = "applockCipher";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
//@ts-ignore
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
|
||||
const key = (await getDatabaseKey()) as string;
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = (await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
)) as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = (await encrypt(
|
||||
appLockCredentials,
|
||||
generatePassword()
|
||||
)) as Cipher;
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY: string | undefined;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword?: string) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
if (hasKey) {
|
||||
const credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = (credentials as Keychain.UserCredentials).password;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
DatabaseLogger.info("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
NOTESNOOK_DB_KEY_SALT
|
||||
);
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
DB_KEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook"
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher: Cipher = (await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
)) as Cipher;
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
}
|
||||
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
throw new Error(
|
||||
`Failed to get database key, ${await Keychain.hasInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return DB_KEY;
|
||||
}
|
||||
|
||||
export async function deriveCryptoKeyFallback(data: SerializedKey) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error(
|
||||
"Invalid password and salt provided to deriveCryptoKeyFallback"
|
||||
);
|
||||
|
||||
const credentials = await Sodium.deriveKeyFallback?.(
|
||||
data.password,
|
||||
data.salt
|
||||
);
|
||||
|
||||
if (!credentials) return;
|
||||
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key fallback stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data: SerializedKey) {
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error("Invalid password and salt provided to deriveCryptoKey");
|
||||
|
||||
const credentials = (await Sodium.deriveKey(
|
||||
data.password,
|
||||
data.salt
|
||||
)) as Password;
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey() {
|
||||
try {
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey() {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length: number) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(
|
||||
password: string,
|
||||
email: string,
|
||||
options?: { usesFallback?: boolean }
|
||||
) {
|
||||
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
|
||||
|
||||
if (options?.usesFallback && Platform.OS !== "ios") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
options?.usesFallback
|
||||
? await Sodium.hashPasswordFallback?.(password, email)
|
||||
: await Sodium.hashPassword(password, email)
|
||||
) as string;
|
||||
}
|
||||
|
||||
export async function generateCryptoKey(password: string, salt?: string) {
|
||||
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export async function generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey> {
|
||||
return Sodium.deriveKeyFallback?.(
|
||||
password,
|
||||
salt as string
|
||||
) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant: number) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
|
||||
const _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decrypt(key, _data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decrypt(fallbackKey, _data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(
|
||||
password: Password,
|
||||
data: Cipher<"base64">[]
|
||||
) {
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decryptMulti(key, data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt as string
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decryptMulti(fallbackKey, data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg: string) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
encryptionAlgorithm: enc,
|
||||
kdfAlgorithm: kdf,
|
||||
compressionAlgorithm: compressionAlg,
|
||||
isCompress: compressed === "1",
|
||||
base64_variant: base64variant
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password: SerializedKey, plainText: string) {
|
||||
const result = await Sodium.encrypt<"base64">(password, {
|
||||
type: "plain",
|
||||
data: plainText
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(
|
||||
password: SerializedKey,
|
||||
plainText: string[]
|
||||
) {
|
||||
const results = await Sodium.encryptMulti<"base64">(
|
||||
password,
|
||||
plainText.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
@@ -16,50 +16,48 @@ 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 "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger, ICompressor } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { logger as dbLogger } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import SettingsService from "../../services/settings";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { FileStorage } from "../filesystem";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import "./logger";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { Storage } from "./storage";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export async function setupDatabase(password?: string) {
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
database.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
MONOGRAPH_HOST: "https://monogr.ph",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: (Platform.OS === "ios"
|
||||
? EventSource
|
||||
: AndroidEventSource) as any,
|
||||
fs: FileStorage,
|
||||
compressor: async () =>
|
||||
({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
} as ICompressor),
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: () => ({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}),
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
@@ -26,7 +26,7 @@ import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location?: string };
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
|
||||
export class RNSqliteDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
|
||||
@@ -16,52 +16,58 @@ 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 { IStorage } from "@notesnook/core";
|
||||
import { MMKVInstance } from "react-native-mmkv-storage";
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
deriveCryptoKeyFallback,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
generateCryptoKeyFallback
|
||||
removeCryptoKey
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
export class KV {
|
||||
storage: MMKVInstance;
|
||||
constructor(storage: MMKVInstance) {
|
||||
/**
|
||||
* @type {typeof MMKV}
|
||||
*/
|
||||
storage = null;
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
async read<T>(key: string, isArray?: boolean) {
|
||||
if (!key) return undefined;
|
||||
const data = this.storage.getString(key);
|
||||
if (!data) return undefined;
|
||||
async read(key) {
|
||||
if (!key) return null;
|
||||
let data = this.storage.getString(key);
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data) as T;
|
||||
let parse = JSON.parse(data);
|
||||
return parse;
|
||||
} catch (e) {
|
||||
return data as T;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
async write<T>(key: string, data: T) {
|
||||
async write(key, data) {
|
||||
this.storage.setString(
|
||||
key,
|
||||
typeof data === "string" ? data : JSON.stringify(data)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async readMulti<T>(keys: string[]) {
|
||||
async readMulti(keys) {
|
||||
if (keys.length <= 0) {
|
||||
return [];
|
||||
} else {
|
||||
try {
|
||||
const data = await this.storage.getMultipleItemsAsync<any>(
|
||||
let data = await this.storage.getMultipleItemsAsync(
|
||||
keys.slice(),
|
||||
"string"
|
||||
);
|
||||
@@ -73,24 +79,24 @@ export class KV {
|
||||
obj = value;
|
||||
}
|
||||
return [key, obj];
|
||||
}) as [string, T][];
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
this.storage.removeItem(key);
|
||||
async remove(key) {
|
||||
return this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys: string[]) {
|
||||
if (!keys) return;
|
||||
this.storage.removeItems(keys);
|
||||
async removeMulti(keys) {
|
||||
if (!keys) return true;
|
||||
return this.storage.removeItems(keys);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.storage.clearStore();
|
||||
return this.storage.clearStore();
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
@@ -107,46 +113,54 @@ export class KV {
|
||||
return keys;
|
||||
}
|
||||
|
||||
async writeMulti(items: [string, any][]) {
|
||||
await this.storage.setMultipleItemsAsync(items, "object");
|
||||
async writeMulti(items) {
|
||||
return this.storage.setMultipleItemsAsync(items, "object");
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultStorage = new KV(MMKV);
|
||||
|
||||
export const Storage: IStorage = {
|
||||
write<T>(key: string, data: T): Promise<void> {
|
||||
return DefaultStorage.write(key, data);
|
||||
},
|
||||
writeMulti<T>(entries: [string, T][]): Promise<void> {
|
||||
return DefaultStorage.writeMulti(entries);
|
||||
},
|
||||
readMulti<T>(keys: string[]): Promise<[string, T][]> {
|
||||
return DefaultStorage.readMulti(keys);
|
||||
},
|
||||
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
|
||||
return DefaultStorage.read(key, isArray);
|
||||
},
|
||||
remove(key: string): Promise<void> {
|
||||
return DefaultStorage.remove(key);
|
||||
},
|
||||
removeMulti(keys: string[]): Promise<void> {
|
||||
return DefaultStorage.removeMulti(keys);
|
||||
},
|
||||
clear(): Promise<void> {
|
||||
return DefaultStorage.clear();
|
||||
},
|
||||
getAllKeys(): Promise<string[]> {
|
||||
return DefaultStorage.getAllKeys();
|
||||
},
|
||||
hash,
|
||||
getCryptoKey,
|
||||
async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
async function checkAndCreateDir(path) {
|
||||
let dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
let exists = await RNFetchBlob.fs.exists(dir);
|
||||
let isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
export default {
|
||||
read: (key) => DefaultStorage.read(key),
|
||||
write: (key, value) => DefaultStorage.write(key, value),
|
||||
readMulti: (keys) => DefaultStorage.readMulti(keys),
|
||||
remove: (key) => DefaultStorage.remove(key),
|
||||
clear: () => DefaultStorage.clear(),
|
||||
getAllKeys: () => DefaultStorage.getAllKeys(),
|
||||
writeMulti: (items) => DefaultStorage.writeMulti(items),
|
||||
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
getRandomBytes,
|
||||
checkAndCreateDir,
|
||||
requestPermission,
|
||||
deriveCryptoKey,
|
||||
getCryptoKey,
|
||||
removeCryptoKey,
|
||||
hash,
|
||||
generateCryptoKey,
|
||||
generateCryptoKeyFallback,
|
||||
deriveCryptoKeyFallback
|
||||
encryptMulti
|
||||
};
|
||||
@@ -27,9 +27,13 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
*/
|
||||
export async function compressToBase64(path, type) {
|
||||
const { width: screenWidth, scale } = Dimensions.get("window");
|
||||
|
||||
console.log("COMPRESSING TO BASE64...");
|
||||
return new Promise((resolve) => {
|
||||
console.log(path, "image path...");
|
||||
|
||||
Image.getSize(path, async (width) => {
|
||||
console.log("image width", width);
|
||||
|
||||
const response = await ImageResizer.createResizedImage(
|
||||
path,
|
||||
screenWidth * scale,
|
||||
@@ -51,12 +55,10 @@ export async function compressToBase64(path, type) {
|
||||
: response.uri,
|
||||
"base64"
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(response.uri.replace("file://", ""))
|
||||
.catch(console.log);
|
||||
|
||||
resolve(base64);
|
||||
});
|
||||
@@ -78,8 +80,6 @@ export async function compressToFile(path, type) {
|
||||
onlyScaleDown: true
|
||||
}
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
return response.uri;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -24,12 +25,12 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import filesystem from ".";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
@@ -50,23 +51,23 @@ export async function downloadAllAttachments() {
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param {string[]} attachmentIds
|
||||
* @param {string[]} attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(attachmentIds: string[]) {
|
||||
export async function downloadAttachments(attachments) {
|
||||
await createCacheDir();
|
||||
if (!attachmentIds || !attachmentIds.length) return;
|
||||
if (!attachments || !attachments.length) return;
|
||||
const groupId = `download-all-${Date.now()}`;
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
const file = await ScopedStorage.openDocumentTree(true);
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
@@ -82,10 +83,8 @@ export async function downloadAttachments(attachmentIds: string[]) {
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const isCancelled = () => {
|
||||
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: 0,
|
||||
@@ -98,21 +97,19 @@ export async function downloadAttachments(attachmentIds: string[]) {
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < attachmentIds.length; i++) {
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
if (isCancelled()) return;
|
||||
const attachment = await db.attachments.attachment(attachmentIds[i]);
|
||||
if (!attachment) continue;
|
||||
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachmentIds.length,
|
||||
total: attachments.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
// Download to cache
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
@@ -187,58 +184,48 @@ export async function downloadAttachments(attachmentIds: string[]) {
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e as Error, "Error zipping attachments");
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hashOrId: string,
|
||||
hash,
|
||||
global = true,
|
||||
options?: {
|
||||
silent?: boolean;
|
||||
cache?: boolean;
|
||||
throwError?: boolean;
|
||||
groupId?: string;
|
||||
base64?: boolean;
|
||||
text?: boolean;
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
const attachment = await db.attachments.attachment(hashOrId);
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
DatabaseLogger.log("Attachment not found", {
|
||||
hash: hashOrId
|
||||
});
|
||||
DatabaseLogger.log("Attachment not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let folder: {
|
||||
uri: string;
|
||||
} | null = null;
|
||||
if (!options?.cache) {
|
||||
let folder = {};
|
||||
if (!options.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder = {
|
||||
uri: await filesystem.checkAndCreateDir("/downloads/")
|
||||
};
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
groupId: options.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 1,
|
||||
filename: attachment.filename
|
||||
@@ -247,13 +234,13 @@ export default async function downloadAttachment(
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options?.groupId || attachment.hash,
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
groupId: options.groupId || attachment.hash,
|
||||
current: 1,
|
||||
total: 1,
|
||||
filename: attachment.filename,
|
||||
@@ -265,7 +252,7 @@ export default async function downloadAttachment(
|
||||
return;
|
||||
}
|
||||
|
||||
if (options?.base64 || options?.text) {
|
||||
if (options.base64 || options.text) {
|
||||
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
|
||||
return await db.attachments.read(
|
||||
attachment.hash,
|
||||
@@ -273,16 +260,14 @@ export default async function downloadAttachment(
|
||||
);
|
||||
}
|
||||
|
||||
const filename = await getFileNameWithExtension(
|
||||
let filename = await getFileNameWithExtension(
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
|
||||
const key = await db.attachments.decryptKey(attachment.key);
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
if (!key) return;
|
||||
|
||||
const info = {
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
length: attachment.size,
|
||||
@@ -290,18 +275,18 @@ export default async function downloadAttachment(
|
||||
hash: attachment.hash,
|
||||
hashType: attachment.hashType,
|
||||
mime: attachment.mimeType,
|
||||
fileName: options?.cache ? undefined : filename,
|
||||
uri: options?.cache ? undefined : folder?.uri,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
options?.cache ? "cache" : "file"
|
||||
options.cache ? "cache" : "file"
|
||||
);
|
||||
|
||||
if (!options?.silent) {
|
||||
if (!options.silent) {
|
||||
ToastManager.show({
|
||||
heading: strings.network.downloadSuccess(),
|
||||
message: strings.network.fileDownloaded(filename),
|
||||
@@ -309,15 +294,15 @@ export default async function downloadAttachment(
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !options?.cache) {
|
||||
fileUri = folder?.uri + `/${filename}`;
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
if (!options?.silent) {
|
||||
if (!options.silent) {
|
||||
presentSheet({
|
||||
title: strings.network.fileDownloaded(),
|
||||
paragraph: strings.fileSaved(filename, Platform.OS),
|
||||
icon: "download",
|
||||
context: global ? "global" : attachment.hash,
|
||||
context: global ? null : attachment.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
@@ -327,18 +312,14 @@ export default async function downloadAttachment(
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
groupId: options.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 0,
|
||||
filename: attachment.filename,
|
||||
@@ -346,7 +327,7 @@ export default async function downloadAttachment(
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options?.throwError) {
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ 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 { RequestOptions } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
@@ -27,13 +26,7 @@ import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
@@ -43,11 +36,9 @@ export async function downloadFile(
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
|
||||
const { url, headers, chunkSize } = requestOptions;
|
||||
const tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
const originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
@@ -55,8 +46,6 @@ export async function downloadFile(
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
@@ -82,21 +71,21 @@ export async function downloadFile(
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const resolveUrlResponse = await fetch(url, {
|
||||
let res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers
|
||||
});
|
||||
|
||||
if (!resolveUrlResponse.ok) {
|
||||
if (!res.ok) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
|
||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error(
|
||||
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
);
|
||||
}
|
||||
|
||||
const downloadUrl = await resolveUrlResponse.text();
|
||||
const downloadUrl = await res.text();
|
||||
|
||||
if (!downloadUrl) {
|
||||
DatabaseLogger.log(
|
||||
@@ -106,12 +95,12 @@ export async function downloadFile(
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
const request = RNFetchBlob.config({
|
||||
let request = RNFetchBlob.config({
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
})
|
||||
.fetch("GET", downloadUrl)
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
@@ -120,16 +109,15 @@ export async function downloadFile(
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = async (reason) => {
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
};
|
||||
|
||||
const response = await request;
|
||||
let response = await request;
|
||||
console.log(response.info().headers);
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
@@ -140,18 +128,14 @@ export async function downloadFile(
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
const status = response.info().status;
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (await exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
if (exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
@@ -159,14 +143,11 @@ export async function downloadFile(
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (
|
||||
(e as Error).message !== "canceled" &&
|
||||
!(e as Error).message.includes("NoSuchKey")
|
||||
) {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
const toast = {
|
||||
heading: strings.downloadError((e as Error).message),
|
||||
message: (e as Error).message,
|
||||
type: "error" as const,
|
||||
heading: strings.downloadError(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
};
|
||||
ToastManager.show(toast);
|
||||
@@ -175,20 +156,17 @@ export async function downloadFile(
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.error(e, "Download failed: ", {
|
||||
url
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash: string) {
|
||||
export async function checkAttachment(hash) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
@@ -198,7 +176,7 @@ export async function checkAttachment(hash: string) {
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
|
||||
console.log("File Size", size);
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0)
|
||||
@@ -206,7 +184,7 @@ export async function checkAttachment(hash: string) {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: (e as Error)?.message };
|
||||
return { failed: e?.message };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
@@ -17,41 +17,24 @@ 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 { IFileStorage } from "@notesnook/core";
|
||||
import { checkAttachment, downloadFile } from "./download";
|
||||
import {
|
||||
bulkExists,
|
||||
clearCache,
|
||||
clearFileStorage,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
deleteFile,
|
||||
exists,
|
||||
getCacheSize,
|
||||
hashBase64,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import {
|
||||
cancelable,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize,
|
||||
requestPermission
|
||||
} from "./utils";
|
||||
|
||||
export default {
|
||||
checkAttachment,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
getCacheSize,
|
||||
requestPermission,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize
|
||||
};
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
|
||||
export const FileStorage: IFileStorage = {
|
||||
export default {
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
@@ -61,5 +44,10 @@ export const FileStorage: IFileStorage = {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
bulkExists
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
};
|
||||
@@ -18,13 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
FileEncryptionMetadataWithHash,
|
||||
FileEncryptionMetadataWithOutputType,
|
||||
Output,
|
||||
RequestOptions
|
||||
} from "@notesnook/core";
|
||||
import { DataFormat, SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
@@ -32,21 +25,17 @@ import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
filename: string,
|
||||
key: SerializedKey,
|
||||
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
|
||||
) {
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const output = await Sodium.decryptFile(
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
@@ -58,16 +47,15 @@ export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
|
||||
return output as Output<TOutputFormat>;
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data: string) {
|
||||
export async function hashBase64(data) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
@@ -79,79 +67,61 @@ export async function hashBase64(data: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64(
|
||||
data: string,
|
||||
encryptionKey: SerializedKey,
|
||||
mimeType: string
|
||||
): Promise<FileEncryptionMetadataWithHash> {
|
||||
export async function writeEncryptedBase64(data, key) {
|
||||
await createCacheDir();
|
||||
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
const output = await Sodium.encryptFile(encryptionKey, {
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
|
||||
RNFetchBlob.fs.unlink(filepath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
console.log("encrypted file output: ", output);
|
||||
output.size = output.length;
|
||||
delete output.length;
|
||||
return {
|
||||
...output,
|
||||
alg: "xcha-stream"
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
export async function deleteFile(filename, data) {
|
||||
await createCacheDir();
|
||||
const localFilePath = cacheDir + `/${filename}`;
|
||||
if (!requestOptions) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
return true;
|
||||
}
|
||||
|
||||
const { url, headers } = requestOptions;
|
||||
|
||||
let { url, headers } = data;
|
||||
try {
|
||||
const response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
const status = response.info().status;
|
||||
const ok = status >= 200 && status < 300;
|
||||
let response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
let status = response.info().status;
|
||||
let ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "Delete file", {
|
||||
url: url
|
||||
});
|
||||
console.log("delete file: ", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (const file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
for (let file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
}
|
||||
for (const file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
for (let file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "clearFileStorage");
|
||||
console.log("clearFileStorage", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,91 +142,62 @@ export async function migrateFilesFromCache() {
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
console.log("Files migrated already");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (const file of oldCache) {
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
console.log("Moved", file);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "migrateFilesFromCache");
|
||||
console.log("migrateFilesFromCache", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path: string) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name: string) {
|
||||
export async function deleteCacheFileByName(name) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache") || file.startsWith("NN_")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCachePathForFile(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
const exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
return existsInAppGroup ? appGroupPath : path;
|
||||
}
|
||||
|
||||
export async function exists(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
@@ -270,7 +211,6 @@ export async function exists(filename: string) {
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.size + totalAbytes;
|
||||
@@ -285,9 +225,7 @@ export async function exists(filename: string) {
|
||||
);
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -296,7 +234,7 @@ export async function exists(filename: string) {
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files: string[]) {
|
||||
export async function bulkExists(files) {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
@@ -305,7 +243,7 @@ export async function bulkExists(files: string[]) {
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter(
|
||||
@@ -323,9 +261,9 @@ export async function getCacheSize() {
|
||||
await createCacheDir();
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
|
||||
stat.forEach((file) => {
|
||||
total += parseInt(file.size as unknown as string);
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
@@ -17,7 +17,6 @@ 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 { RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -25,22 +24,11 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import {
|
||||
cacheDir,
|
||||
checkUpload,
|
||||
FileSizeResult,
|
||||
getUploadedFileSize
|
||||
} from "./utils";
|
||||
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
|
||||
|
||||
export async function uploadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
const { url, headers } = requestOptions;
|
||||
let { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
@@ -51,7 +39,7 @@ export async function uploadFile(
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
filePath = appGroupPath;
|
||||
@@ -66,15 +54,14 @@ export async function uploadFile(
|
||||
|
||||
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
|
||||
|
||||
const remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === FileSizeResult.Error) return false;
|
||||
|
||||
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
|
||||
let remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === -1) return false;
|
||||
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const uploadUrlResponse = await fetch(url, {
|
||||
let uploadUrlResponse = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
@@ -91,8 +78,7 @@ export async function uploadFile(
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
const uploadRequest = RNFetchBlob.config({
|
||||
//@ts-ignore
|
||||
let uploadRequest = RNFetchBlob.config({
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
@@ -112,14 +98,14 @@ export async function uploadFile(
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = async () => {
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
uploadRequest.cancel();
|
||||
};
|
||||
|
||||
const uploadResponse = await uploadRequest;
|
||||
const status = uploadResponse.info().status;
|
||||
const uploaded = status >= 200 && status < 300;
|
||||
let uploadResponse = await uploadRequest;
|
||||
let status = uploadResponse.info().status;
|
||||
let uploaded = status >= 200 && status < 300;
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
@@ -132,13 +118,12 @@ export async function uploadFile(
|
||||
);
|
||||
}
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
||||
return uploaded;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
ToastManager.error(e as Error, "File upload failed");
|
||||
ToastManager.error(e, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { hosts, RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { hosts } from "@notesnook/core";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
@@ -31,13 +31,18 @@ export const cacheDir =
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
|
||||
export function getRandomId(prefix: string) {
|
||||
export function getRandomId(prefix) {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
export function parseS3Error(data?: string) {
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
const error = {
|
||||
@@ -65,19 +70,11 @@ export function parseS3Error(data?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelable(
|
||||
operation: (
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) => Promise<boolean>
|
||||
) {
|
||||
export function cancelable(operation) {
|
||||
const cancelToken = {
|
||||
cancel: async (reason?: string) => {}
|
||||
cancel: () => {}
|
||||
};
|
||||
return (filename: string, requestOptions: RequestOptions) => {
|
||||
return (filename, requestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
@@ -87,35 +84,29 @@ export function cancelable(
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source: string, dest: string) {
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
//@ts-ignore
|
||||
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path: string) {
|
||||
export async function releasePermissions(path) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (const uri of uris) {
|
||||
for (let uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FileSizeResult = {
|
||||
Empty: 0,
|
||||
Error: -1
|
||||
};
|
||||
|
||||
export async function getUploadedFileSize(hash: string, retry = 0) {
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
@@ -123,37 +114,17 @@ export async function getUploadedFileSize(hash: string, retry = 0) {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (
|
||||
!attachmentInfo.ok ||
|
||||
attachmentInfo.headers?.get("content-length") === null
|
||||
) {
|
||||
if (retry < 3) {
|
||||
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
|
||||
return getUploadedFileSize(hash, retry + 1);
|
||||
}
|
||||
throw new Error(
|
||||
`File size check failed: ${hash}, ${
|
||||
attachmentInfo.status
|
||||
}, ${attachmentInfo.headers?.get("content-length")}`
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length") as string
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return FileSizeResult.Error;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpload(
|
||||
filename: string,
|
||||
chunkSize: number,
|
||||
expectedSize: number
|
||||
) {
|
||||
export async function checkUpload(filename, chunkSize, expectedSize) {
|
||||
const size = await getUploadedFileSize(filename);
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
@@ -167,25 +138,3 @@ export async function checkUpload(
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
export async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
export async function checkAndCreateDir(path: string) {
|
||||
const dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
const exists = await RNFetchBlob.fs.exists(dir);
|
||||
const isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function info(context: string, ...logs: unknown[]) {}
|
||||
function info(context: string, ...logs: unknown[]) {
|
||||
console.log(`${new Date().toLocaleDateString()}::info::${context}:`, ...logs);
|
||||
}
|
||||
|
||||
function error(context: string, ...logs: unknown[]) {
|
||||
console.log(
|
||||
|
||||
@@ -24,7 +24,6 @@ import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Announcement = ({ color }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -37,8 +36,9 @@ export const Announcement = ({ color }) => {
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -54,7 +54,7 @@ export const Announcement = ({ color }) => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 12
|
||||
}}
|
||||
>
|
||||
{announcement?.body
|
||||
|
||||
@@ -20,13 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Body = ({ text, style = {} }) => {
|
||||
return (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
...getStyle(style)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -23,13 +23,12 @@ import { Linking, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eCloseAnnouncementDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PricingPlans } from "../premium/pricing-plans";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { allowedOnPlatform, getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -42,9 +41,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
await sleep(500);
|
||||
}
|
||||
if (item.type === "link") {
|
||||
Linking.openURL(item.data).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
Linking.openURL(item.data).catch(console.log);
|
||||
} else if (item.type === "promo") {
|
||||
presentSheet({
|
||||
component: (
|
||||
@@ -62,7 +59,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
...getStyle(style),
|
||||
flexDirection: inline ? "row" : "column"
|
||||
}}
|
||||
@@ -76,7 +73,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontSize={SIZE.sm}
|
||||
type="transparent"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
@@ -84,7 +81,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
onPress={() => onPress(item)}
|
||||
bold
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
@@ -96,7 +93,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontSize={SIZE.sm}
|
||||
type="plain"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
@@ -119,7 +116,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={AppFontSize.md}
|
||||
fontSize={SIZE.md}
|
||||
buttonType={{
|
||||
color: color ? color : colors.primary.accent,
|
||||
text: color
|
||||
@@ -131,7 +128,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
onPress={() => onPress(item)}
|
||||
width={250}
|
||||
style={{
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginBottom: 5,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
@@ -142,14 +139,14 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={AppFontSize.xs}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
style={{
|
||||
minWidth: "50%",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
marginTop: 5
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getStyle } from "./functions";
|
||||
|
||||
@@ -30,7 +30,7 @@ export const Description = ({ text, style = {}, inline }) => {
|
||||
...getStyle(style),
|
||||
textAlign: inline ? "left" : style?.textAlign
|
||||
}}
|
||||
size={inline ? AppFontSize.sm : AppFontSize.md}
|
||||
size={inline ? SIZE.sm : SIZE.md}
|
||||
>
|
||||
{text}
|
||||
</Paragraph>
|
||||
|
||||
@@ -28,7 +28,6 @@ import { List } from "./list";
|
||||
import { Photo } from "./photo";
|
||||
import { SubHeading } from "./subheading";
|
||||
import { Title } from "./title";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export function allowedOnPlatform(platforms) {
|
||||
if (!platforms) return true;
|
||||
@@ -54,7 +53,7 @@ const Features = () => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
|
||||
@@ -22,13 +22,12 @@ import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const List = ({ items, listType, style = {} }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
paddingLeft: listType === "ordered" ? 25 : 25,
|
||||
...getStyle(style)
|
||||
}}
|
||||
@@ -37,7 +36,7 @@ export const List = ({ items, listType, style = {} }) => {
|
||||
<View
|
||||
key={item.text}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: 6,
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,18 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const SubHeading = ({ text, style = {} }) => {
|
||||
return (
|
||||
<Heading
|
||||
size={AppFontSize.md + 2}
|
||||
size={SIZE.md + 2}
|
||||
style={{
|
||||
marginHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginHorizontal: 12,
|
||||
marginTop: 12,
|
||||
...getStyle(style)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -20,11 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Title = ({ text, style = {}, inline }) => {
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
@@ -37,19 +36,19 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: inline ? DefaultAppStyles.GAP_VERTICAL_SMALL : 0
|
||||
marginBottom: inline ? 5 : 0
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
marginHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginHorizontal: 12,
|
||||
marginTop: 12,
|
||||
...getStyle(style),
|
||||
textAlign: inline ? "left" : style?.textAlign,
|
||||
flexShrink: 1
|
||||
}}
|
||||
numberOfLines={1}
|
||||
size={inline ? AppFontSize.md : AppFontSize.xl}
|
||||
size={inline ? SIZE.md : SIZE.xl}
|
||||
>
|
||||
{inline ? text?.toUpperCase() : text}
|
||||
</Heading>
|
||||
@@ -68,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
right: 0
|
||||
}}
|
||||
iconSize={24}
|
||||
fontSize={AppFontSize.xs}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingVertical: 0,
|
||||
@@ -81,9 +80,9 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
) : (
|
||||
<Heading
|
||||
style={{
|
||||
marginHorizontal: DefaultAppStyles.GAP,
|
||||
marginHorizontal: 12,
|
||||
...getStyle(style),
|
||||
marginTop: style?.marginTop || DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: style?.marginTop || 12
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -44,6 +44,7 @@ import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
@@ -53,9 +54,6 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -84,7 +82,7 @@ const verifyUserPassword = async (password: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const AppLocked = () => {
|
||||
const AppLockedOverlay = () => {
|
||||
const initialLaunchBiometricRequest = useRef(true);
|
||||
const { colors } = useThemeColors();
|
||||
const user = getUser();
|
||||
@@ -111,25 +109,17 @@ const AppLocked = () => {
|
||||
);
|
||||
|
||||
const onUnlockAppRequested = useCallback(async () => {
|
||||
editorController.current?.commands.blur(useTabStore.getState().currentTab);
|
||||
editorController.current?.commands.blurPassInput();
|
||||
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
) {
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
biometricUnlockAwaitingUserInput.current = true;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await NotesnookModule.getActivityName();
|
||||
if (
|
||||
activityName !== "MainActivity" &&
|
||||
activityName !== "NotePreviewConfigureActivity"
|
||||
)
|
||||
return;
|
||||
if (activityName !== "MainActivity") return;
|
||||
}
|
||||
useSettingStore.getState().setRequestBiometrics(true);
|
||||
|
||||
@@ -187,15 +177,10 @@ const AppLocked = () => {
|
||||
(prevState === "background" || initialLaunchBiometricRequest.current)
|
||||
) {
|
||||
if (SettingsService.shouldLockAppOnEnterForeground()) {
|
||||
if (useSettingStore.getState().appDidEnterBackgroundForAction) {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseLogger.info("Locking app on entering foreground");
|
||||
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
biometricUnlockAwaitingUserInput.current ||
|
||||
@@ -294,16 +279,6 @@ const AppLocked = () => {
|
||||
keyboardType={
|
||||
appLockHasPasswordSecurity ? keyboardType : "default"
|
||||
}
|
||||
onLayout={async () => {
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
) {
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 32);
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
appLockHasPasswordSecurity
|
||||
? keyboardType === "numeric"
|
||||
@@ -336,7 +311,7 @@ const AppLocked = () => {
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={AppFontSize.md}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@@ -357,4 +332,4 @@ const AppLocked = () => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AppLocked;
|
||||
export default AppLockedOverlay;
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
eDBItemUpdate,
|
||||
eOnLoadNote
|
||||
} from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
@@ -58,7 +58,6 @@ import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -183,17 +182,18 @@ const Actions = ({
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused = useTabStore.getState().currentTab === tab.id;
|
||||
const tab = useTabStore.getState().getTabForNote(id);
|
||||
if (tab !== undefined) {
|
||||
const isFocused = useTabStore.getState().currentTab === tab;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(true, tab.id);
|
||||
editorController.current.commands.setLoading(true, tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
close?.();
|
||||
},
|
||||
@@ -227,15 +227,14 @@ const Actions = ({
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom:
|
||||
notes && notes.length > 0 ? 0 : DefaultAppStyles.GAP_VERTICAL
|
||||
marginBottom: notes && notes.length > 0 ? 0 : 12
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{filename}
|
||||
</Heading>
|
||||
@@ -243,21 +242,21 @@ const Actions = ({
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 6,
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{attachment.mimeType}
|
||||
</Paragraph>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
|
||||
{notes.length ? (
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{strings.notes(notes.length)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
@@ -270,7 +269,7 @@ const Actions = ({
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
size={AppFontSize.xs}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.hash}
|
||||
@@ -285,16 +284,16 @@ const Actions = ({
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{strings.listOf()} {strings.dataTypesPlural.note()}:
|
||||
</Heading>
|
||||
@@ -309,14 +308,14 @@ const Actions = ({
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: 12,
|
||||
alignItems: "flex-start",
|
||||
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs}>{item.title}</Paragraph>
|
||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
@@ -337,7 +336,7 @@ const Actions = ({
|
||||
icon={item.icon}
|
||||
loading={loading?.name === item.name}
|
||||
type="plain"
|
||||
fontSize={AppFontSize.sm}
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
justifyContent: "flex-start",
|
||||
@@ -349,7 +348,7 @@ const Actions = ({
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{failed ? (
|
||||
|
||||
@@ -25,14 +25,13 @@ import { TouchableOpacity, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Actions from "./actions";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
function getFileExtension(filename: string) {
|
||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
@@ -77,8 +76,8 @@ export const AttachmentItem = ({
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
minHeight: 45
|
||||
}}
|
||||
>
|
||||
@@ -102,7 +101,7 @@ export const AttachmentItem = ({
|
||||
paddingHorizontal: 2,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: defaultBorderRadius
|
||||
borderRadius: 5
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
@@ -125,7 +124,7 @@ export const AttachmentItem = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
@@ -137,10 +136,7 @@ export const AttachmentItem = ({
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xxs}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
|
||||
{strings.fileSize()}: {formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
@@ -163,7 +159,7 @@ export const AttachmentItem = ({
|
||||
}}
|
||||
>
|
||||
<ProgressCircleComponent
|
||||
size={AppFontSize.xxl}
|
||||
size={SIZE.xxl}
|
||||
progress={
|
||||
currentProgress?.value ? currentProgress?.value / 100 : 0
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import { downloadAttachments } from "../../common/filesystem/download-attachment
|
||||
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Header } from "../header";
|
||||
@@ -50,7 +50,6 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const DEFAULT_SORTING: SortOptions = {
|
||||
sortBy: "dateEdited",
|
||||
@@ -326,31 +325,30 @@ export const AttachmentDialog = ({
|
||||
title={strings.manageAttachments()}
|
||||
renderedInRoute="SettingsGroup"
|
||||
canGoBack
|
||||
// TODO: Add headerRightButtons
|
||||
// headerRightButtons={[
|
||||
// {
|
||||
// onPress() {
|
||||
// onCheck();
|
||||
// },
|
||||
// title: strings.recheckAll()
|
||||
// },
|
||||
// {
|
||||
// onPress() {
|
||||
// if (!attachments) return;
|
||||
// presentDialog({
|
||||
// title: strings.doActions.download.attachment(
|
||||
// attachments.placeholders.length
|
||||
// ),
|
||||
// positiveText: strings.network.download(),
|
||||
// positivePress: async () => {
|
||||
// downloadAttachments(await attachments.ids());
|
||||
// },
|
||||
// negativeText: strings.cancel()
|
||||
// });
|
||||
// },
|
||||
// title: strings.downloadAllAttachments()
|
||||
// }
|
||||
// ]}
|
||||
headerRightButtons={[
|
||||
{
|
||||
onPress() {
|
||||
onCheck();
|
||||
},
|
||||
title: strings.recheckAll()
|
||||
},
|
||||
{
|
||||
onPress() {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: strings.doActions.download.attachment(
|
||||
attachments.placeholders.length
|
||||
),
|
||||
positiveText: strings.network.download(),
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: strings.cancel()
|
||||
});
|
||||
},
|
||||
title: strings.downloadAllAttachments()
|
||||
}
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
@@ -358,7 +356,7 @@ export const AttachmentDialog = ({
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
|
||||
@@ -371,15 +369,21 @@ export const AttachmentDialog = ({
|
||||
<IconButton
|
||||
name="check-all"
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
onPress={onCheck}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
@@ -394,7 +398,7 @@ export const AttachmentDialog = ({
|
||||
negativeText: strings.cancel()
|
||||
});
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -404,7 +408,7 @@ export const AttachmentDialog = ({
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
@@ -424,7 +428,7 @@ export const AttachmentDialog = ({
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
gap: 12,
|
||||
@@ -469,7 +473,7 @@ export const AttachmentDialog = ({
|
||||
<IconButton
|
||||
type={rechecker.isWorking ? "errorShade" : "plain"}
|
||||
name={rechecker.isWorking ? "close" : "check"}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
rechecker.isWorking ? colors.error.icon : colors.primary.accent
|
||||
}
|
||||
@@ -490,7 +494,7 @@ export const AttachmentDialog = ({
|
||||
backgroundColor: colors.primary.background,
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingVertical: 12
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignItems: "center"
|
||||
@@ -507,10 +511,10 @@ export const AttachmentDialog = ({
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
height: 40,
|
||||
minWidth: 80
|
||||
}}
|
||||
@@ -524,9 +528,7 @@ export const AttachmentDialog = ({
|
||||
setAttachments(results);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -37,7 +37,6 @@ import { hideAuth, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
@@ -134,7 +133,7 @@ const AuthModal = () => {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
@@ -168,7 +167,7 @@ const AuthModal = () => {
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -36,7 +36,6 @@ import { Dialog } from "../dialog";
|
||||
import BackupService from "../../services/backup";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const passwordInputRef = useRef();
|
||||
@@ -69,15 +68,11 @@ export const ChangePassword = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(
|
||||
false,
|
||||
"change-password-dialog",
|
||||
"partial"
|
||||
);
|
||||
if (result.error) {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (!result.error)
|
||||
throw new Error(strings.backupFailed() + `: ${result.error}`);
|
||||
}
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
ToastManager.show({
|
||||
heading: strings.passwordChangedSuccessfully(),
|
||||
@@ -104,7 +99,7 @@ export const ChangePassword = () => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
@@ -149,7 +144,7 @@ export const ChangePassword = () => {
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 10,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
|
||||
@@ -20,20 +20,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { createRef } from "react";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { eCloseLoginDialog } from "../../utils/events";
|
||||
import Navigation from "../../services/navigation";
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
signup: 1,
|
||||
welcomeSignup: 2,
|
||||
trialSignup: 3
|
||||
};
|
||||
|
||||
export const initialAuthMode = createRef(0);
|
||||
export function hideAuth() {
|
||||
eSendEvent(eCloseLoginDialog);
|
||||
if (initialAuthMode.current === AuthMode.welcomeSignup) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
|
||||
// if (initialAuthMode.current === 2) {
|
||||
// Navigation.replace(
|
||||
// {
|
||||
// name: 'Notes'
|
||||
// },
|
||||
// {
|
||||
// menu: true
|
||||
// }
|
||||
// );
|
||||
// } else {
|
||||
// Navigation.goBack();
|
||||
// }
|
||||
|
||||
// tabBarRef.current?.unlock();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
const { colors } = useThemeColors("sheet");
|
||||
@@ -108,7 +107,7 @@ export const ForgotPassword = () => {
|
||||
{sent ? (
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP,
|
||||
padding: 12,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 50
|
||||
@@ -139,7 +138,7 @@ export const ForgotPassword = () => {
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
@@ -165,7 +164,7 @@ export const ForgotPassword = () => {
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 10,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
|
||||
@@ -17,98 +17,35 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { AuthMode, initialAuthMode } from "./common";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
signup: 1,
|
||||
welcomeSignup: 2,
|
||||
trialSignup: 3
|
||||
};
|
||||
|
||||
const Auth = ({ navigation, route }) => {
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(
|
||||
route?.params?.mode || AuthMode.login
|
||||
);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
initialAuthMode.current = route?.params.mode || AuthMode.login;
|
||||
useNavigationFocus(navigation, {});
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
//tabBarRef?.current.lock();
|
||||
initialAuthMode.current = route?.params.mode || AuthMode.login;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: insets.top,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
backgroundColor:
|
||||
deviceMode === "mobile" ? colors.secondary.background : null,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
if (initialAuthMode.current === 2) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title={strings.skip()}
|
||||
onPress={() => {
|
||||
if (initialAuthMode.current === 2) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
|
||||
@@ -17,7 +17,6 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
@@ -25,9 +24,10 @@ import { SheetManager } from "react-native-actions-sheet";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
@@ -38,7 +38,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -71,8 +71,8 @@ export const Login = ({ changeMode }) => {
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
setStep(LoginSteps.emailAuth);
|
||||
@@ -96,24 +96,23 @@ export const Login = ({ changeMode }) => {
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "70%",
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
@@ -144,10 +143,10 @@ export const Login = ({ changeMode }) => {
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
@@ -164,7 +163,7 @@ export const Login = ({ changeMode }) => {
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -217,7 +216,7 @@ export const Login = ({ changeMode }) => {
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
height: 30,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
@@ -227,7 +226,7 @@ export const Login = ({ changeMode }) => {
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
@@ -245,8 +244,11 @@ export const Login = ({ changeMode }) => {
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
width: 250
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
/>
|
||||
@@ -256,16 +258,19 @@ export const Login = ({ changeMode }) => {
|
||||
title={strings.cancelLogin()}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: 250
|
||||
height: 30,
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="secondaryAccented"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -278,17 +283,17 @@ export const Login = ({ changeMode }) => {
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.signUp()}
|
||||
|
||||
@@ -34,7 +34,7 @@ import Sync from "../../services/sync";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
@@ -48,8 +48,18 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getObfuscatedEmail } from "../../utils/functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
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) {
|
||||
for (let i = 0; i < gp3.length; i++) {
|
||||
gp2 += "*";
|
||||
}
|
||||
return gp2;
|
||||
});
|
||||
}
|
||||
|
||||
export const SessionExpired = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -158,7 +168,7 @@ export const SessionExpired = () => {
|
||||
<View
|
||||
style={{
|
||||
width: focused ? "100%" : "99.9%",
|
||||
padding: DefaultAppStyles.GAP,
|
||||
padding: 12,
|
||||
justifyContent: "center",
|
||||
flex: 1,
|
||||
backgroundColor: colors.primary.background
|
||||
@@ -183,7 +193,7 @@ export const SessionExpired = () => {
|
||||
color={colors.error.icon}
|
||||
size={50}
|
||||
/>
|
||||
<Heading size={AppFontSize.xxxl} color={colors.primary.heading}>
|
||||
<Heading size={SIZE.xxxl} color={colors.primary.heading}>
|
||||
{strings.sessionExpired()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
@@ -214,7 +224,7 @@ export const SessionExpired = () => {
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 10,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
@@ -226,7 +236,7 @@ export const SessionExpired = () => {
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 10,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
|
||||
@@ -17,8 +17,6 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
@@ -26,17 +24,19 @@ import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -50,8 +50,8 @@ 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 { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
@@ -105,24 +105,23 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "70%",
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
@@ -154,9 +153,9 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{strings.createYourAccount()}
|
||||
</Heading>
|
||||
@@ -165,7 +164,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
@@ -234,12 +233,12 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
@@ -248,12 +247,11 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[1]()}
|
||||
</Paragraph>{" "}
|
||||
</Paragraph>
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
@@ -262,9 +260,8 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
</Paragraph>
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
|
||||
@@ -273,8 +270,11 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
width: 250
|
||||
marginRight: 12,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -286,14 +286,14 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
|
||||
@@ -30,14 +30,13 @@ import {
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -205,9 +204,9 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
caretHidden
|
||||
height={60}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
fontSize: SIZE.lg,
|
||||
height: 60,
|
||||
textAlign: "center",
|
||||
letterSpacing: 10,
|
||||
width: 250
|
||||
@@ -217,13 +216,11 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
height: 60,
|
||||
borderWidth: 0,
|
||||
width: undefined,
|
||||
minWidth: "50%"
|
||||
}}
|
||||
wrapperStyle={{
|
||||
height: 60
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -232,12 +229,9 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
width={250}
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
/>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondaryAccented"
|
||||
onPress={onCancel}
|
||||
width={250}
|
||||
style={{
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -248,6 +242,13 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="plain"
|
||||
onPress={onCancel}
|
||||
height={30}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -261,8 +262,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
marginTop: 0,
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
@@ -274,6 +275,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
<IconButton
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={15}
|
||||
@@ -285,7 +288,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.md}>{item.title}</Paragraph>
|
||||
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
|
||||
@@ -70,7 +70,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
|
||||
console.log("email auth", mfaInfo);
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa, callback) => {
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Keyboard, TouchableOpacity, View, ViewStyle } from "react-native";
|
||||
import { Keyboard, View } from "react-native";
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
@@ -33,31 +33,20 @@ import { editorState } from "../../screens/editor/tiptap/utils";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { AppFontSize, normalize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
|
||||
import { SIZE, normalize } from "../../utils/size";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
|
||||
interface FloatingButtonProps {
|
||||
onPress: () => void;
|
||||
color?: string;
|
||||
shouldShow?: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
icon?: string;
|
||||
testID?: string;
|
||||
position?: "left" | "right";
|
||||
size?: "small" | "large";
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const FloatingButton = ({
|
||||
onPress,
|
||||
color,
|
||||
alwaysVisible = false,
|
||||
icon,
|
||||
testID,
|
||||
position = "right",
|
||||
size = "large",
|
||||
style
|
||||
alwaysVisible = false
|
||||
}: FloatingButtonProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
@@ -119,26 +108,20 @@ const FloatingButton = ({
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
right: position === "right" ? DefaultAppStyles.GAP : undefined,
|
||||
left: position === "left" ? DefaultAppStyles.GAP : undefined,
|
||||
right: 12,
|
||||
bottom: 20,
|
||||
zIndex: 10,
|
||||
...style
|
||||
zIndex: 10
|
||||
},
|
||||
animatedStyle
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
testID={testID || notesnook.buttons.add}
|
||||
activeOpacity={0.95}
|
||||
<Pressable
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={color}
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
borderRadius: size === "small" ? 15 : 20,
|
||||
borderTopWidth: 0,
|
||||
borderBottomWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderRightWidth: 0,
|
||||
backgroundColor: colors.primary.background
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -146,29 +129,23 @@ const FloatingButton = ({
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: normalize(size === "small" ? 40 : 60),
|
||||
width: normalize(size === "small" ? 40 : 60),
|
||||
backgroundColor: color
|
||||
? RGB_Linear_Shade(0.87, hexToRGBA(color))
|
||||
: colors.primary.shade,
|
||||
borderRadius: size === "small" ? 15 : 20
|
||||
height: normalize(60),
|
||||
width: normalize(60)
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
icon
|
||||
? icon
|
||||
: route.name === "Notebooks"
|
||||
route.name === "Notebooks"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
}
|
||||
color={color || colors.primary.accent}
|
||||
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}
|
||||
color={colors.primary.accentForeground}
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,8 +23,6 @@ import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { ColorValues } from "../../utils/colors";
|
||||
import { hexToRGBA } from "../../utils/colors";
|
||||
import { defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -42,7 +40,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{hasAnnoucements ? (
|
||||
@@ -53,7 +51,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
borderRadius: 10,
|
||||
marginBottom: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -61,7 +59,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: 150,
|
||||
height: 20,
|
||||
backgroundColor: colors.primary.hover,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
@@ -70,7 +68,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: 250,
|
||||
height: 14,
|
||||
backgroundColor: colors.primary.hover,
|
||||
borderRadius: defaultBorderRadius
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -80,7 +78,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
height: 15,
|
||||
backgroundColor: shadeColor,
|
||||
borderRadius: 3,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -95,7 +93,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
marginBottom: 20,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -113,7 +111,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: 150,
|
||||
height: 12,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
@@ -122,7 +120,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: 250,
|
||||
height: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -136,7 +134,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 10,
|
||||
marginBottom: 20,
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
padding: 5,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
@@ -181,7 +179,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: 200,
|
||||
height: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
@@ -189,15 +187,15 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: "85%",
|
||||
height: 13,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
borderRadius: 5,
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -205,7 +203,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: 50,
|
||||
height: 10,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
@@ -213,7 +211,7 @@ export const DefaultPlaceholder = ({ color }: { color: string }) => {
|
||||
width: 60,
|
||||
height: 10,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
marginLeft: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -17,10 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import { ViewProps } from "react-native";
|
||||
import Animated, { FadeOutUp } from "react-native-reanimated";
|
||||
import { useDelayLayout } from "../../hooks/use-delay-layout";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { DefaultPlaceholder } from "./default-placeholder";
|
||||
import { SettingsPlaceholder } from "./settings-placeholder";
|
||||
|
||||
@@ -48,7 +49,8 @@ export default function DelayLayout({
|
||||
const Placeholder = placeholder[props.type || "default"];
|
||||
|
||||
return loading || props.wait ? (
|
||||
<View
|
||||
<Animated.View
|
||||
exiting={animated ? FadeOutUp : undefined}
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
flex: 1,
|
||||
@@ -56,7 +58,7 @@ export default function DelayLayout({
|
||||
}}
|
||||
>
|
||||
<Placeholder color={props.color || colors.primary.accent} />
|
||||
</View>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<>{props.children}</>
|
||||
);
|
||||
|
||||
@@ -20,8 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const SettingsPlaceholder = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -33,7 +31,7 @@ export const SettingsPlaceholder = () => {
|
||||
width: 100,
|
||||
height: 12,
|
||||
backgroundColor: colors.primary.shade,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
marginLeft: 12,
|
||||
marginBottom: 12
|
||||
}}
|
||||
@@ -46,7 +44,7 @@ export const SettingsPlaceholder = () => {
|
||||
marginBottom: 20,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 16
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -64,7 +62,7 @@ export const SettingsPlaceholder = () => {
|
||||
width: 150,
|
||||
height: 12,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
@@ -73,7 +71,7 @@ export const SettingsPlaceholder = () => {
|
||||
width: 250,
|
||||
height: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -87,7 +85,7 @@ export const SettingsPlaceholder = () => {
|
||||
marginBottom: 20,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 16,
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
@@ -106,7 +104,7 @@ export const SettingsPlaceholder = () => {
|
||||
width: 150,
|
||||
height: 12,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
@@ -115,7 +113,7 @@ export const SettingsPlaceholder = () => {
|
||||
width: 250,
|
||||
height: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -17,7 +17,6 @@ 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 { ScopedThemeProvider } from "@notesnook/theme";
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
@@ -31,12 +30,15 @@ import {
|
||||
} from "react-native";
|
||||
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { BouncingView } from "../ui/transitions/bouncing-view";
|
||||
import { ScopedThemeProvider } from "@notesnook/theme";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
|
||||
export interface BaseDialogProps extends PropsWithChildren {
|
||||
animation?: "fade" | "none" | "slide" | undefined;
|
||||
visible?: boolean;
|
||||
visible: boolean;
|
||||
onRequestClose?: () => void;
|
||||
onShow?: () => void;
|
||||
premium?: boolean;
|
||||
@@ -73,9 +75,9 @@ const BaseDialog = ({
|
||||
background
|
||||
}: BaseDialogProps) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const appState = useAppState();
|
||||
const lockEvents = useRef(false);
|
||||
const [internalVisible, setIntervalVisible] = useState(true);
|
||||
const locked = useUserStore((state) => state.appLocked);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -84,20 +86,26 @@ const BaseDialog = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (locked) {
|
||||
setIntervalVisible(false);
|
||||
lockEvents.current = true;
|
||||
const unsub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked) {
|
||||
setIntervalVisible(true);
|
||||
unsub();
|
||||
setTimeout(() => {
|
||||
lockEvents.current = false;
|
||||
if (useUserStore.getState().disableAppLockRequests) return;
|
||||
|
||||
if (SettingsService.canLockAppInBackground()) {
|
||||
if (appState === "background") {
|
||||
setIntervalVisible(false);
|
||||
if (useUserStore.getState().appLocked) {
|
||||
lockEvents.current = true;
|
||||
const unsub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked) {
|
||||
setIntervalVisible(true);
|
||||
unsub();
|
||||
setTimeout(() => {
|
||||
lockEvents.current = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [locked]);
|
||||
}, [appState]);
|
||||
|
||||
const Wrapper = useSafeArea ? SafeAreaView : View;
|
||||
|
||||
|
||||
@@ -17,17 +17,16 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const DialogButtons = ({
|
||||
onPressPositive,
|
||||
@@ -47,7 +46,9 @@ const DialogButtons = ({
|
||||
{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 60,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomRightRadius: 5,
|
||||
borderBottomLeftRadius: 5,
|
||||
paddingHorizontal: 12,
|
||||
borderTopWidth: 0.7,
|
||||
borderTopColor: getColorLinearShade(
|
||||
colors.secondary.background,
|
||||
@@ -57,7 +58,9 @@ const DialogButtons = ({
|
||||
}
|
||||
]}
|
||||
>
|
||||
{doneText ? (
|
||||
{loading ? (
|
||||
<ActivityIndicator color={colors.primary.accent} size={SIZE.lg} />
|
||||
) : doneText ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -67,7 +70,7 @@ const DialogButtons = ({
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
name="check-circle-outline"
|
||||
size={AppFontSize.md}
|
||||
size={SIZE.md}
|
||||
/>
|
||||
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
|
||||
</View>
|
||||
@@ -83,7 +86,7 @@ const DialogButtons = ({
|
||||
>
|
||||
<Button
|
||||
onPress={onPressNegative}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontSize={SIZE.md}
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="plain"
|
||||
bold
|
||||
@@ -92,12 +95,11 @@ const DialogButtons = ({
|
||||
{onPressPositive ? (
|
||||
<Button
|
||||
onPress={onPressPositive}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontSize={SIZE.md}
|
||||
testID={notesnook.ids.default.dialog.yes}
|
||||
style={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
loading={loading}
|
||||
bold
|
||||
type={positiveType || "transparent"}
|
||||
title={positiveTitle}
|
||||
@@ -115,6 +117,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,12 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { Text, View, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableProps } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
type DialogHeaderProps = {
|
||||
icon?: string;
|
||||
@@ -83,7 +82,7 @@ const DialogHeader = ({
|
||||
>
|
||||
<Heading
|
||||
style={{ textAlign: centered ? "center" : "left" }}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{title}{" "}
|
||||
{titlePart ? (
|
||||
@@ -98,7 +97,7 @@ const DialogHeader = ({
|
||||
onPress={button.onPress}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
loading={button.loading}
|
||||
fontSize={13}
|
||||
|
||||
@@ -17,35 +17,33 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
|
||||
import { defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import BaseDialog from "./base-dialog";
|
||||
import DialogButtons from "./dialog-buttons";
|
||||
import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const values = useRef({
|
||||
inputValue: undefined
|
||||
});
|
||||
@@ -74,26 +72,16 @@ export const Dialog = ({ context = "global" }) => {
|
||||
const onPressPositive = async () => {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
setLoading(true);
|
||||
let result;
|
||||
try {
|
||||
result = await dialogInfo.positivePress(
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
checked
|
||||
);
|
||||
} catch (e) {
|
||||
/** Empty */
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
let result = await dialogInfo.positivePress(
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
checked
|
||||
);
|
||||
if (result === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setVisible(false);
|
||||
hide();
|
||||
};
|
||||
|
||||
const show = useCallback(
|
||||
@@ -116,14 +104,13 @@ export const Dialog = ({ context = "global" }) => {
|
||||
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
};
|
||||
}, [hide, show]);
|
||||
}, [show]);
|
||||
|
||||
const hide = React.useCallback(() => {
|
||||
const hide = () => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setVisible(false);
|
||||
dialogInfo.onClose?.();
|
||||
}, [dialogInfo]);
|
||||
};
|
||||
|
||||
const onNegativePress = async () => {
|
||||
if (dialogInfo.onClose) {
|
||||
@@ -136,11 +123,10 @@ export const Dialog = ({ context = "global" }) => {
|
||||
...getElevationStyle(5),
|
||||
width: DDS.isTab ? 400 : "85%",
|
||||
maxHeight: 450,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
...getContainerBorder(colors.primary.border, 0.5),
|
||||
overflow: "hidden"
|
||||
...getContainerBorder(colors.primary.border, 0.5)
|
||||
};
|
||||
|
||||
return visible ? (
|
||||
@@ -176,7 +162,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
{dialogInfo.input ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -200,7 +186,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
@@ -221,7 +207,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
iconColor={checked ? colors.secondary.icon : colors.primary.icon}
|
||||
style={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
@@ -237,7 +222,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
loading={loading}
|
||||
positiveTitle={dialogInfo.positiveText}
|
||||
negativeTitle={dialogInfo.negativeText}
|
||||
positiveType={dialogInfo.positiveType}
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
eCloseAppLocKPasswordDailog,
|
||||
eOpenAppLockPasswordDialog
|
||||
} from "../../../utils/events";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
@@ -50,7 +50,6 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export const AppLockPassword = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -129,7 +128,7 @@ export const AppLockPassword = () => {
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{mode === "change" ? (
|
||||
@@ -193,7 +192,7 @@ export const AppLockPassword = () => {
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -249,7 +248,7 @@ export const AppLockPassword = () => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
iconSize={AppFontSize.lg}
|
||||
iconSize={SIZE.lg}
|
||||
type="plain"
|
||||
iconColor={
|
||||
accountPass ? colors.primary.accent : colors.primary.icon
|
||||
@@ -260,7 +259,7 @@ export const AppLockPassword = () => {
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
height: 30
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -27,13 +27,12 @@ import {
|
||||
import { Image as ImageType } from "react-native-image-crop-picker";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Notice } from "../../ui/notice";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export default function AttachImage({
|
||||
response,
|
||||
@@ -51,26 +50,21 @@ export default function AttachImage({
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginBottom: 12,
|
||||
height: 140,
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
padding: 5,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.primary.paragraph,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
>
|
||||
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
|
||||
{strings.attachImageHeading(response?.length || 1)}
|
||||
</Paragraph>
|
||||
<ScrollView horizontal>
|
||||
@@ -86,7 +80,7 @@ export default function AttachImage({
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
backgroundColor: "black",
|
||||
marginRight: 6
|
||||
}}
|
||||
@@ -102,7 +96,7 @@ export default function AttachImage({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginBottom: 12,
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
@@ -111,7 +105,7 @@ export default function AttachImage({
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
name={compress ? "checkbox-marked" : "checkbox-blank-outline"}
|
||||
color={compress ? colors.primary.accent : colors.primary.icon}
|
||||
style={{
|
||||
@@ -128,7 +122,7 @@ export default function AttachImage({
|
||||
flexShrink: 1,
|
||||
marginLeft: 3
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{strings.compress()} ({strings.recommended().toLowerCase()})
|
||||
</Paragraph>
|
||||
@@ -141,7 +135,7 @@ export default function AttachImage({
|
||||
size="small"
|
||||
style={{
|
||||
width: "100%",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
marginBottom: 12
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -151,7 +145,7 @@ export default function AttachImage({
|
||||
size="small"
|
||||
style={{
|
||||
width: "100%",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
marginBottom: 12
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -186,7 +180,7 @@ AttachImage.present = (response: ImageType[], context?: string) => {
|
||||
close={close}
|
||||
onAttach={(result) => {
|
||||
resolved = true;
|
||||
|
||||
console.log("closing");
|
||||
resolve(result);
|
||||
close?.();
|
||||
}}
|
||||
|
||||
@@ -34,7 +34,6 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const HEX_COLOR_REGEX_ALPHA =
|
||||
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
|
||||
@@ -106,16 +105,16 @@ const ColorPicker = ({
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
columnGap: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingTop: 10,
|
||||
columnGap: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -156,7 +155,7 @@ const ColorPicker = ({
|
||||
<Button
|
||||
title={strings.addColor()}
|
||||
style={{
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
marginBottom: 10
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!selectedColor)
|
||||
@@ -180,7 +179,6 @@ const ColorPicker = ({
|
||||
title: title.current,
|
||||
colorCode: selectedColor
|
||||
});
|
||||
if (!id) return;
|
||||
useRelationStore.getState().update();
|
||||
useMenuStore.getState().setColorNotes();
|
||||
setVisible(false);
|
||||
|
||||
@@ -39,11 +39,10 @@ import {
|
||||
eOpenJumpToDialog,
|
||||
eScrollEvent
|
||||
} from "../../../utils/events";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const JumpToSectionDialog = () => {
|
||||
const scrollRef = useRef<RefObject<FlatList>>();
|
||||
@@ -153,13 +152,13 @@ const JumpToSectionDialog = () => {
|
||||
maxHeight: "65%",
|
||||
borderRadius: 10,
|
||||
alignSelf: "center",
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
padding: 10,
|
||||
paddingTop: 30
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
color={colors.primary.accent}
|
||||
style={{
|
||||
marginBottom: 20
|
||||
@@ -189,15 +188,15 @@ const JumpToSectionDialog = () => {
|
||||
style={{
|
||||
minWidth: "20%",
|
||||
width: null,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
margin: 5,
|
||||
borderRadius: 100,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
height: 30,
|
||||
marginVertical: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
size={SIZE.sm}
|
||||
color={
|
||||
currentIndex === index
|
||||
? colors.static.white
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from "../../../services/event-manager";
|
||||
import { useState } from "react";
|
||||
import { eCloseLoading, eOpenLoading } from "../../../utils/events";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export const LoadingDialog = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -68,7 +67,7 @@ export const LoadingDialog = () => {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: DefaultAppStyles.GAP
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
|
||||
@@ -19,7 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Dimensions, TextInput, View } from "react-native";
|
||||
import Orientation from "react-native-orientation-locker";
|
||||
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";
|
||||
@@ -31,7 +34,7 @@ import {
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
@@ -43,7 +46,6 @@ import { sleep } from "../../../utils/time";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
@@ -96,9 +98,9 @@ const PDFPreview = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Orientation.addDeviceOrientationListener(onOrientationChange);
|
||||
addOrientationListener(onOrientationChange);
|
||||
return () => {
|
||||
Orientation.removeDeviceOrientationListener(onOrientationChange);
|
||||
removeOrientationListener(onOrientationChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -182,7 +184,7 @@ const PDFPreview = () => {
|
||||
/>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}}
|
||||
color={colors.static.white}
|
||||
>
|
||||
@@ -198,7 +200,7 @@ const PDFPreview = () => {
|
||||
marginTop: insets.top,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
paddingLeft: 6
|
||||
}}
|
||||
>
|
||||
@@ -214,7 +216,7 @@ const PDFPreview = () => {
|
||||
style={{
|
||||
marginRight: 12
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -305,7 +307,9 @@ const PDFPreview = () => {
|
||||
password={password}
|
||||
maxScale={6}
|
||||
onError={onError}
|
||||
onPressLink={(uri) => {}}
|
||||
onPressLink={(uri) => {
|
||||
console.log(`Link pressed: ${uri}`);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
@@ -29,7 +29,6 @@ import { Button } from "../../ui/button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export type ProgressOptions = {
|
||||
progress?: string;
|
||||
@@ -63,7 +62,7 @@ export default function Progress() {
|
||||
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
|
||||
console.log("options", options.fillBackground);
|
||||
setData({
|
||||
title: options.title,
|
||||
paragraph: options.paragraph,
|
||||
@@ -112,7 +111,7 @@ export default function Progress() {
|
||||
>
|
||||
<DialogContainer
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
noBorder={data?.fillBackground ? true : false}
|
||||
@@ -122,7 +121,7 @@ export default function Progress() {
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
gap: 10,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
@@ -132,7 +131,7 @@ export default function Progress() {
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{data?.title}
|
||||
</Heading>
|
||||
@@ -149,7 +148,7 @@ export default function Progress() {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
|
||||
@@ -27,14 +27,13 @@ import {
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { eCloseResultDialog, eOpenResultDialog } from "../../../utils/events";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { ProFeatures } from "./pro-features";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const ResultDialog = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -79,15 +78,15 @@ const ResultDialog = () => {
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
textBreakStrategy="balanced"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 10,
|
||||
maxWidth: "100%",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{dialogData.title}
|
||||
@@ -95,12 +94,12 @@ const ResultDialog = () => {
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.md}
|
||||
size={SIZE.md}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
maxWidth: "80%",
|
||||
lineHeight: AppFontSize.sm + 5
|
||||
lineHeight: SIZE.sm + 5
|
||||
}}
|
||||
>
|
||||
{dialogData.paragraph}
|
||||
@@ -110,7 +109,7 @@ const ResultDialog = () => {
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: 12,
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
@@ -125,17 +124,17 @@ const ResultDialog = () => {
|
||||
width: "100%",
|
||||
borderBottomRightRadius: 10,
|
||||
borderBottomLeftRadius: 10,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingVertical: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={dialogData.button}
|
||||
width={null}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
onPress={close}
|
||||
fontSize={AppFontSize.md + 2}
|
||||
fontSize={SIZE.md + 2}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -27,10 +27,9 @@ import {
|
||||
eCloseResultDialog,
|
||||
eOpenPremiumDialog
|
||||
} from "../../../utils/events";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
export const ProFeatures = ({ count = 6 }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
@@ -68,17 +67,13 @@ export const ProFeatures = ({ count = 6 }) => {
|
||||
width: "100%",
|
||||
height: 40,
|
||||
paddingHorizontal: 0,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginBottom: 10,
|
||||
alignItems: "center",
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: 5,
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.accent}
|
||||
name="check"
|
||||
/>
|
||||
<Icon size={SIZE.lg} color={colors.primary.accent} name="check" />
|
||||
<Paragraph style={{ marginLeft: 5, flexShrink: 1 }}>
|
||||
{item.content}
|
||||
</Paragraph>
|
||||
@@ -91,7 +86,7 @@ export const ProFeatures = ({ count = 6 }) => {
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
}}
|
||||
size={AppFontSize.xs}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.secondary.paragraph
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
eUpdateNoteInEditor
|
||||
} from "../../../utils/events";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
import { convertNoteToText } from "../../../utils/note-to-text";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
@@ -53,7 +53,6 @@ import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export class VaultDialog extends Component {
|
||||
constructor(props) {
|
||||
@@ -337,7 +336,7 @@ export class VaultDialog extends Component {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("VAULT UPDATED EVENT");
|
||||
eSendEvent("vaultUpdated");
|
||||
this.setState({
|
||||
loading: false
|
||||
@@ -407,6 +406,7 @@ export class VaultDialog extends Component {
|
||||
} else {
|
||||
await db.vault.add(this.state.note.id);
|
||||
|
||||
console.log("update note event...");
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
|
||||
this.close();
|
||||
@@ -460,7 +460,7 @@ export class VaultDialog extends Component {
|
||||
async _deleteNote() {
|
||||
try {
|
||||
await db.vault.remove(this.state.note.id, this.password);
|
||||
await deleteItems("note", [this.state.note.id]);
|
||||
await deleteItems([this.state.note.id], "note");
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
@@ -543,6 +543,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("Error", e);
|
||||
this._takeErrorAction(e);
|
||||
});
|
||||
}
|
||||
@@ -554,7 +555,7 @@ export class VaultDialog extends Component {
|
||||
item: note
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
tabBarRef.current?.goToPage(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -678,7 +679,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{(novault ||
|
||||
@@ -751,7 +752,7 @@ export class VaultDialog extends Component {
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}}
|
||||
width="100%"
|
||||
title={strings.deleteAllNotes()}
|
||||
@@ -848,7 +849,7 @@ export class VaultDialog extends Component {
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: 10
|
||||
}}
|
||||
icon="fingerprint"
|
||||
width="100%"
|
||||
|
||||
@@ -78,10 +78,10 @@ class ExceptionHandler extends React.Component<{
|
||||
}
|
||||
|
||||
export const withErrorBoundry = (Element: React.ElementType, name: string) => {
|
||||
return function ErrorBoundary(props: any) {
|
||||
return function ErrorBoundary() {
|
||||
return (
|
||||
<ExceptionHandler component={name}>
|
||||
<Element {...props} />
|
||||
<Element />
|
||||
</ExceptionHandler>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,52 +17,60 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { RouteName } from "../../stores/use-navigation-store";
|
||||
import useNavigationStore, {
|
||||
RouteName
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { IconButtonProps } from "../ui/icon-button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LeftMenus } from "./left-menus";
|
||||
import { RightMenus } from "./right-menus";
|
||||
import { Title } from "./title";
|
||||
|
||||
type HeaderRightButton = {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
renderedInRoute,
|
||||
onLeftMenuButtonPress,
|
||||
title,
|
||||
titleHiddenOnRender,
|
||||
headerRightButtons,
|
||||
id,
|
||||
accentColor,
|
||||
isBeta,
|
||||
canGoBack,
|
||||
onPressDefaultRightButton,
|
||||
hasSearch,
|
||||
onSearch,
|
||||
rightButton
|
||||
onSearch
|
||||
}: {
|
||||
onLeftMenuButtonPress?: () => void;
|
||||
renderedInRoute?: RouteName;
|
||||
renderedInRoute: RouteName;
|
||||
id?: string;
|
||||
title: string;
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
titleHiddenOnRender?: boolean;
|
||||
accentColor?: string;
|
||||
isBeta?: boolean;
|
||||
canGoBack?: boolean;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
rightButton?: IconButtonProps;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
const [selectedItemsList, selectionMode] = useSelectionStore((state) => [
|
||||
state.selectedItemsList,
|
||||
state.selectionMode
|
||||
]);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||
@@ -85,48 +93,88 @@ export const Header = ({
|
||||
};
|
||||
}, [borderHidden, onScroll]);
|
||||
|
||||
const HeaderWrapper = hasSearch ? Pressable : View;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<HeaderWrapper
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: DefaultAppStyles.GAP_SMALL,
|
||||
borderRadius: 10,
|
||||
paddingVertical: 3,
|
||||
borderWidth: hasSearch ? 1 : 0,
|
||||
borderColor: colors.primary.border,
|
||||
paddingHorizontal: !hasSearch ? 0 : DefaultAppStyles.GAP_SMALL,
|
||||
alignItems: "center"
|
||||
}}
|
||||
testID="search-header"
|
||||
onPress={() => {
|
||||
onSearch?.();
|
||||
}}
|
||||
return selectionMode && isFocused ? null : (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
marginTop: Platform.OS === "android" ? insets.top : null,
|
||||
backgroundColor: colors.primary.background,
|
||||
overflow: "hidden",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: borderHidden
|
||||
? "transparent"
|
||||
: colors.secondary.background,
|
||||
justifyContent: "space-between"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<LeftMenus
|
||||
canGoBack={canGoBack}
|
||||
onLeftButtonPress={onLeftMenuButtonPress}
|
||||
/>
|
||||
<>
|
||||
<View style={styles.leftBtnContainer}>
|
||||
<LeftMenus
|
||||
canGoBack={canGoBack}
|
||||
onLeftButtonPress={onLeftMenuButtonPress}
|
||||
/>
|
||||
|
||||
{hasSearch ? (
|
||||
<Paragraph>
|
||||
{selectionMode
|
||||
? `${selectedItemsList.length} selected`
|
||||
: strings.searchInRoute(title)}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Heading size={AppFontSize.lg}>{title}</Heading>
|
||||
)}
|
||||
|
||||
<RightMenus rightButton={rightButton} />
|
||||
</HeaderWrapper>
|
||||
</View>
|
||||
<Title
|
||||
isHiddenOnRender={titleHiddenOnRender}
|
||||
renderedInRoute={renderedInRoute}
|
||||
id={id}
|
||||
accentColor={accentColor}
|
||||
title={title}
|
||||
isBeta={isBeta}
|
||||
/>
|
||||
</View>
|
||||
<RightMenus
|
||||
renderedInRoute={renderedInRoute}
|
||||
id={id}
|
||||
headerRightButtons={headerRightButtons}
|
||||
onPressDefaultRightButton={onPressDefaultRightButton}
|
||||
search={hasSearch}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
zIndex: 11,
|
||||
height: 50,
|
||||
maxHeight: 50,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%"
|
||||
},
|
||||
leftBtnContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
},
|
||||
leftBtn: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginLeft: -5,
|
||||
marginRight: 25
|
||||
},
|
||||
rightBtnContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
},
|
||||
rightBtn: {
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-end",
|
||||
height: 40,
|
||||
width: 40,
|
||||
paddingRight: 0
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const LeftMenus = ({
|
||||
@@ -40,7 +41,7 @@ export const LeftMenus = ({
|
||||
if (onLeftButtonPress) return onLeftButtonPress();
|
||||
|
||||
if (!canGoBack) {
|
||||
if (fluidTabsRef.current?.isDrawerOpen()) {
|
||||
if (tabBarRef.current?.isDrawerOpen()) {
|
||||
Navigation.closeDrawer();
|
||||
} else {
|
||||
Navigation.openDrawer();
|
||||
@@ -53,14 +54,23 @@ export const LeftMenus = ({
|
||||
return isTablet && !canGoBack ? null : (
|
||||
<IconButton
|
||||
testID={notesnook.ids.default.header.buttons.left}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: DDS.isLargeTablet() ? 10 : 7
|
||||
}}
|
||||
left={40}
|
||||
top={40}
|
||||
right={DDS.isLargeTablet() ? 10 : 10}
|
||||
onPress={_onLeftButtonPress}
|
||||
onLongPress={() => {
|
||||
Navigation.popToTop();
|
||||
}}
|
||||
name={canGoBack ? "arrow-left" : "menu"}
|
||||
color={colors.primary.icon}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,30 +17,127 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { IconButton, IconButtonProps } from "../ui/icon-button";
|
||||
import { Menu } from "react-native-material-menu";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import {
|
||||
HeaderRightButton,
|
||||
RouteName
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const RightMenus = ({
|
||||
rightButton
|
||||
headerRightButtons,
|
||||
renderedInRoute,
|
||||
id,
|
||||
onPressDefaultRightButton,
|
||||
search,
|
||||
onSearch
|
||||
}: {
|
||||
rightButton?: IconButtonProps;
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
renderedInRoute: RouteName;
|
||||
id?: string;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
search?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { colors: contextMenuColors } = useThemeColors("contextMenu");
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const menuRef = useRef<Menu>(null);
|
||||
|
||||
return (
|
||||
<View style={styles.rightBtnContainer}>
|
||||
{rightButton ? (
|
||||
<IconButton {...rightButton} color={colors.primary.icon} />
|
||||
) : (
|
||||
<View
|
||||
{search ? (
|
||||
<IconButton
|
||||
onPress={onSearch}
|
||||
testID="icon-search"
|
||||
name="magnify"
|
||||
color={colors.primary.paragraph}
|
||||
style={styles.rightBtn}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{deviceMode !== "mobile" ? (
|
||||
<Button
|
||||
onPress={onPressDefaultRightButton}
|
||||
testID={notesnook.ids.default.addBtn}
|
||||
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
|
||||
iconSize={SIZE.xl}
|
||||
type="accent"
|
||||
hitSlop={{
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
left: 0
|
||||
}}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40
|
||||
marginLeft: 10,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.accent
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{headerRightButtons && headerRightButtons.length > 0 ? (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
animationDuration={200}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: contextMenuColors.primary.background,
|
||||
marginTop: 35
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
}}
|
||||
anchor={
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.show();
|
||||
}}
|
||||
name="dots-vertical"
|
||||
color={colors.primary.paragraph}
|
||||
style={styles.rightBtn}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{headerRightButtons.map((item) => (
|
||||
<Button
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 0,
|
||||
alignSelf: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
type="plain"
|
||||
buttonType={{
|
||||
text: contextMenuColors.primary.paragraph
|
||||
}}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
if (Platform.OS === "ios") await sleep(300);
|
||||
item.onPress();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -52,6 +149,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
rightBtn: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginLeft: 10,
|
||||
paddingRight: 0
|
||||
}
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Tag from "../ui/tag";
|
||||
import Heading from "../ui/typography/heading";
|
||||
|
||||
@@ -41,7 +41,7 @@ export const Title = ({
|
||||
isHiddenOnRender?: boolean;
|
||||
accentColor?: string;
|
||||
isBeta?: boolean;
|
||||
renderedInRoute?: string;
|
||||
renderedInRoute: string;
|
||||
id?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -75,7 +75,7 @@ export const Title = ({
|
||||
{!visible ? (
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
size={AppFontSize.lg}
|
||||
size={SIZE.lg}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginTop: Platform.OS === "ios" ? -1 : 0
|
||||
@@ -83,7 +83,7 @@ export const Title = ({
|
||||
color={accentColor || colors.primary.heading}
|
||||
>
|
||||
{isTag ? (
|
||||
<Heading size={AppFontSize.xl} color={colors.primary.accent}>
|
||||
<Heading size={SIZE.xl} color={colors.primary.accent}>
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { DataURL, getFileNameWithExtension } from "@notesnook/core";
|
||||
import { DataURL } from "@notesnook/core";
|
||||
import type { ImageAttributes } from "@notesnook/editor";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
@@ -34,12 +34,6 @@ import {
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import Share from "react-native-share";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const ImagePreview = () => {
|
||||
const { colors } = useThemeColors("dialog");
|
||||
|
||||
@@ -47,8 +41,6 @@ const ImagePreview = () => {
|
||||
const [image, setImage] = useState<string>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const imageRef = useRef<ImageAttributes>();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [showHeader, setShowHeader] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("ImagePreview", open);
|
||||
@@ -81,86 +73,16 @@ const ImagePreview = () => {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
|
||||
if (!uri) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
const path = `${cacheDir}/${
|
||||
"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
|
||||
}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
setImage("file://" + path);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
image &&
|
||||
RNFetchBlob.fs.unlink(image.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
const close = () => {
|
||||
setImage(undefined);
|
||||
setVisible(false);
|
||||
}, [image]);
|
||||
|
||||
const renderHeader = React.useCallback(
|
||||
() => (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: insets.top,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
display: showHeader ? "flex" : "none"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
height: 50,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="share"
|
||||
color="white"
|
||||
style={{
|
||||
borderWidth: 0
|
||||
}}
|
||||
onPress={async () => {
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
await Share.open({
|
||||
url: image
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(false);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
name="close"
|
||||
color="white"
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
[close, image, insets.top, showHeader]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
visible && (
|
||||
@@ -168,7 +90,6 @@ const ImagePreview = () => {
|
||||
background="black"
|
||||
animation="slide"
|
||||
visible={true}
|
||||
useSafeArea={false}
|
||||
onRequestClose={close}
|
||||
transparent
|
||||
>
|
||||
@@ -209,25 +130,43 @@ const ImagePreview = () => {
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ImageViewer
|
||||
enableImageZoom={true}
|
||||
renderIndicator={() => <></>}
|
||||
enableSwipeDown
|
||||
useNativeDriver
|
||||
onSwipeDown={close}
|
||||
saveToLocalByLongPress={false}
|
||||
onClick={() => {
|
||||
setShowHeader(!showHeader);
|
||||
}}
|
||||
renderHeader={renderHeader}
|
||||
imageUrls={[
|
||||
{
|
||||
url: image as string
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
<ImageViewer
|
||||
enableImageZoom={true}
|
||||
renderIndicator={() => <></>}
|
||||
enableSwipeDown
|
||||
useNativeDriver
|
||||
onSwipeDown={close}
|
||||
saveToLocalByLongPress={false}
|
||||
renderHeader={() => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
height: 50,
|
||||
paddingHorizontal: 24,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
marginTop: Platform.OS === "android" ? 30 : 0
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="close"
|
||||
color="white"
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
imageUrls={[
|
||||
{
|
||||
url: image as string
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -23,28 +23,31 @@ import React from "react";
|
||||
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eOpenLoginDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AuthMode } from "../auth";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const Intro = () => {
|
||||
const Intro = ({ navigation }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const isTablet = width > 600;
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item }: { item: (typeof strings.introData)[0] }) => (
|
||||
({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
width: isTablet ? width / 2 : width,
|
||||
paddingHorizontal: isTablet ? (width / 2) * 0.05 : width * 0.05
|
||||
width: deviceMode !== "mobile" ? width / 2 : width,
|
||||
paddingHorizontal:
|
||||
deviceMode !== "mobile" ? (width / 2) * 0.05 : width * 0.05
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -73,7 +76,7 @@ const Intro = () => {
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 10,
|
||||
maxWidth: "90%",
|
||||
width: "100%"
|
||||
}}
|
||||
@@ -85,21 +88,21 @@ const Intro = () => {
|
||||
marginBottom: 5
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{heading()}
|
||||
</Heading>
|
||||
))}
|
||||
|
||||
{item.body ? (
|
||||
<Paragraph size={AppFontSize.sm}>{item.body()}</Paragraph>
|
||||
<Paragraph size={SIZE.sm}>{item.body()}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.tesimonial ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
fontSize: AppFontSize.lg
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(item.link);
|
||||
@@ -111,37 +114,32 @@ const Intro = () => {
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
[colors.primary.accent, colors.secondary.background, isTablet, width]
|
||||
[colors.primary.accent, colors.secondary.background, deviceMode, width]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
testID="notesnook.splashscreen"
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7 - (insets.top + insets.bottom)
|
||||
},
|
||||
isTablet && {
|
||||
width: width / 2,
|
||||
alignSelf: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 20,
|
||||
marginTop: 50
|
||||
}
|
||||
]}
|
||||
style={{
|
||||
width: deviceMode !== "mobile" ? width / 2 : "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
autoplay
|
||||
@@ -173,12 +171,23 @@ const Intro = () => {
|
||||
<Button
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
SettingsService.set({ introCompleted: true });
|
||||
Navigation.push("Auth", {
|
||||
mode: AuthMode.welcomeSignup
|
||||
});
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
}, 1000);
|
||||
}}
|
||||
fontSize={AppFontSize.md}
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "center",
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={strings.getStarted()}
|
||||
/>
|
||||
125
apps/mobile/app/components/intro/welcome.js
Normal file
125
apps/mobile/app/components/intro/welcome.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user