Compare commits

..

35 Commits

Author SHA1 Message Date
Ammar Ahmed
f545216dc3 ci: fix artifacts location 2024-11-13 09:31:37 +05:00
Ammar Ahmed
989de21e44 ci: generate debug.keystore 2024-11-13 09:26:12 +05:00
Ammar Ahmed
2c051f9d21 ci: disable gradle caching 2024-11-13 02:18:16 +05:00
Ammar Ahmed
4845d40c0b ci: push 2024-11-13 01:05:52 +05:00
Ammar Ahmed
b4615fa9aa ci: enable build 2024-11-12 22:51:53 +05:00
Ammar Ahmed
f24ff63c06 ci: disable more steps 2024-11-12 22:42:39 +05:00
Ammar Ahmed
7518e6badc ci: skip build 2024-11-12 22:33:21 +05:00
Ammar Ahmed
0fcbdbcdc3 ci: fix ci
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 21:57:07 +05:00
Ammar Ahmed
999856311f ci: free disk space before build 2024-11-12 20:04:26 +05:00
Ammar Ahmed
98f4e08c62 ci: fix 2024-11-12 18:50:05 +05:00
Ammar Ahmed
f3140cbd8f ci: fix 2024-11-12 18:00:00 +05:00
Ammar Ahmed
029cadedc4 ci: run command from root
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 16:46:19 +05:00
Ammar Ahmed
6cdeb48e56 ci: do not clean after build
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 16:05:41 +05:00
Ammar Ahmed
2c8abc4d00 ci: fix working dir path
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 15:11:16 +05:00
Ammar Ahmed
04820f6500 ci: update workflow
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 14:15:42 +05:00
Ammar Ahmed
fc6a843a1a ci: fix action 2024-11-12 14:14:05 +05:00
Ammar Ahmed
129dad06c2 ci: format 2024-11-12 14:12:45 +05:00
Ammar Ahmed
063b387807 ci: install detox-cli 2024-11-12 14:11:07 +05:00
Ammar Ahmed
e0058fe30b ci: change script working dir 2024-11-12 13:48:11 +05:00
Ammar Ahmed
a3c297636e ci: clean runner after build
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 13:10:09 +05:00
Ammar Ahmed
afc534806d ci: clean gh action space 2024-11-12 11:08:43 +05:00
Ammar Ahmed
4456cc2184 mobile: fix intl imports 2024-11-12 10:08:26 +05:00
Ammar Ahmed
2610db90d2 mobile: fix ts errors 2024-11-12 09:54:17 +05:00
Ammar Ahmed
f16c9804ee ci: fix build 2024-11-11 22:48:01 +05:00
Ammar Ahmed
7a3a1aa9c2 ci: update 2024-11-11 19:53:01 +05:00
Ammar Ahmed
57d0904bee mobile: add e2e test workflow 2024-11-11 19:25:56 +05:00
Ammar Ahmed
5ca6f98c6c mobile: fix e2e tests 2024-11-11 18:46:08 +05:00
Abdullah Atta
dbe32e8af6 web: fix more failing tests 2024-11-08 17:06:38 +05:00
Abdullah Atta
e0758bf353 web: fix note & notebook tests 2024-11-08 17:06:38 +05:00
Abdullah Atta
3c1b5d1ee8 intl: update po files 2024-11-08 17:06:38 +05:00
Abdullah Atta
074008ac67 web: localize more missing strings 2024-11-08 17:06:38 +05:00
Ammar Ahmed
7c56a6beb3 mobile: cleanup and fix errors 2024-11-08 17:06:38 +05:00
Abdullah Atta
3e9d7a70d0 web: localize missing strings 2024-11-08 17:06:38 +05:00
Abdullah Atta
e2ca1a0df6 intl: fix type error in vite.config.mts due to duplicate rollup packages 2024-11-08 17:06:38 +05:00
Abdullah Atta
392456a0d9 global: fix many localization related errors and issues 2024-11-08 17:06:38 +05:00
519 changed files with 63487 additions and 34637 deletions

View File

@@ -10,7 +10,7 @@ const authors = readFileSync("AUTHORS", "utf-8");
const isAuthor = authors.includes(`<${authorEmail}>`);
const SCOPES = [
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
"mobile",
"web",
@@ -36,8 +36,7 @@ const SCOPES = [
"global",
"docs",
"themebuilder",
"intl",
"webclipper"
"intl"
];
module.exports = {

View File

@@ -1,3 +1,4 @@
name: e2e-android
on: workflow_dispatch
@@ -18,7 +19,7 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Free Disk Space
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
@@ -26,7 +27,7 @@ jobs:
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
@@ -45,20 +46,38 @@ jobs:
architecture: "x64"
- name: Install Detox CLI
run: npm install detox-cli --global
run: npm install detox-cli --global
- name: Generate debug.keystore
working-directory: apps/mobile/native/android/app
run: |
keytool -genkey -v -keystore debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "C=US, O=Android, CN=Android Debug"
- name: Detox build
run: |
yarn build:android
ls apps/mobile/native/android/app/build/outputs/apk
ls apps/mobile/native/android/app/build/outputs/apk/release
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Free Disk Space After Build
run: |
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/local/lib/android/sdk/ndk
sudo apt-get clean
echo "Disk space after cleanup:"
df -h
- name: Get device name
id: device
run: |
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
@@ -89,12 +108,13 @@ jobs:
disable-animations: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation: false
script: yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing
script: |
yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing --artifacts-location apps/mobile/native/artifacts
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: detox-artifacts
path: artifacts
path: apps/mobile/native/artifacts
retention-days: 14

View File

@@ -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

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -84,26 +84,6 @@ jobs:
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
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 .
cd -
node scripts/generate-sources.mjs
- name: Upload flatpak sources
uses: softprops/action-gh-release@v1
if: inputs.publish-github && inputs.build-linux
with:
draft: true
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./generated-sources.json
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
build-macos:
name: Build for macOS
needs: build
@@ -112,7 +92,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -156,12 +136,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
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: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
@@ -214,7 +188,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -238,12 +212,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
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: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
@@ -279,7 +247,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
@@ -304,12 +272,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
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: Build
run: npx nx run release --project @notesnook/desktop

View File

@@ -1,233 +0,0 @@
name: Test @notesnook/desktop
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "app/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
test-macos-x64:
name: Test macOS x64
needs: build
runs-on: macos-13
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --mac --dir --x64
working-directory: ./apps/desktop
- name: Run tests x64
run: |
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos-x64
path: apps/desktop/test-results
retention-days: 5
test-macos:
name: Test macOS
needs: build
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --mac --dir --arm64
working-directory: ./apps/desktop
- name: Run tests arm64
run: |
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos
path: apps/desktop/test-results
retention-days: 5
test-linux:
name: Test for Linux
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
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: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --linux --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
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
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-linux
path: apps/desktop/test-results
retention-days: 5
test-windows:
name: Test for Windows
needs: build
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
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: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: |
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-win
path: apps/desktop/test-results
retention-days: 5

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -1,6 +1,6 @@
name: Publish Notesnook Help
on:
on:
workflow_dispatch:
push:
branches:
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-latest
runs-on: macos-13
timeout-minutes: 60
steps:
@@ -89,13 +89,13 @@ jobs:
- name: CCache Stats After Build
run: ccache -sv
# - name: "Upload app to TestFlight"
# uses: apple-actions/upload-testflight-build@v1
# with:
# app-path: Notesnook.ipa
# issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
# api-key-id: ${{ secrets.API_KEY_ID }}
# api-private-key: ${{ secrets.API_KEY }}
- name: "Upload app to TestFlight"
uses: apple-actions/upload-testflight-build@v1
with:
app-path: Notesnook.ipa
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
api-key-id: ${{ secrets.API_KEY_ID }}
api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v4

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -1,44 +0,0 @@
name: Benchmark @notesnook/web
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.benchmarks.yml"
pull_request:
jobs:
bench:
name: Bench
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Build
run: npm run build:web
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: apps/web
- name: Run benchmarks
id: benchmark
run: node __bench__/app.bench.mjs
working-directory: apps/web

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v4

View File

@@ -1,53 +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 "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) => {
const { app, page } = await launchApp();
try {
await page.waitForSelector("#authForm");
assert.ok(
await page.getByRole("button", { name: "Create account" }).isVisible()
);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.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();
}
});

View File

@@ -1,38 +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 { _electron as electron } from "playwright";
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
});
const page = await app.firstWindow();
return { app, page };
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,22 +2,21 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.24",
"version": "3.0.18",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
}
},
@@ -29,29 +28,25 @@
"@notesnook/intl": "file:../../packages/intl",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "11.5.0",
"better-sqlite3-multiple-ciphers": "11.2.1",
"electron-trpc": "0.6.1",
"electron-updater": "^6.3.4",
"icojs": "^0.19.4",
"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",
"@types/node": "22.7.0",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.1",
"electron": "^31.7.4",
"electron-builder": "^25.1.8",
"esbuild": "0.21.5",
"vitest": "2.1.8",
"electron": "^30.5.1",
"electron-builder": "^25.1.7",
"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",
"tree-kill": "^1.2.2",
"undici": "^6.19.8"
},
@@ -62,11 +57,10 @@
"start": "node scripts/dev.mjs",
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "node ../../scripts/build.mjs",
"build": "tsup-node src/index.ts",
"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": "node --test --test-timeout 30000 --test-force-exit"
"postinstall": "patch-package"
},
"author": {
"name": "Streetwriters (Private) Limited",
@@ -95,20 +89,7 @@
"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"
],
@@ -181,12 +162,10 @@
]
}
],
"signtoolOptions": {
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js"
},
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {
@@ -262,4 +241,4 @@
}
]
}
}
}

View File

@@ -18,17 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import fs from "fs/promises";
import { existsSync } from "fs";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
import { fileURLToPath } from "url";
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
);
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
@@ -36,7 +38,7 @@ 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, "..", "..", "..")
);
}
@@ -44,6 +46,12 @@ 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();
// if (os.platform() === "win32")
// await exec(
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
// path.join(__dirname, "..", "node_modules", "sodium-native")
// );
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
@@ -58,13 +66,11 @@ if (args.variant === "mas") {
await exec(`yarn run build`);
if (args.run) {
await exec(`yarn electron-builder --dir --${process.arch}`);
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`);
}
@@ -77,3 +83,21 @@ async function exec(cmd, cwd) {
cwd: cwd || process.cwd()
});
}
async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.2.2";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}

View File

@@ -36,8 +36,6 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
await onChange(true);
console.log("Watching...");
@@ -55,8 +53,6 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec("yarn electron-builder install-app-deps");
}
@@ -70,8 +66,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")
);
}

View File

@@ -1,48 +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 { readFile, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}
if (process.argv[1] === __filename) {
console.log("Patching better-sqlite3");
patchBetterSQLite3();
}

View File

@@ -46,8 +46,6 @@ export class SQLite {
this.sqlite = require("better-sqlite3-multiple-ciphers")(
filePath
).unsafeMode(true);
const betterTrigram = require("sqlite-better-trigram");
betterTrigram.load(this.sqlite);
}
/**

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeImage, nativeImage } from "electron";
import path from "path";
import { isDevelopment } from "./index";
// @ts-expect-error stupid icojs
import { ParsedImage, parseICO } from "icojs";
import { getSystemTheme } from "./theme";
import { readFile } from "fs/promises";

View File

@@ -57,8 +57,7 @@ function getSystemTheme() {
nativeTheme.themeSource = oldThemeSource;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
return currentTheme;
@@ -73,8 +72,7 @@ function changeTheme(theme: Theme) {
nativeTheme.themeSource = theme;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
}

View File

@@ -3,7 +3,8 @@
"compilerOptions": {
"outDir": "./dist/",
"lib": ["ESNext"],
"moduleResolution": "Bundler"
"moduleResolution": "nodenext",
"module": "nodenext"
},
"include": ["src", "global.d.ts"]
}

View File

@@ -32,14 +32,12 @@ import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { IntentService } from "./services/intent";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
@@ -49,8 +47,7 @@ if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
IntentService.onLaunch();
const App = (props: { configureMode: "note-preview" }) => {
const App = () => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
@@ -64,7 +61,6 @@ const App = (props: { configureMode: "note-preview" }) => {
TipManager.init();
}, 100);
}, []);
return (
<View
style={{
@@ -94,11 +90,7 @@ const App = (props: { configureMode: "note-preview" }) => {
width: "100%"
}}
>
{props.configureMode === "note-preview" ? (
<NotePreviewConfigure />
) : (
<ApplicationHolder />
)}
<ApplicationHolder />
</GestureHandlerRootView>
<AppLockedOverlay />
</View>
@@ -111,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,
@@ -131,14 +123,13 @@ 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);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -153,7 +144,7 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
return (
<I18nProvider i18n={i18n}>
<Element {...props} />
<Element />
</I18nProvider>
);
};

View 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)
}));
}

View File

@@ -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)
}));
}

View File

@@ -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) => ({

View File

@@ -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;

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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,85 +184,59 @@ 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,
current: 0,
total: 1,
filename: attachment.filename
});
await db
.fs()
.downloadFile(
options?.groupId || attachment.hash,
options.groupId || attachment.hash,
attachment.hash,
attachment.chunkSize
);
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 1,
total: 1,
filename: attachment.filename,
success: true
});
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
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 +244,14 @@ export default async function downloadAttachment(
);
}
const filename = await getFileNameWithExtension(
let filename = 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 +259,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 +278,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,26 +296,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,
current: 0,
total: 0,
filename: attachment.filename,
success: false
});
DatabaseLogger.error(e);
useAttachmentStore.getState().remove(attachment.hash);
if (options?.throwError) {
if (options.throwError) {
throw e;
}
}

View File

@@ -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,13 +128,11 @@ 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);
@@ -157,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);
@@ -173,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;
@@ -196,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)
@@ -204,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 };
}

View File

@@ -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
};

View File

@@ -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,71 +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 exists(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)
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
@@ -250,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;
@@ -265,9 +225,7 @@ export async function exists(filename: string) {
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(() => {
/* empty */
});
.catch(console.log);
return false;
}
@@ -276,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);
@@ -285,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(
@@ -303,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;
}

View File

@@ -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
});

View File

@@ -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) {
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
@@ -124,20 +115,16 @@ export async function getUploadedFileSize(hash: string) {
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") || "0"
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;
@@ -151,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;
}

View File

@@ -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(

View File

@@ -41,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: (

View File

@@ -63,11 +63,9 @@ const Actions = ({
attachment,
close,
setAttachments,
fwdRef,
context
fwdRef
}: {
attachment: Attachment;
context: string;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
@@ -81,6 +79,7 @@ const Actions = ({
const [loading, setLoading] = useState<{
name?: string;
}>({});
const actions = [
{
name: strings.network.download(),
@@ -89,7 +88,7 @@ const Actions = ({
await db.fs().cancel(attachment.hash);
useAttachmentStore.getState().remove(attachment.hash);
}
downloadAttachment(attachment.hash, context === "global");
downloadAttachment(attachment.hash, false);
fwdRef.current?.hide();
},
icon: "download"
@@ -376,7 +375,6 @@ Actions.present = (
setAttachments={set}
close={close}
attachment={attachment}
context={context || "global"}
/>
)
});

View File

@@ -31,7 +31,6 @@ 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";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
@@ -70,7 +69,8 @@ export const AttachmentItem = ({
};
return errorOnly && attachment && !attachment?.failed ? null : (
<Pressable
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
@@ -186,6 +186,6 @@ export const AttachmentItem = ({
)}
</>
)}
</Pressable>
</TouchableOpacity>
);
};

View File

@@ -206,7 +206,7 @@ export const AttachmentDialog = ({
errorOnly={currentFilter === "errors"}
attachments={attachments}
id={index}
context={!isSheet ? "global" : "attachments-list"}
context="global"
/>
);
@@ -528,9 +528,7 @@ export const AttachmentDialog = ({
setAttachments(results);
setLoading(false);
})
.catch(() => {
/* empty */
});
.catch(console.log);
}}
/>
)

View File

@@ -68,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(),

View File

@@ -162,7 +162,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
textAlign: "center"
}}
>
{strings["2faCodeHelpText"][currentMethod.method]?.() ||
{strings["2faCodeHelpText"][currentMethod.method]() ||
strings.select2faCodeHelpText()}
</Paragraph>

View File

@@ -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) => {

View File

@@ -38,7 +38,7 @@ 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;
@@ -75,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 () => {
@@ -86,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;

View File

@@ -25,7 +25,7 @@ import {
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "@notesnook/theme";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
@@ -180,7 +180,7 @@ AttachImage.present = (response: ImageType[], context?: string) => {
close={close}
onAttach={(result) => {
resolved = true;
console.log("closing");
resolve(result);
close?.();
}}

View File

@@ -179,7 +179,6 @@ const ColorPicker = ({
title: title.current,
colorCode: selectedColor
});
if (!id) return;
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);

View File

@@ -188,7 +188,7 @@ const PDFPreview = () => {
}}
color={colors.static.white}
>
{strings.loadingWithProgress(progress?.percent)}
{strings.loadingWithProgress(progress.percent)}
</Paragraph>
</Animated.View>
) : (
@@ -307,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,

View File

@@ -62,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,
@@ -164,7 +164,7 @@ export default function Progress() {
/>
</View>
{!data?.canHideProgress ? null : (
{data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="secondaryAccented"

View File

@@ -336,7 +336,7 @@ export class VaultDialog extends Component {
);
});
}
console.log("VAULT UPDATED EVENT");
eSendEvent("vaultUpdated");
this.setState({
loading: false
@@ -406,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();
@@ -459,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);
@@ -542,6 +543,7 @@ export class VaultDialog extends Component {
this.close();
})
.catch((e) => {
console.log("Error", e);
this._takeErrorAction(e);
});
}

View File

@@ -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>
);
};

View File

@@ -54,7 +54,7 @@ export const Header = ({
onSearch
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute?: RouteName;
renderedInRoute: RouteName;
id?: string;
title: string;
headerRightButtons?: HeaderRightButton[];

View File

@@ -19,8 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import { Menu } from "react-native-material-menu";
import Menu from "react-native-reanimated-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import {
HeaderRightButton,
@@ -41,7 +42,7 @@ export const RightMenus = ({
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute?: RouteName;
renderedInRoute: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;
@@ -96,16 +97,14 @@ export const RightMenus = ({
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: 35
marginTop: -40
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -117,10 +116,9 @@ export const RightMenus = ({
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0,
alignSelf: "flex-start",
width: "100%"
borderRadius: 0
}}
type="plain"
buttonType={{
@@ -129,7 +127,6 @@ export const RightMenus = ({
key={item.title}
title={item.title}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();

View File

@@ -41,7 +41,7 @@ export const Title = ({
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute?: string;
renderedInRoute: string;
id?: string;
}) => {
const { colors } = useThemeColors();

View File

@@ -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,11 +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";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
@@ -46,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);
@@ -80,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: 12,
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 && (
@@ -167,7 +90,6 @@ const ImagePreview = () => {
background="black"
animation="slide"
visible={true}
useSafeArea={false}
onRequestClose={close}
transparent
>
@@ -208,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>

View File

@@ -83,7 +83,7 @@ const Intro = ({ navigation }) => {
>
{item.headings?.map((heading) => (
<Heading
key={heading()}
key={heading}
style={{
marginBottom: 5
}}
@@ -128,6 +128,7 @@ const Intro = ({ navigation }) => {
style={{
width: deviceMode !== "mobile" ? width / 2 : "100%",
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
@@ -137,7 +138,7 @@ const Intro = ({ navigation }) => {
marginTop: deviceMode !== "mobile" ? 50 : null,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7 - (insets.top + insets.bottom)
minHeight: height * 0.7
}}
>
<SwiperFlatList

View File

@@ -57,7 +57,10 @@ export const SectionHeader = React.memo<
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
const groupBy = strings.groupByStrings[groupOptions.groupBy]();
const groupBy =
strings.groupByStrings[
groupOptions.groupBy as keyof typeof strings.groupByStrings
]?.();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
);

View File

@@ -81,8 +81,7 @@ const ReminderItem = React.memo(
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
marginTop: 5
flexWrap: "wrap"
}}
>
{item.disabled ? (
@@ -150,8 +149,10 @@ const ReminderItem = React.memo(
fontSize={SIZE.xs}
style={{
justifyContent: "flex-start",
height: 25,
alignSelf: "flex-start"
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
marginTop: 5
}}
/>
</View>

View File

@@ -70,7 +70,6 @@ const SelectionWrapper = ({
}
const onLongPress = () => {
if (isSheet) return;
if (useSelectionStore.getState().selectionMode !== item.type) {
useSelectionStore.getState().setSelectionMode(item.type);
}
@@ -79,7 +78,7 @@ const SelectionWrapper = ({
return (
<Pressable
customColor={isSheet ? colors.secondary.background : "transparent"}
customColor={isSheet ? colors.primary.hover : "transparent"}
testID={testID}
onLongPress={onLongPress}
onPress={onPress}

View File

@@ -153,7 +153,9 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
},
items?.cacheItem(index) ? 100 : 0
);
} catch (e) {}
} catch (e) {
console.log("Error", e);
}
})();
}, [index, items, refreshItem]);
@@ -295,6 +297,7 @@ function getDate(item: Item, groupType?: GroupingKey): number {
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},

View File

@@ -119,6 +119,7 @@ export const PricingPlans = ({
setLoading(false);
} catch (e) {
setLoading(false);
console.log("error getting sku", e);
}
}, [promo?.promoCode]);
@@ -128,10 +129,7 @@ export const PricingPlans = ({
if (code.startsWith("com.streetwriters.notesnook")) {
skuId = code;
} else {
skuId = await db.offers?.getCode(
code.split(":")[0],
Platform.OS as "ios" | "android"
);
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
}
const products = await PremiumService.getProducts();
@@ -170,6 +168,7 @@ export const PricingPlans = ({
});
return true;
} catch (e) {
console.log("PROMOCODE ERROR:", code, e);
return false;
}
};
@@ -229,6 +228,7 @@ export const PricingPlans = ({
});
} catch (e) {
setBuying(false);
console.log(e);
}
};

View File

@@ -137,13 +137,14 @@ export const Items = ({ item, buttons, close }) => {
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
flex: 1
width: topBarItemWidth
}}
>
<View
onPress={item.func}
style={{
height: topBarItemWidth,
width: topBarItemWidth,
justifyContent: "center",
alignItems: "center",
textAlign: "center",
@@ -168,7 +169,7 @@ export const Items = ({ item, buttons, close }) => {
<Paragraph
textBreakStrategy="simple"
size={SIZE.xxs}
size={SIZE.xxs + 1}
style={{ textAlign: "center" }}
>
{item.title}
@@ -241,7 +242,7 @@ export const Items = ({ item, buttons, close }) => {
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
paddingRight: 25,
gap: 15
gap: 5
}}
>
{topBarItems.map(renderTopBarItem)}

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef } from "react";
import {
@@ -27,7 +26,7 @@ import {
Platform,
View
} from "react-native";
import { Menu } from "react-native-material-menu";
import Menu from "react-native-reanimated-material-menu/src/Menu";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
@@ -47,6 +46,7 @@ import { MoveNotebookSheet } from "../sheets/move-notebook";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import { strings } from "@notesnook/intl";
export const SelectionHeader = React.memo(
({
@@ -101,11 +101,9 @@ export const SelectionHeader = React.memo(
};
const deleteItem = async () => {
if (!type) return;
presentDialog({
title: strings.doActions.delete.unknown(type, selectedItemsList.length),
paragraph: strings.actionConfirmations.delete.unknown(
type,
title: strings.doActions.delete[type](selectedItemsList.length),
paragraph: strings.actionConfirmations.delete[type](
selectedItemsList.length
),
positiveText: strings.delete(),
@@ -220,16 +218,14 @@ export const SelectionHeader = React.memo(
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: 35
marginTop: -20
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -296,35 +292,15 @@ export const SelectionHeader = React.memo(
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" && type !== "note" && type !== "notebook",
visible: type !== "trash",
icon: "delete"
},
{
@@ -343,10 +319,9 @@ export const SelectionHeader = React.memo(
!item.visible ? null : (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0,
alignSelf: "flex-start",
width: "100%"
borderRadius: 0
}}
type="plain"
buttonType={{
@@ -356,7 +331,6 @@ export const SelectionHeader = React.memo(
key={item.title}
title={item.title}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();

View File

@@ -100,6 +100,7 @@ export const AddNotebookSheet = ({
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
setImmediate(() => {
console.log(parent, notebook.id);
eSendEvent(eOnNotebookUpdated, notebook.id);
});
}

View File

@@ -90,12 +90,13 @@ export const NotebookItem = ({
for (const key in keys) {
nextState[key] = !state.initialState[key] ? undefined : "deselected";
}
console.log("Single item selection");
state.setSelection({
[item.id]: "selected",
...nextState
});
} else {
console.log("Multi item selection");
state.markAs(item, "selected");
}
};

View File

@@ -134,10 +134,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
) : (
<>
<Input
key="code-input"
fwdRef={codeInputRef}
placeholder={strings.code()}
defaultValue=""
placeholder={strings.verifyNewEmail()}
onChangeText={(code) => {
emailChangeData.current.code = code;
}}

View File

@@ -333,17 +333,13 @@ const ExportNotesSheet = ({
if (Platform.OS === "ios") {
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {
/* empty */
});
}).catch(console.log);
} else {
FileViewer.open(result.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
} as any).catch(console.log);
}
}}
/>

View File

@@ -62,9 +62,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
}).catch(() => {
/* empty */
});
}).catch(console.log);
}}
/>
</View>

View File

@@ -113,9 +113,11 @@ const ManageTagsSheet = (props: {
.sorted()
.then((items) => {
setTags(items);
console.log("searched tags");
});
} else {
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
console.log("items loaded tags", items.placeholders.length);
setTags(items);
});
}

View File

@@ -301,8 +301,8 @@ export const NotebookSheet = () => {
}}
onPress={async () => {
await deleteItems(
"notebook",
useItemSelectionStore.getState().getSelectedItemIds()
useItemSelectionStore.getState().getSelectedItemIds(),
"notebook"
);
useSelectionStore.getState().clearSelection();
useItemSelectionStore.setState({

View File

@@ -150,7 +150,7 @@ const PublishNoteSheet = ({
</View>
) : (
<>
{isPublished && publishUrl ? (
{isPublished && (
<View
style={{
flexDirection: "row",
@@ -204,7 +204,7 @@ const PublishNoteSheet = ({
name="content-copy"
/>
</View>
) : null}
)}
<TouchableOpacity
onPress={() => {

View File

@@ -17,17 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { createRef } from "react";
import { Platform, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import {
eSubscribeEvent,
eUnSubscribeEvent,
@@ -35,6 +30,8 @@ import {
} from "../../../services/event-manager";
import { clearMessage } from "../../../services/message";
import SettingsService from "../../../services/settings";
import { db } from "../../../common/database";
import Storage from "../../../common/database/storage";
import { eOpenRecoveryKeyDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
@@ -44,6 +41,9 @@ import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import RNFetchBlob from "react-native-blob-util";
import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
class RecoveryKeySheet extends React.Component {
constructor(props) {
@@ -126,7 +126,7 @@ class RecoveryKeySheet extends React.Component {
"base64"
);
} else {
path = await filesystem.checkAndCreateDir("/");
path = await Storage.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, data, "base64");
}
ToastManager.show({
@@ -157,7 +157,7 @@ class RecoveryKeySheet extends React.Component {
if (!file) return;
path = file.uri;
} else {
path = await filesystem.checkAndCreateDir("/");
path = await Storage.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, this.state.key, "utf8");
path = path + fileName;
}
@@ -191,17 +191,13 @@ class RecoveryKeySheet extends React.Component {
Share.open({
url: path,
failOnCancel: false
}).catch(() => {
/* empty */
});
}).catch(console.log);
} else {
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
}).catch(() => {
/* empty */
});
}).catch(console.log);
}
} catch (e) {
console.error(e);

View File

@@ -273,7 +273,7 @@ const ListNoteItem = ({
listType,
noteInternalLinks.length
]);
console.log(noteInternalLinks);
const renderBlock = React.useCallback(
(block: ContentBlock) => (
<ListBlockItem
@@ -419,6 +419,7 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => {
id={index}
items={items}
onSelect={(note, blockId) => {
console.log(note.id, blockId);
eSendEvent(eOnLoadNote, {
item: note,
blockId: blockId

View File

@@ -88,14 +88,14 @@ export const RelationsList = ({
sortDirection: "desc"
})
.then((grouped) => {
setTimeout(() => {
setItems(grouped);
}, 300);
setItems(grouped);
});
}, [relationType, referenceType, item?.id, item?.type, updater]);
}, [relationType, referenceType, item?.id, item?.type]);
return (
<View style={{ paddingHorizontal: 12, height: "100%" }}>
<View
style={{ paddingHorizontal: 12, height: hasNoRelations ? 300 : "100%" }}
>
<SheetProvider context="local" />
<DialogHeader
title={title}

View File

@@ -68,19 +68,19 @@ export default function ReminderNotify({
const QuickActions = [
{
title: `5 ${strings.timeShort.minute()}`,
title: `5 ${strings.timeShort.minute}`,
time: 5
},
{
title: `15 ${strings.timeShort.minute()}`,
title: `15 ${strings.timeShort.minute}`,
time: 15
},
{
title: `30 ${strings.timeShort.minute()}`,
title: `30 ${strings.timeShort.minute}`,
time: 30
},
{
title: `1 ${strings.timeShort.hour()}`,
title: `1 ${strings.timeShort.hour}`,
time: 60
}
];

View File

@@ -124,11 +124,9 @@ export default function ReminderSheet({
const referencedItem = reference ? (reference as Note) : null;
const title = useRef<string | undefined>(
!reminder ? referencedItem?.title : reminder?.title
);
const details = useRef<string | undefined>(
!reminder ? referencedItem?.headline : reminder?.description
reminder?.title || referencedItem?.title
);
const details = useRef<string | undefined>(reminder?.description);
const titleRef = useRef<TextInput>(null);
const timer = useRef<NodeJS.Timeout>();
@@ -545,7 +543,7 @@ export default function ReminderSheet({
/>
{reminderMode === ReminderModes.Permanent ? null : (
<RNScrollView
<ScrollView
style={{
flexDirection: "row",
marginTop: 12,
@@ -556,9 +554,9 @@ export default function ReminderSheet({
{Object.keys(ReminderNotificationModes).map((mode) => (
<Button
key={mode}
title={strings.reminderNotificationModes(
mode as keyof typeof ReminderNotificationModes
)}
title={strings.reminderNotificationModes[
mode as keyof typeof strings.reminderNotificationModes
]()}
style={{
marginRight: 12,
borderRadius: 100
@@ -590,7 +588,7 @@ export default function ReminderSheet({
}}
/>
))}
</RNScrollView>
</ScrollView>
)}
</ScrollView>

View File

@@ -38,7 +38,7 @@ const Sort = ({ type, screen }) => {
);
const updateGroupOptions = async (_groupOptions) => {
const groupType = screen === "Notes" ? "home" : type + "s";
console.log("updateGroupOptions for group", groupType, "in", screen);
await db.settings.setGroupOptions(groupType, _groupOptions);
setGroupOptions(_groupOptions);
setTimeout(() => {

View File

@@ -215,9 +215,7 @@ export const Update = ({ version: appVersion, fwdRef }) => {
marginTop: 10
}}
onPress={() => {
Linking.openURL(GITHUB_PAGE_URL).catch(() => {
/* empty */
});
Linking.openURL(GITHUB_PAGE_URL).catch(console.log);
}}
>
{strings.readReleaseNotes()}

View File

@@ -154,9 +154,7 @@ export const Toast = ({ context = "global" }) => {
>
<Icon
name={
toastOptions.icon
? toastOptions.icon
: toastOptions.type === "success"
toastOptions.type === "success"
? "check"
: toastOptions.type === "info"
? "information"

View File

@@ -0,0 +1,158 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { ActivityIndicator, DimensionValue, ViewStyle } from "react-native";
import Animated, {
FadeIn,
FadeOut,
Layout,
LightSpeedInLeft
} from "react-native-reanimated";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { SIZE } from "../../../utils/size";
import NativeTooltip from "../../../utils/tooltip";
import { ButtonProps } from "../button";
import { Pressable, useButton } from "../pressable";
import Heading from "../typography/heading";
import Paragraph from "../typography/paragraph";
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
export const AnimatedButton = ({
height = 45,
width = null,
onPress,
loading = false,
title = null,
icon,
fontSize = SIZE.sm,
type = "transparent",
iconSize = SIZE.md,
style = {},
accentColor = "accent",
accentText = "light",
onLongPress,
tooltipText,
textStyle,
iconPosition = "left",
buttonType,
bold,
iconColor,
fwdRef,
...restProps
}: ButtonProps) => {
const { text } = useButton({
type,
accent: accentColor,
text: accentText
});
const textColor = buttonType?.text ? buttonType.text : text;
const Component = bold ? Heading : Paragraph;
return (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
layout={Layout.springify()}
>
<Pressable
{...restProps}
fwdRef={fwdRef}
onPress={onPress}
onLongPress={(event) => {
if (onLongPress) {
onLongPress(event);
return;
}
if (tooltipText) {
NativeTooltip.show(event, tooltipText, NativeTooltip.POSITIONS.TOP);
}
}}
disabled={loading}
type={type}
accentColor={accentColor}
accentText={accentText}
customColor={buttonType?.color}
customSelectedColor={buttonType?.selected}
customOpacity={buttonType?.opacity}
customAlpha={buttonType?.alpha}
style={{
height: height,
width: (width as DimensionValue) || undefined,
paddingHorizontal: 12,
borderRadius: 5,
alignSelf: "center",
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
...(style as ViewStyle)
}}
>
{loading ? (
<ActivityIndicator color={textColor} size={fontSize + 4} />
) : null}
{icon && !loading && iconPosition === "left" ? (
<AnimatedIcon
exiting={FadeOut.duration(100)}
entering={LightSpeedInLeft}
layout={Layout.springify()}
name={icon}
style={{
marginRight: 0
}}
color={iconColor || buttonType?.text || textColor}
size={iconSize}
/>
) : null}
{!title ? null : (
<Component
layout={Layout.springify()}
animated={true}
color={textColor as string}
size={fontSize}
numberOfLines={1}
style={[
{
marginLeft:
icon || (loading && iconPosition === "left") ? 5 : 0,
marginRight:
icon || (loading && iconPosition === "right") ? 5 : 0
},
textStyle
]}
>
{title}
</Component>
)}
{icon && !loading && iconPosition === "right" ? (
<Icon
name={icon}
style={{
marginLeft: 0
}}
color={iconColor || buttonType?.text || textColor}
size={iconSize}
/>
) : null}
</Pressable>
</Animated.View>
);
};

View File

@@ -51,6 +51,7 @@ export const ReminderTime = ({
<Button
title={time}
key={reminder.id}
height={20}
icon="bell"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
@@ -66,10 +67,12 @@ export const ReminderTime = ({
marginRight: 0
}}
style={{
height: "auto",
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: colors.primary.border,
paddingHorizontal: 6,
marginBottom: 5,
...(style as ViewStyle)
}}
{...props}

View File

@@ -59,9 +59,8 @@ const SheetWrapper = ({
const smallTablet = deviceMode === "smallTablet";
const dimensions = useSettingStore((state) => state.dimensions);
const insets = useGlobalSafeAreaInsets();
const appState = useAppState();
const lockEvents = useRef(false);
const locked = useUserStore((state) => state.appLocked);
let width = dimensions.width > 600 ? 600 : 500;
const style = React.useMemo(() => {
@@ -100,21 +99,24 @@ const SheetWrapper = ({
};
useEffect(() => {
if (locked) {
const ref = fwdRef || localRef;
ref?.current?.hide();
if (useUserStore.getState().appLocked) {
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
ref?.current?.show();
unsub();
lockEvents.current = false;
}
});
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
const ref = fwdRef || localRef;
ref?.current?.hide();
if (useUserStore.getState().appLocked) {
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
ref?.current?.show();
unsub();
lockEvents.current = false;
}
});
}
}
}
}, [locked, fwdRef]);
}, [appState, fwdRef]);
return (
<ScopedThemeProvider value="sheet">

View File

@@ -18,15 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { DimensionValue, View } from "react-native";
import { View } from "react-native";
import { SvgXml } from "./lazy";
export const SvgView = ({
width = 250,
height = 250,
src
}: {
width?: DimensionValue;
height?: DimensionValue;
width?: number | string;
height?: number | number;
src?: string;
}) => {
if (!src) return null;

View File

@@ -315,9 +315,7 @@ const trialstarted: { id: string; steps: TStep[] } = {
actionButton: {
text: strings.joinDiscord(),
action: () => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
/* empty */
});
Linking.openURL("https://discord.gg/zQBK97EE22").catch(console.log);
}
}
}
@@ -368,9 +366,7 @@ const Support = () => {
width: "90%"
}}
onPress={() => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
/* empty */
});
Linking.openURL("https://discord.gg/zQBK97EE22").catch(console.log);
}}
icon="discord"
type="secondary"
@@ -384,9 +380,7 @@ const Support = () => {
width: "90%"
}}
onPress={() => {
Linking.openURL("https://t.me/notesnook").catch(() => {
/* empty */
});
Linking.openURL("https://t.me/notesnook").catch(console.log);
}}
icon="telegram"
type="secondary"

View File

@@ -254,8 +254,8 @@ export const useActions = ({
item.type === "color"
) {
presentDialog({
title: strings.doActions.delete.unknown(item.type, 1),
paragraph: strings.actionConfirmations.delete.unknown(item.type, 1),
title: strings.doActions.delete[item.type](1),
paragraph: strings.actionConfirmations.delete[item.type](1),
positivePress: async () => {
if (item.type === "reminder") {
await db.reminders.remove(item.id);
@@ -290,7 +290,7 @@ export const useActions = ({
});
} else {
try {
await deleteItems(item.type, [item.id]);
await deleteItems([item.id], item.type);
} catch (e) {
console.error(e);
}
@@ -303,8 +303,8 @@ export const useActions = ({
close();
await sleep(300);
presentDialog({
title: strings.doActions.delete.unknown(item.itemType, 1),
paragraph: strings.actionConfirmations.delete.unknown(item.itemType, 1),
title: strings.doActions.delete[item.itemType](1),
paragraph: strings.actionConfirmations.delete[item.itemType](1),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: async () => {
@@ -313,7 +313,10 @@ export const useActions = ({
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: strings.actions.deleted.unknown(item.itemType, 1),
heading:
strings.actions.deleted[
item.itemType as keyof typeof strings.actions.deleted
](1),
type: "success",
context: "local"
});
@@ -954,10 +957,11 @@ export const useActions = ({
id: "trash",
title:
item.type !== "notebook" && item.type !== "note"
? strings.doActions.delete.unknown(
item.type === "trash" ? item.itemType : item.type,
1
)
? strings.doActions.delete[
item.type === "trash"
? item.itemType
: (item.type as keyof typeof strings.doActions.delete)
](1)
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",

View File

@@ -17,15 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
EV,
EVENTS,
EventManagerSubscription,
SYNC_CHECK_IDS,
SyncStatusEvent,
User
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { User } from "@notesnook/core";
import { EV, EVENTS, SYNC_CHECK_IDS, SyncStatusEvent } from "@notesnook/core";
import { EventManagerSubscription } from "@notesnook/core";
import notifee from "@notifee/react-native";
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
import React, { useCallback, useEffect, useRef } from "react";
@@ -47,7 +41,6 @@ import * as RNIap from "react-native-iap";
import { DatabaseLogger, db, setupDatabase } from "../common/database";
import { initializeLogger } from "../common/database/logger";
import { MMKV } from "../common/database/mmkv";
import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import { Walkthrough } from "../components/walkthroughs";
@@ -59,14 +52,12 @@ import {
} from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
import BiometricService from "../services/biometrics";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
presentSheet
} from "../services/event-manager";
import { IntentService } from "../services/intent";
import {
clearMessage,
setEmailVerifyMessage,
@@ -75,17 +66,14 @@ import {
setRecoveryKeyMessage,
setUpdateAvailableMessage
} from "../services/message";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import Sync from "../services/sync";
import { clearAllStores, initAfterSync } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { initAfterSync } from "../stores";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useMessageStore } from "../stores/use-message-store";
import { useSettingStore } from "../stores/use-setting-store";
import { changeSystemBarColors } from "../stores/use-theme-store";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
@@ -101,8 +89,10 @@ import {
} from "../utils/events";
import { getGithubVersion } from "../utils/github-version";
import { tabBarRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import { NotesnookModule } from "../utils/notesnook-module";
import { changeSystemBarColors } from "../stores/use-theme-store";
import { strings } from "@notesnook/intl";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -217,19 +207,11 @@ const onRequestPartialSync = async (
};
const onLogout = async (reason: string) => {
DatabaseLogger.log("User Logged Out " + reason);
setLoginMessage();
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
clearAllStores();
setImmediate(() => {
refreshAllStores();
DatabaseLogger.log("User Logged Out" + reason);
Notifications.setupReminders(true);
SettingsService.set({
introCompleted: true
});
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
};
async function checkForShareExtensionLaunchedInBackground() {
@@ -254,7 +236,9 @@ async function checkForShareExtensionLaunchedInBackground() {
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
} catch (e) {}
} catch (e) {
console.log(e);
}
}
async function saveEditorState() {
@@ -495,14 +479,12 @@ export const useAppEvents = () => {
}
if (fullBackup) {
await BackupService.run(false, undefined, "full");
await BackupService.run(true, undefined, "full");
}
}
if (SettingsService.getProperty("offlineMode")) {
db.attachments.cacheAttachments().catch(() => {
/* empty */
});
db.attachments.cacheAttachments().catch(console.log);
}
}
}, []);
@@ -550,6 +532,7 @@ export const useAppEvents = () => {
});
}
} catch (e) {
console.log(e);
ToastManager.error(e as Error, "Error updating user", "global");
}
@@ -594,29 +577,6 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.uploadCanceled, (data) => {
useAttachmentStore.getState().setUploading(data);
}),
EV.subscribe(EVENTS.migrationStarted, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
)
return;
startProgress({
title: "Migrating Data",
paragraph: "Please wait while we migrate your data",
canHideProgress: false,
fillBackground: true
});
}),
EV.subscribe(EVENTS.migrationFinished, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
)
return;
endProgress();
}),
EV.subscribe(EVENTS.vaultLocked, async () => {
// Lock all notes in all tabs...
for (const tab of useTabStore.getState().tabs) {
@@ -644,7 +604,6 @@ export const useAppEvents = () => {
const emitterSubscriptions = [
Linking.addEventListener("url", onAppOpenedFromURL),
SodiumEventEmitter.addListener(
"onSodiumProgress",
onFileEncryptionProgress
@@ -695,10 +654,6 @@ export const useAppEvents = () => {
// Reset the editor if the app has been in background for more than 10 minutes.
eSendEvent(eEditorReset);
}
setTimeout(async () => {
IntentService.onAppStateChanged();
}, 100);
} else {
await saveEditorState();
if (
@@ -783,7 +738,9 @@ export const useAppEvents = () => {
useEffect(() => {
if (!appLocked && isAppLoading) {
initializeLogger()
.catch((e) => {})
.catch((e) => {
console.log(e);
})
.finally(() => {
//@ts-ignore
initializeDatabase();

View File

@@ -32,11 +32,18 @@ export function useAppState() {
const subscription = AppState.addEventListener("change", onChange);
return () => {
subscription.remove();
// @ts-expect-error - React Native >= 0.65
if (typeof subscription?.remove === "function") {
// @ts-expect-error - need update @types/react-native@0.65.x
subscription.remove();
} else {
// React Native < 0.65
AppState.removeEventListener("change", onChange);
}
};
}, []);
return appState;
}
export type { AppStateStatus };
export { AppStateStatus };

View File

@@ -196,6 +196,7 @@ export const useTotalNotes = (type: "notebook" | "tag" | "color") => {
}
setTotalNotesById(totalNotesById);
});
console.log("useTotalNotes.getTotalNotes");
},
[type]
);

View File

@@ -31,7 +31,7 @@ export function useGroupOptions(type: any) {
const onUpdate = (groupType: string) => {
if (groupType !== type) return;
const options = db.settings?.getGroupOptions(type) as any;
console.log("useGroupOptions.onUpdate", type, options);
if (
groupOptions?.groupBy !== options.groupBy ||
groupOptions?.sortBy !== options.sortBy ||

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { Keyboard, KeyboardEventListener, KeyboardMetrics } from "react-native";
import { Keyboard, KeyboardEventListener, ScreenRect } from "react-native";
const emptyCoordinates = Object.freeze({
screenX: 0,
@@ -34,8 +34,8 @@ const initialValue = {
export default function useKeyboard() {
const [shown, setShown] = useState(false);
const [coordinates, setCoordinates] = useState<{
start: undefined | KeyboardMetrics;
end: KeyboardMetrics;
start: undefined | ScreenRect;
end: ScreenRect;
}>(initialValue);
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);

View File

@@ -74,8 +74,10 @@ export const useNotebook = (
};
const onUpdate = (type: string) => {
console.log("event", type);
if (type !== "notebooks") return;
onRequestUpdate();
console.log("useNotebook.onUpdate", item?.id, Date.now());
};
eSubscribeEvent(eGroupOptionsUpdated, onUpdate);

View File

@@ -45,9 +45,6 @@ import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import { strings } from "@notesnook/intl";
import { IntentService } from "../services/intent";
import ReminderSheet from "../components/sheets/reminder";
import { db } from "../common/database";
const NativeStack = createNativeStackNavigator();
const IntroStack = createNativeStackNavigator();
@@ -79,24 +76,12 @@ const _Tabs = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const height = useSettingStore((state) => state.dimensions.height);
const insets = useGlobalSafeAreaInsets();
const screenHeight = height - (50 + insets.top + insets.bottom);
React.useEffect(() => {
setTimeout(async () => {
setTimeout(() => {
useNavigationStore.getState().update(homepage);
const intent = IntentService.getLaunchIntent();
if (intent && intent["com.streetwriters.notesnook.OpenReminderId"]) {
const reminder = await db.reminders.reminder(
intent["com.streetwriters.notesnook.OpenReminderId"]
);
if (reminder) {
ReminderSheet.present(reminder);
}
} else if (intent["com.streetwriters.notesnook.NewReminder"]) {
ReminderSheet.present();
}
}, 1000);
}, [homepage]);

View File

@@ -97,7 +97,7 @@ const _TabsHolder = () => {
tabBarRef.current?.goToPage(1, false);
return;
}
if (item && item.type === "notesnook.action.newnote") {
if (item.type === "notesnook.action.newnote") {
clearAppState();
if (!tabBarRef.current) {
await sleep(3000);

View File

@@ -9,19 +9,21 @@
"@streetwriters/showdown": "^3.0.1-alpha.2",
"absolutify": "^0.1.0",
"buffer": "^6.0.3",
"dayjs": "^1.10.4",
"deprecated-react-native-prop-types": "^4.1.0",
"entities": "^3.0.1",
"fflate": "^0.7.3",
"html-to-text": "9.0.5",
"phone": "^3.1.14",
"qclone": "^1.2.0",
"dayjs": "^1.11.13",
"react-native-actions-sheet": "0.9.7",
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
"react-native-drax": "^0.10.2",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-progress": "^5.0.0",
"react-native-qrcode-svg": "^6.0.6",
"react-native-reanimated-material-menu": "github:ammarahm-ed/react-native-reanimated-material-menu",
"react-native-reanimated-progress-bar": "1.0.1",
"react-native-swiper-flatlist": "3.2.2",
"timeago.js": "4.0.2",
@@ -32,14 +34,17 @@
"katex": "0.16.2",
"@readme/data-urls": "3.0.0",
"react-native-wheel-color-picker": "^1.3.1",
"tinycolor2": "1.6.0",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "10.45.2",
"@trpc/react-query": "10.45.2",
"@trpc/server": "10.45.2",
"@streetwriters/kysely": "^0.27.4",
"pathe": "1.1.2",
"react-native-format-currency": "0.0.5",
"@lingui/core": "5.1.2",
"@lingui/react": "5.1.2",
"react-native-check-version": "^1.3.0",
"react-native-material-menu": "^2.0.0"
"@lingui/react": "4.11.2",
"@lingui/core": "4.11.2"
},
"sideEffects": false
}

View File

@@ -229,7 +229,7 @@ const useLockedNoteHandler = () => {
const unlockWithBiometrics = async () => {
try {
if (!tabRef.current?.noteLocked || !tabRef.current) return;
console.log("Trying to unlock with biometrics...");
const credentials = await BiometricService.getCredentials(
"Unlock note",
"Unlock note to open it in editor."
@@ -305,6 +305,7 @@ const useLockedNoteHandler = () => {
locked: false
});
} catch (e) {
console.log(e);
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error"
@@ -323,6 +324,7 @@ const useLockedNoteHandler = () => {
unlockWithBiometrics();
}, 150);
} else {
console.log("Biometrics unavailable.", editorState().movedAway);
if (!editorState().movedAway) {
setTimeout(() => {
if (tabRef.current && tabRef.current?.locked) {

View File

@@ -34,7 +34,6 @@ import { EditorEvents } from "./tiptap/utils";
import { useThemeColors } from "@notesnook/theme";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { db } from "../../common/database";
import { i18n } from "@lingui/core";
const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
if (request.url.includes("https")) {
@@ -74,6 +73,7 @@ export function ReadonlyEditor(props: {
}
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
console.log("Readonly editor loaded.");
props.onLoad?.((content: { data: string; id: string }) => {
setTimeout(() => {
noteId.current = content.id;
@@ -89,6 +89,7 @@ export function ReadonlyEditor(props: {
} else if (editorMessage.type === EventTypes.getAttachmentData) {
const attachment = (editorMessage.value as any).attachment as Attachment;
console.log("Getting attachment data:", attachment.hash, attachment.type);
downloadAttachment(attachment.hash, true, {
base64: attachment.type === "image",
text: attachment.type === "web-clip",
@@ -113,6 +114,7 @@ export function ReadonlyEditor(props: {
);
})
.catch(() => {
console.log("Error downloading attachment data");
editorRef.current?.postMessage(
JSON.stringify({
type: EditorEvents.attachmentData,
@@ -141,17 +143,10 @@ export function ReadonlyEditor(props: {
ref={editorRef}
key={"readonly-editor:" + props.editorId}
nestedScrollEnabled
injectedJavaScript={`
globalThis.__DEV__ = ${__DEV__}
globalThis.readonlyEditor=true;
globalThis.LINGUI_LOCALE = "${i18n.locale}";
globalThis.LINGUI_LOCALE_DATA = ${JSON.stringify({
[i18n.locale]: i18n.messages
})};
globalThis.loadApp();`}
injectedJavaScriptBeforeContentLoaded={`globalThis.readonlyEditor=true;`}
injectedJavaScript="globalThis.readonlyEditor=true;"
useSharedProcessPool={false}
javaScriptEnabled={true}
webviewDebuggingEnabled={__DEV__}
focusable={true}
setSupportMultipleWindows={false}
overScrollMode="never"

Some files were not shown because too many files have changed in this diff Show More