mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
294 Commits
fix-ios-sy
...
fix-ts-err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c9eb4e175 | ||
|
|
0517c6ce41 | ||
|
|
54d06328c1 | ||
|
|
c74677acd4 | ||
|
|
7689e1a7c5 | ||
|
|
5ffae2a08b | ||
|
|
5fa80abdc8 | ||
|
|
a30598fedf | ||
|
|
48ffecfb5f | ||
|
|
89fe712104 | ||
|
|
5dd298ee86 | ||
|
|
275b285a8b | ||
|
|
96310a46e9 | ||
|
|
42f9bf1593 | ||
|
|
25aab6913a | ||
|
|
3bd7da68be | ||
|
|
a218738eff | ||
|
|
8bf283b3bc | ||
|
|
cb09430ff4 | ||
|
|
66d75492bb | ||
|
|
d772d38dc7 | ||
|
|
46a74f9512 | ||
|
|
7df1037e3f | ||
|
|
5c3136d07d | ||
|
|
6a9ddb770e | ||
|
|
f4c54fd27f | ||
|
|
05b12df7d7 | ||
|
|
dcf053eede | ||
|
|
5430966b02 | ||
|
|
7271140627 | ||
|
|
eb5ae0773b | ||
|
|
9661dd0c5e | ||
|
|
8e70860918 | ||
|
|
3540ecf4ab | ||
|
|
f1dba9ffc2 | ||
|
|
3f1761a540 | ||
|
|
a25d21038a | ||
|
|
d55a6a6fc0 | ||
|
|
e5cf902780 | ||
|
|
a99996fcf6 | ||
|
|
0cc9c31bc3 | ||
|
|
62da8484ad | ||
|
|
821b8eebaa | ||
|
|
269941c297 | ||
|
|
e37ff88f1f | ||
|
|
1a1b4bc43c | ||
|
|
2c59df9fb7 | ||
|
|
4fc30cc224 | ||
|
|
9bd7d392af | ||
|
|
3bcd288bc7 | ||
|
|
3b86e51c49 | ||
|
|
c286b1d595 | ||
|
|
2f6b1e6bcc | ||
|
|
abb918e179 | ||
|
|
dd0dc09977 | ||
|
|
211ab47165 | ||
|
|
bc44e28e1e | ||
|
|
ade60e4272 | ||
|
|
3f5bfcb499 | ||
|
|
782a979397 | ||
|
|
e7f1e2d533 | ||
|
|
12e5f2acc1 | ||
|
|
dccc79ace3 | ||
|
|
a9a5ba09ff | ||
|
|
a6c8bbbf0a | ||
|
|
e19e9eb96f | ||
|
|
9208140b97 | ||
|
|
8241bb8ae5 | ||
|
|
329c2e220f | ||
|
|
140e343289 | ||
|
|
0d99367a93 | ||
|
|
b802b3e165 | ||
|
|
46583e12d9 | ||
|
|
c9a3f31e8f | ||
|
|
53f68aaf81 | ||
|
|
f31df9a6cc | ||
|
|
c812581bb9 | ||
|
|
1e6e940f17 | ||
|
|
9ab670d933 | ||
|
|
c46e57418b | ||
|
|
83557401f5 | ||
|
|
2277644e89 | ||
|
|
5eb09a1d3e | ||
|
|
d957be7a7b | ||
|
|
44ddb49d0e | ||
|
|
24fcd5cd0c | ||
|
|
2a34225d49 | ||
|
|
e81c825fc7 | ||
|
|
c80286b587 | ||
|
|
e85f8b60b0 | ||
|
|
5daa99d969 | ||
|
|
073bb576b5 | ||
|
|
eaeac130df | ||
|
|
b21519c24e | ||
|
|
4f9fe9b1e0 | ||
|
|
f657380ec7 | ||
|
|
05b2e559d8 | ||
|
|
ab21f02c2d | ||
|
|
12b51ed72a | ||
|
|
b672fd540c | ||
|
|
95a20b3740 | ||
|
|
6f5a7a4923 | ||
|
|
1b4d98ac0e | ||
|
|
ef2ed2bba0 | ||
|
|
abf4612977 | ||
|
|
9c6637908d | ||
|
|
e1006ed501 | ||
|
|
7d034f7f16 | ||
|
|
76eec5f8bb | ||
|
|
698866f53d | ||
|
|
21e1e73bee | ||
|
|
52e89c7842 | ||
|
|
8b357559fa | ||
|
|
2be35fadff | ||
|
|
5fe366f4f3 | ||
|
|
a13cbadd44 | ||
|
|
aa993a2fce | ||
|
|
975af5701b | ||
|
|
3add56cfe4 | ||
|
|
522ea1520d | ||
|
|
c680121eb0 | ||
|
|
a9de0696c9 | ||
|
|
4a4c52e4c8 | ||
|
|
92f5e04788 | ||
|
|
0cad098551 | ||
|
|
46105b2175 | ||
|
|
8ffbda9626 | ||
|
|
1fc55a69c9 | ||
|
|
c417d4df91 | ||
|
|
43ccf72773 | ||
|
|
a177d36571 | ||
|
|
4428ccd3a0 | ||
|
|
218ffbd758 | ||
|
|
5ace95c9d1 | ||
|
|
77a2f86119 | ||
|
|
71db85458d | ||
|
|
0be59c71de | ||
|
|
2357e3de0b | ||
|
|
53b89a59ed | ||
|
|
202c9135c2 | ||
|
|
69e6263d34 | ||
|
|
ebe830bbcc | ||
|
|
44bb9752d2 | ||
|
|
2f6af50fe2 | ||
|
|
1d80ca7d3c | ||
|
|
d33baebac8 | ||
|
|
25aaef361a | ||
|
|
f3af39a093 | ||
|
|
149c2013a2 | ||
|
|
8fb0b6f94a | ||
|
|
f09e5cadcf | ||
|
|
724c69655b | ||
|
|
800f3ca5d3 | ||
|
|
193007370a | ||
|
|
c8aff3764b | ||
|
|
d72b502977 | ||
|
|
263c970320 | ||
|
|
aeebc9878d | ||
|
|
4f37c92ecd | ||
|
|
3180a88ef0 | ||
|
|
6672db6ac2 | ||
|
|
b6da62469e | ||
|
|
e67e6374c3 | ||
|
|
ff1fc5b391 | ||
|
|
2f8bf191aa | ||
|
|
c4af2b2159 | ||
|
|
690b157387 | ||
|
|
5a276a43a4 | ||
|
|
a364ae2e9d | ||
|
|
1e2055c81b | ||
|
|
31d110252c | ||
|
|
702976c6cf | ||
|
|
3ec8500515 | ||
|
|
2c03420f33 | ||
|
|
868ae597ae | ||
|
|
59d3e06935 | ||
|
|
b296f5dd5c | ||
|
|
2dc5e4fef5 | ||
|
|
f836de3854 | ||
|
|
631a591477 | ||
|
|
1bb27145ce | ||
|
|
79b89c2e85 | ||
|
|
ceb2a8c747 | ||
|
|
bee06ecf18 | ||
|
|
33c7eb5a74 | ||
|
|
2cb788712a | ||
|
|
90d2338412 | ||
|
|
4bebf5a7b5 | ||
|
|
68eb5023f8 | ||
|
|
8692c87ed7 | ||
|
|
ea0623c6bc | ||
|
|
b526dc4d39 | ||
|
|
76a1025f59 | ||
|
|
85e046a379 | ||
|
|
58a6bbd262 | ||
|
|
07aabf9a2a | ||
|
|
8c7ba6b963 | ||
|
|
d125e0f6d9 | ||
|
|
43d017d64a | ||
|
|
bc3e7c6a53 | ||
|
|
6e8610c358 | ||
|
|
ab00645c23 | ||
|
|
e041976ae0 | ||
|
|
9d7e6b40a0 | ||
|
|
db40a648ab | ||
|
|
805e60a808 | ||
|
|
8c7093189b | ||
|
|
3092696c83 | ||
|
|
c2e7f7576e | ||
|
|
6b4f7900ec | ||
|
|
523ae5b07b | ||
|
|
a628b3fd92 | ||
|
|
e9f3037129 | ||
|
|
46d4b9dece | ||
|
|
c701baaa2c | ||
|
|
a798f8e280 | ||
|
|
baebf79046 | ||
|
|
5727aa6c4d | ||
|
|
36cf1feff9 | ||
|
|
91a3b0f5ed | ||
|
|
293219d781 | ||
|
|
2cd4f44a92 | ||
|
|
4b9a95ded7 | ||
|
|
565b0382f9 | ||
|
|
1e837f2a9c | ||
|
|
765ebbe10c | ||
|
|
e95d1601a6 | ||
|
|
c6dd1d1615 | ||
|
|
bb50850995 | ||
|
|
83083134e9 | ||
|
|
61b8960aa5 | ||
|
|
5229408156 | ||
|
|
0921fef267 | ||
|
|
60989359e2 | ||
|
|
8fe381afa9 | ||
|
|
fea9c4e42b | ||
|
|
1d78402d5d | ||
|
|
aa72c33aea | ||
|
|
69504a150e | ||
|
|
a78a2e4ae2 | ||
|
|
0ffbf83141 | ||
|
|
75c346a460 | ||
|
|
93af79926d | ||
|
|
1b354e4bca | ||
|
|
6d53b78f6a | ||
|
|
8e90adccab | ||
|
|
c19635b16a | ||
|
|
94e4b00294 | ||
|
|
76908b1fbd | ||
|
|
7409c5382d | ||
|
|
2462eb1631 | ||
|
|
986ff32293 | ||
|
|
8f7fd9d8ee | ||
|
|
54d6a5c307 | ||
|
|
d672a43a83 | ||
|
|
48343e5d5c | ||
|
|
0d3a8c88c8 | ||
|
|
31367066f7 | ||
|
|
f7c097988a | ||
|
|
b7f1f36090 | ||
|
|
8756299d80 | ||
|
|
3525d60f8c | ||
|
|
d08a485111 | ||
|
|
c203b035c4 | ||
|
|
0f4b2bdae7 | ||
|
|
cb0bc39cd1 | ||
|
|
84abfa3478 | ||
|
|
a40a48e2c1 | ||
|
|
124499cabb | ||
|
|
e874d1efd4 | ||
|
|
fb1a17566f | ||
|
|
8dd8447212 | ||
|
|
eba7341d45 | ||
|
|
08bd36d518 | ||
|
|
7a45522ff3 | ||
|
|
4ee2923181 | ||
|
|
48459efdb1 | ||
|
|
c848f3c588 | ||
|
|
52a75cfdaf | ||
|
|
9331b5f919 | ||
|
|
2ff53d4650 | ||
|
|
7f2c8ca05a | ||
|
|
9e752e9406 | ||
|
|
b8c3e2098f | ||
|
|
4308e3bf49 | ||
|
|
25956ccadb | ||
|
|
af3fba5176 | ||
|
|
4afaa6a36e | ||
|
|
e98e1a5f36 | ||
|
|
abfdad0fec | ||
|
|
99e50ddad6 | ||
|
|
67a61dd3f3 | ||
|
|
13df01d55a | ||
|
|
af4ea036c8 |
@@ -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-private/blob/master/CONTRIBUTING.md#commit-guidelines
|
||||
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
|
||||
|
||||
"mobile",
|
||||
"web",
|
||||
@@ -36,7 +36,8 @@ const SCOPES = [
|
||||
"global",
|
||||
"docs",
|
||||
"themebuilder",
|
||||
"intl"
|
||||
"intl",
|
||||
"webclipper"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -14,6 +14,8 @@ runs:
|
||||
apps/mobile/package-lock.json
|
||||
apps/desktop/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
apps/monograph/package-lock.json
|
||||
apps/theme-builder/package-lock.json
|
||||
extensions/web-clipper/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
@@ -24,3 +26,4 @@ runs:
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
packages/intl/package-lock.json
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
|
||||
2
.github/workflows/core.tests.yml
vendored
2
.github/workflows/core.tests.yml
vendored
@@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
16
.github/workflows/desktop.publish.yml
vendored
16
.github/workflows/desktop.publish.yml
vendored
@@ -37,7 +37,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -49,6 +49,13 @@ jobs:
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -57,6 +64,10 @@ jobs:
|
||||
- name: Generate desktop build
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Build desktop bundle
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run bundle
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -71,7 +82,8 @@ jobs:
|
||||
|
||||
- name: Package desktop build
|
||||
run: |
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/web/build/
|
||||
cp -r ./apps/web/build ./apps/desktop/
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/desktop/build/
|
||||
echo "Build folder archived to ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip"
|
||||
|
||||
- name: Upload desktop build
|
||||
|
||||
4
.github/workflows/desktop.tests.yml
vendored
4
.github/workflows/desktop.tests.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
|
||||
2
.github/workflows/editor.tests.yml
vendored
2
.github/workflows/editor.tests.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/help.publish.yml
vendored
2
.github/workflows/help.publish.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/monograph.publish.yml
vendored
2
.github/workflows/monograph.publish.yml
vendored
@@ -14,7 +14,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/theme-builder.publish.yml
vendored
2
.github/workflows/theme-builder.publish.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/vericrypt.publish.yml
vendored
2
.github/workflows/vericrypt.publish.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/web.benchmarks.yml
vendored
2
.github/workflows/web.benchmarks.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
bench:
|
||||
name: Bench
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/web.publish.yml
vendored
2
.github/workflows/web.publish.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/web.tests.yml
vendored
2
.github/workflows/web.tests.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ nx-cloud.env
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
*.patch
|
||||
|
||||
1127
apps/desktop/package-lock.json
generated
1127
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.21",
|
||||
"version": "3.0.25",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -25,7 +25,7 @@
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@lingui/core": "4.11.4",
|
||||
"@lingui/core": "5.1.2",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
@@ -33,10 +33,10 @@
|
||||
"electron-trpc": "0.6.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"icojs": "^0.19.4",
|
||||
"sqlite-better-trigram": "^0.0.2",
|
||||
"sqlite-better-trigram": "0.0.2",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
@@ -45,15 +45,15 @@
|
||||
"chokidar": "^4.0.1",
|
||||
"electron": "^31.7.4",
|
||||
"electron-builder": "^25.1.8",
|
||||
"esbuild": "^0.24.0",
|
||||
"esbuild": "0.21.5",
|
||||
"vitest": "2.1.8",
|
||||
"node-abi": "^3.68.0",
|
||||
"node-gyp-build": "^4.8.2",
|
||||
"playwright": "^1.48.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "^1.6.6",
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^2.1.5"
|
||||
"undici": "^6.19.8"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
|
||||
@@ -18,19 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs, { readFile, writeFile } from "fs/promises";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } 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"));
|
||||
|
||||
@@ -38,7 +36,7 @@ await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
"npx nx build:desktop @notesnook/web",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
@@ -46,12 +44,6 @@ 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
|
||||
@@ -66,11 +58,13 @@ if (args.variant === "mas") {
|
||||
await exec(`yarn run build`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
await exec(`yarn electron-builder --dir --${process.arch}`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
if (process.arch === "arm64")
|
||||
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
@@ -83,21 +77,3 @@ 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.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));
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
["npm", "run", "start:desktop"],
|
||||
path.join(__dirname, "..", "..", "web"),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
}
|
||||
|
||||
48
apps/desktop/scripts/patch-better-sqlite3.mjs
Normal file
48
apps/desktop/scripts/patch-better-sqlite3.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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();
|
||||
}
|
||||
@@ -32,6 +32,7 @@ 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";
|
||||
@@ -47,7 +48,7 @@ if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const App = (props: { configureMode: "note-preview" }) => {
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
@@ -61,6 +62,7 @@ const App = () => {
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -90,7 +92,11 @@ const App = () => {
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<ApplicationHolder />
|
||||
{props.configureMode === "note-preview" ? (
|
||||
<NotePreviewConfigure />
|
||||
) : (
|
||||
<ApplicationHolder />
|
||||
)}
|
||||
</GestureHandlerRootView>
|
||||
<AppLockedOverlay />
|
||||
</View>
|
||||
@@ -103,8 +109,8 @@ let currTheme =
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element: () => JSX.Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
return function AppWithThemeProvider(props: any) {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
state.darkTheme,
|
||||
@@ -123,13 +129,14 @@ export const withTheme = (Element: () => JSX.Element) => {
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}, 1000);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -144,7 +151,7 @@ export const withTheme = (Element: () => JSX.Element) => {
|
||||
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Element />
|
||||
<Element {...props} />
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,385 +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 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)
|
||||
}));
|
||||
}
|
||||
473
apps/mobile/app/common/database/encryption.ts
Normal file
473
apps/mobile/app/common/database/encryption.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
|
||||
import { SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
const APPLOCK_CIPHER = "applockCipher";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
//@ts-ignore
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
|
||||
const key = (await getDatabaseKey()) as string;
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = (await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
)) as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = (await encrypt(
|
||||
appLockCredentials,
|
||||
generatePassword()
|
||||
)) as Cipher;
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY: string | undefined;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword?: string) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
if (hasKey) {
|
||||
const credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = (credentials as Keychain.UserCredentials).password;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
DatabaseLogger.info("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
NOTESNOOK_DB_KEY_SALT
|
||||
);
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
DB_KEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook"
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher: Cipher = (await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
)) as Cipher;
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
}
|
||||
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
throw new Error(
|
||||
`Failed to get database key, ${await Keychain.hasInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return DB_KEY;
|
||||
}
|
||||
|
||||
export async function deriveCryptoKeyFallback(data: SerializedKey) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error(
|
||||
"Invalid password and salt provided to deriveCryptoKeyFallback"
|
||||
);
|
||||
|
||||
const credentials = await Sodium.deriveKeyFallback?.(
|
||||
data.password,
|
||||
data.salt
|
||||
);
|
||||
|
||||
if (!credentials) return;
|
||||
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key fallback stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data: SerializedKey) {
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error("Invalid password and salt provided to deriveCryptoKey");
|
||||
|
||||
const credentials = (await Sodium.deriveKey(
|
||||
data.password,
|
||||
data.salt
|
||||
)) as Password;
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey() {
|
||||
try {
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey() {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length: number) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(
|
||||
password: string,
|
||||
email: string,
|
||||
options?: { usesFallback?: boolean }
|
||||
) {
|
||||
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
|
||||
|
||||
if (options?.usesFallback && Platform.OS !== "ios") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
options?.usesFallback
|
||||
? await Sodium.hashPasswordFallback?.(password, email)
|
||||
: await Sodium.hashPassword(password, email)
|
||||
) as string;
|
||||
}
|
||||
|
||||
export async function generateCryptoKey(password: string, salt?: string) {
|
||||
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export async function generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey> {
|
||||
return Sodium.deriveKeyFallback?.(
|
||||
password,
|
||||
salt as string
|
||||
) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant: number) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
|
||||
const _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decrypt(key, _data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decrypt(fallbackKey, _data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(
|
||||
password: Password,
|
||||
data: Cipher<"base64">[]
|
||||
) {
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decryptMulti(key, data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt as string
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decryptMulti(fallbackKey, data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg: string) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
encryptionAlgorithm: enc,
|
||||
kdfAlgorithm: kdf,
|
||||
compressionAlgorithm: compressionAlg,
|
||||
isCompress: compressed === "1",
|
||||
base64_variant: base64variant
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password: SerializedKey, plainText: string) {
|
||||
const result = await Sodium.encrypt<"base64">(password, {
|
||||
type: "plain",
|
||||
data: plainText
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(
|
||||
password: SerializedKey,
|
||||
plainText: string[]
|
||||
) {
|
||||
const results = await Sodium.encryptMulti<"base64">(
|
||||
password,
|
||||
plainText.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
@@ -16,48 +16,50 @@ 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 } 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 { logger as dbLogger, ICompressor } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { strings } from "@notesnook/intl";
|
||||
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 { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { Storage } from "./storage";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
export async function setupDatabase(password?: string) {
|
||||
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,
|
||||
fs: filesystem,
|
||||
compressor: () => ({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}),
|
||||
eventsource: (Platform.OS === "ios"
|
||||
? EventSource
|
||||
: AndroidEventSource) as any,
|
||||
fs: FileStorage,
|
||||
compressor: async () =>
|
||||
({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
} as ICompressor),
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
@@ -26,7 +26,7 @@ import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
type Config = { dbName: string; async: boolean; location?: string };
|
||||
|
||||
export class RNSqliteDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
|
||||
@@ -16,58 +16,52 @@ 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 { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { IStorage } from "@notesnook/core";
|
||||
import { MMKVInstance } from "react-native-mmkv-storage";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
deriveCryptoKeyFallback,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey
|
||||
generateCryptoKeyFallback
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
export class KV {
|
||||
/**
|
||||
* @type {typeof MMKV}
|
||||
*/
|
||||
storage = null;
|
||||
constructor(storage) {
|
||||
storage: MMKVInstance;
|
||||
constructor(storage: MMKVInstance) {
|
||||
this.storage = storage;
|
||||
}
|
||||
async read(key) {
|
||||
if (!key) return null;
|
||||
let data = this.storage.getString(key);
|
||||
if (!data) return null;
|
||||
|
||||
async read<T>(key: string, isArray?: boolean) {
|
||||
if (!key) return undefined;
|
||||
const data = this.storage.getString(key);
|
||||
if (!data) return undefined;
|
||||
try {
|
||||
let parse = JSON.parse(data);
|
||||
return parse;
|
||||
return JSON.parse(data) as T;
|
||||
} catch (e) {
|
||||
return data;
|
||||
return data as T;
|
||||
}
|
||||
}
|
||||
|
||||
async write(key, data) {
|
||||
async write<T>(key: string, data: T) {
|
||||
this.storage.setString(
|
||||
key,
|
||||
typeof data === "string" ? data : JSON.stringify(data)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async readMulti(keys) {
|
||||
async readMulti<T>(keys: string[]) {
|
||||
if (keys.length <= 0) {
|
||||
return [];
|
||||
} else {
|
||||
try {
|
||||
let data = await this.storage.getMultipleItemsAsync(
|
||||
const data = await this.storage.getMultipleItemsAsync<any>(
|
||||
keys.slice(),
|
||||
"string"
|
||||
);
|
||||
@@ -79,24 +73,24 @@ export class KV {
|
||||
obj = value;
|
||||
}
|
||||
return [key, obj];
|
||||
});
|
||||
}) as [string, T][];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
return this.storage.removeItem(key);
|
||||
async remove(key: string) {
|
||||
this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
if (!keys) return true;
|
||||
return this.storage.removeItems(keys);
|
||||
async removeMulti(keys: string[]) {
|
||||
if (!keys) return;
|
||||
this.storage.removeItems(keys);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return this.storage.clearStore();
|
||||
this.storage.clearStore();
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
@@ -113,54 +107,46 @@ export class KV {
|
||||
return keys;
|
||||
}
|
||||
|
||||
async writeMulti(items) {
|
||||
return this.storage.setMultipleItemsAsync(items, "object");
|
||||
async writeMulti(items: [string, any][]) {
|
||||
await this.storage.setMultipleItemsAsync(items, "object");
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultStorage = new KV(MMKV);
|
||||
|
||||
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),
|
||||
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,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
getRandomBytes,
|
||||
checkAndCreateDir,
|
||||
requestPermission,
|
||||
deriveCryptoKey,
|
||||
getCryptoKey,
|
||||
removeCryptoKey,
|
||||
hash,
|
||||
generateCryptoKey,
|
||||
encryptMulti
|
||||
generateCryptoKeyFallback,
|
||||
deriveCryptoKeyFallback
|
||||
};
|
||||
@@ -27,13 +27,9 @@ 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,
|
||||
@@ -55,10 +51,12 @@ export async function compressToBase64(path, type) {
|
||||
: response.uri,
|
||||
"base64"
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(response.uri.replace("file://", ""))
|
||||
.catch(console.log);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
resolve(base64);
|
||||
});
|
||||
@@ -80,6 +78,8 @@ export async function compressToFile(path, type) {
|
||||
onlyScaleDown: true
|
||||
}
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return response.uri;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ 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";
|
||||
@@ -25,12 +24,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 { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { presentSheet, ToastManager } 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";
|
||||
|
||||
@@ -51,23 +50,23 @@ export async function downloadAllAttachments() {
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param {string[]} attachments
|
||||
* @param {string[]} attachmentIds
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(attachments) {
|
||||
export async function downloadAttachments(attachmentIds: string[]) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
if (!attachmentIds || !attachmentIds.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
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
const file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
@@ -83,8 +82,10 @@ export async function downloadAttachments(attachments) {
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const isCancelled = () => {
|
||||
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: 0,
|
||||
@@ -97,19 +98,21 @@ export async function downloadAttachments(attachments) {
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
for (let i = 0; i < attachmentIds.length; i++) {
|
||||
if (isCancelled()) return;
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const attachment = await db.attachments.attachment(attachmentIds[i]);
|
||||
if (!attachment) continue;
|
||||
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachments.length,
|
||||
total: attachmentIds.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
@@ -184,59 +187,85 @@ export async function downloadAttachments(attachments) {
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
ToastManager.error(e as Error, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hash,
|
||||
hashOrId: string,
|
||||
global = true,
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
options?: {
|
||||
silent?: boolean;
|
||||
cache?: boolean;
|
||||
throwError?: boolean;
|
||||
groupId?: string;
|
||||
base64?: boolean;
|
||||
text?: boolean;
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
const attachment = await db.attachments.attachment(hashOrId);
|
||||
if (!attachment) {
|
||||
DatabaseLogger.log("Attachment not found");
|
||||
DatabaseLogger.log("Attachment not found", {
|
||||
hash: hashOrId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let folder = {};
|
||||
if (!options.cache) {
|
||||
let folder: {
|
||||
uri: string;
|
||||
} | null = null;
|
||||
if (!options?.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
folder = {
|
||||
uri: await filesystem.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,
|
||||
@@ -244,14 +273,16 @@ export default async function downloadAttachment(
|
||||
);
|
||||
}
|
||||
|
||||
let filename = await getFileNameWithExtension(
|
||||
const filename = await getFileNameWithExtension(
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
const key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
let info = {
|
||||
if (!key) return;
|
||||
|
||||
const info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
length: attachment.size,
|
||||
@@ -259,18 +290,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),
|
||||
@@ -278,15 +309,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 ? null : attachment.hash,
|
||||
context: global ? "global" : attachment.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
@@ -296,14 +327,26 @@ export default async function downloadAttachment(
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(console.log);
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(console.log);
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RequestOptions } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
@@ -26,7 +27,13 @@ import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
export async function downloadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
@@ -36,9 +43,11 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const { url, headers, chunkSize } = requestOptions;
|
||||
const tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
const originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
@@ -46,6 +55,8 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
@@ -71,21 +82,21 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
const resolveUrlResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (!resolveUrlResponse.ok) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
||||
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error(
|
||||
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
);
|
||||
}
|
||||
|
||||
const downloadUrl = await res.text();
|
||||
const downloadUrl = await resolveUrlResponse.text();
|
||||
|
||||
if (!downloadUrl) {
|
||||
DatabaseLogger.log(
|
||||
@@ -95,12 +106,12 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
let request = RNFetchBlob.config({
|
||||
const request = RNFetchBlob.config({
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.fetch("GET", downloadUrl)
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
@@ -109,15 +120,16 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
cancelToken.cancel = async (reason) => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
|
||||
};
|
||||
|
||||
let response = await request;
|
||||
console.log(response.info().headers);
|
||||
const response = await request;
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
@@ -128,11 +140,13 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
let status = response.info().status;
|
||||
const status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
if (await exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
@@ -143,11 +157,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
if (
|
||||
(e as Error).message !== "canceled" &&
|
||||
!(e as Error).message.includes("NoSuchKey")
|
||||
) {
|
||||
const toast = {
|
||||
heading: strings.downloadError(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
heading: strings.downloadError((e as Error).message),
|
||||
message: (e as Error).message,
|
||||
type: "error" as const,
|
||||
context: "global"
|
||||
};
|
||||
ToastManager.show(toast);
|
||||
@@ -156,17 +173,20 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.error(e, "Download failed: ", {
|
||||
url
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
export async function checkAttachment(hash: string) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
@@ -176,7 +196,7 @@ export async function checkAttachment(hash) {
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
console.log("File Size", size);
|
||||
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0)
|
||||
@@ -184,7 +204,7 @@ export async function checkAttachment(hash) {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
return { failed: (e as Error)?.message };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
@@ -17,24 +17,41 @@ 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,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
getCacheSize,
|
||||
hashBase64,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import {
|
||||
cancelable,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize,
|
||||
requestPermission
|
||||
} from "./utils";
|
||||
|
||||
export default {
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
getCacheSize,
|
||||
requestPermission,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize
|
||||
};
|
||||
|
||||
export default {
|
||||
export const FileStorage: IFileStorage = {
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
@@ -44,10 +61,5 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
bulkExists
|
||||
};
|
||||
@@ -18,6 +18,13 @@ 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";
|
||||
@@ -25,17 +32,21 @@ import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
filename: string,
|
||||
key: SerializedKey,
|
||||
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
|
||||
) {
|
||||
await migrateFilesFromCache();
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
const output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
@@ -47,15 +58,16 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
|
||||
return output;
|
||||
return output as Output<TOutputFormat>;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data) {
|
||||
export async function hashBase64(data: string) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
@@ -67,61 +79,79 @@ export async function hashBase64(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64(data, key) {
|
||||
export async function writeEncryptedBase64(
|
||||
data: string,
|
||||
encryptionKey: SerializedKey,
|
||||
mimeType: string
|
||||
): Promise<FileEncryptionMetadataWithHash> {
|
||||
await createCacheDir();
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
const output = await Sodium.encryptFile(encryptionKey, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
console.log("encrypted file output: ", output);
|
||||
output.size = output.length;
|
||||
delete output.length;
|
||||
|
||||
RNFetchBlob.fs.unlink(filepath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
return {
|
||||
...output,
|
||||
alg: "xcha-stream"
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteFile(filename, data) {
|
||||
export async function deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
await createCacheDir();
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
const localFilePath = cacheDir + `/${filename}`;
|
||||
if (!requestOptions) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
let { url, headers } = data;
|
||||
const { url, headers } = requestOptions;
|
||||
|
||||
try {
|
||||
let response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
let status = response.info().status;
|
||||
let ok = status >= 200 && status < 300;
|
||||
const response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
const status = response.info().status;
|
||||
const ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
console.log("delete file: ", e, url, headers);
|
||||
DatabaseLogger.error(e, "Delete file", {
|
||||
url: url
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (let file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
for (const file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
for (let file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
for (const file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("clearFileStorage", e);
|
||||
DatabaseLogger.error(e, "clearFileStorage");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,62 +172,71 @@ export async function migrateFilesFromCache() {
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
console.log("Files migrated already");
|
||||
return;
|
||||
}
|
||||
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (const file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(console.log);
|
||||
console.log("Moved", file);
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
console.log("migrateFilesFromCache", e);
|
||||
DatabaseLogger.error(e, "migrateFilesFromCache");
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
export async function deleteCacheFileByPath(path: string) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name) {
|
||||
export async function deleteCacheFileByName(name: string) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
if (file.includes("_dcache") || file.startsWith("NN_")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
export async function exists(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
@@ -211,6 +250,7 @@ export async function exists(filename) {
|
||||
|
||||
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;
|
||||
@@ -225,7 +265,9 @@ export async function exists(filename) {
|
||||
);
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(console.log);
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -234,7 +276,7 @@ export async function exists(filename) {
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
export async function bulkExists(files: string[]) {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
@@ -243,7 +285,7 @@ export async function bulkExists(files) {
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter(
|
||||
@@ -261,9 +303,9 @@ export async function getCacheSize() {
|
||||
await createCacheDir();
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
|
||||
stat.forEach((file) => {
|
||||
total += parseInt(file.size as unknown as string);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -24,11 +25,22 @@ 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, getUploadedFileSize } from "./utils";
|
||||
import {
|
||||
cacheDir,
|
||||
checkUpload,
|
||||
FileSizeResult,
|
||||
getUploadedFileSize
|
||||
} from "./utils";
|
||||
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
export async function uploadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
const { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
@@ -39,7 +51,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
filePath = appGroupPath;
|
||||
@@ -54,14 +66,15 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
|
||||
|
||||
let remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === -1) return false;
|
||||
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
|
||||
const remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === FileSizeResult.Error) return false;
|
||||
|
||||
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let uploadUrlResponse = await fetch(url, {
|
||||
const uploadUrlResponse = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
@@ -78,7 +91,8 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
let uploadRequest = RNFetchBlob.config({
|
||||
const uploadRequest = RNFetchBlob.config({
|
||||
//@ts-ignore
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
@@ -98,14 +112,14 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
cancelToken.cancel = async () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
uploadRequest.cancel();
|
||||
};
|
||||
|
||||
let uploadResponse = await uploadRequest;
|
||||
let status = uploadResponse.info().status;
|
||||
let uploaded = status >= 200 && status < 300;
|
||||
const uploadResponse = await uploadRequest;
|
||||
const status = uploadResponse.info().status;
|
||||
const uploaded = status >= 200 && status < 300;
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
@@ -118,12 +132,13 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
);
|
||||
}
|
||||
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, "File upload failed");
|
||||
ToastManager.error(e as Error, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { hosts, RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { hosts } from "@notesnook/core";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
@@ -31,18 +31,13 @@ export const cacheDir =
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
|
||||
export function getRandomId(prefix) {
|
||||
export function getRandomId(prefix: string) {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
export function parseS3Error(data?: string) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
const error = {
|
||||
@@ -70,11 +65,19 @@ export function parseS3Error(data) {
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelable(operation) {
|
||||
export function cancelable(
|
||||
operation: (
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) => Promise<boolean>
|
||||
) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
cancel: async (reason?: string) => {}
|
||||
};
|
||||
return (filename, requestOptions) => {
|
||||
return (filename: string, requestOptions: RequestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
@@ -84,29 +87,35 @@ export function cancelable(operation) {
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
export function copyFileAsync(source: string, dest: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
//@ts-ignore
|
||||
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path) {
|
||||
export async function releasePermissions(path: string) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (let uri of uris) {
|
||||
for (const uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
export const FileSizeResult = {
|
||||
Empty: 0,
|
||||
Error: -1
|
||||
};
|
||||
|
||||
export async function getUploadedFileSize(hash: string, retry = 0) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
@@ -114,17 +123,37 @@ export async function getUploadedFileSize(hash) {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (
|
||||
!attachmentInfo.ok ||
|
||||
attachmentInfo.headers?.get("content-length") === null
|
||||
) {
|
||||
if (retry < 3) {
|
||||
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
|
||||
return getUploadedFileSize(hash, retry + 1);
|
||||
}
|
||||
throw new Error(
|
||||
`File size check failed: ${hash}, ${
|
||||
attachmentInfo.status
|
||||
}, ${attachmentInfo.headers?.get("content-length")}`
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
attachmentInfo.headers?.get("content-length") as string
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
return FileSizeResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpload(filename, chunkSize, expectedSize) {
|
||||
export async function checkUpload(
|
||||
filename: string,
|
||||
chunkSize: number,
|
||||
expectedSize: number
|
||||
) {
|
||||
const size = await getUploadedFileSize(filename);
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
@@ -138,3 +167,25 @@ export async function checkUpload(filename, chunkSize, expectedSize) {
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
export async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
export async function checkAndCreateDir(path: string) {
|
||||
const dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
const exists = await RNFetchBlob.fs.exists(dir);
|
||||
const isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
@@ -17,9 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function info(context: string, ...logs: unknown[]) {
|
||||
console.log(`${new Date().toLocaleDateString()}::info::${context}:`, ...logs);
|
||||
}
|
||||
function info(context: string, ...logs: unknown[]) {}
|
||||
|
||||
function error(context: string, ...logs: unknown[]) {
|
||||
console.log(
|
||||
|
||||
@@ -41,7 +41,9 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
await sleep(500);
|
||||
}
|
||||
if (item.type === "link") {
|
||||
Linking.openURL(item.data).catch(console.log);
|
||||
Linking.openURL(item.data).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else if (item.type === "promo") {
|
||||
presentSheet({
|
||||
component: (
|
||||
|
||||
@@ -112,14 +112,19 @@ const AppLockedOverlay = () => {
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
biometricUnlockAwaitingUserInput.current = true;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await NotesnookModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
if (
|
||||
activityName !== "MainActivity" &&
|
||||
activityName !== "NotePreviewConfigureActivity"
|
||||
)
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setRequestBiometrics(true);
|
||||
|
||||
@@ -180,7 +185,6 @@ const AppLockedOverlay = () => {
|
||||
DatabaseLogger.info("Locking app on entering foreground");
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
biometricUnlockAwaitingUserInput.current ||
|
||||
|
||||
@@ -63,9 +63,11 @@ const Actions = ({
|
||||
attachment,
|
||||
close,
|
||||
setAttachments,
|
||||
fwdRef
|
||||
fwdRef,
|
||||
context
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
context: string;
|
||||
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
|
||||
close?: () => void;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
@@ -79,7 +81,6 @@ const Actions = ({
|
||||
const [loading, setLoading] = useState<{
|
||||
name?: string;
|
||||
}>({});
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: strings.network.download(),
|
||||
@@ -88,7 +89,7 @@ const Actions = ({
|
||||
await db.fs().cancel(attachment.hash);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
}
|
||||
downloadAttachment(attachment.hash, false);
|
||||
downloadAttachment(attachment.hash, context === "global");
|
||||
fwdRef.current?.hide();
|
||||
},
|
||||
icon: "download"
|
||||
@@ -181,18 +182,17 @@ const Actions = ({
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
const tab = useTabStore.getState().getTabForNote(id);
|
||||
if (tab !== undefined) {
|
||||
const isFocused = useTabStore.getState().currentTab === tab;
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused = useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(true, tab);
|
||||
editorController.current.commands.setLoading(true, tab.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
close?.();
|
||||
},
|
||||
@@ -375,6 +375,7 @@ Actions.present = (
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
context={context || "global"}
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ 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);
|
||||
@@ -69,8 +70,7 @@ export const AttachmentItem = ({
|
||||
};
|
||||
|
||||
return errorOnly && attachment && !attachment?.failed ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -186,6 +186,6 @@ export const AttachmentItem = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -206,7 +206,7 @@ export const AttachmentDialog = ({
|
||||
errorOnly={currentFilter === "errors"}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
context="global"
|
||||
context={!isSheet ? "global" : "attachments-list"}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -528,7 +528,9 @@ export const AttachmentDialog = ({
|
||||
setAttachments(results);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(console.log);
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -68,11 +68,15 @@ export const ChangePassword = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (!result.error)
|
||||
const result = await BackupService.run(
|
||||
false,
|
||||
"change-password-dialog",
|
||||
"partial"
|
||||
);
|
||||
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(),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,26 +86,20 @@ const BaseDialog = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
if (locked) {
|
||||
setIntervalVisible(false);
|
||||
lockEvents.current = true;
|
||||
const unsub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked) {
|
||||
setIntervalVisible(true);
|
||||
unsub();
|
||||
setTimeout(() => {
|
||||
lockEvents.current = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [appState]);
|
||||
}, [locked]);
|
||||
|
||||
const Wrapper = useSafeArea ? SafeAreaView : View;
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setVisible(false);
|
||||
dialogInfo.onClose();
|
||||
};
|
||||
|
||||
const onNegativePress = async () => {
|
||||
|
||||
@@ -180,7 +180,7 @@ AttachImage.present = (response: ImageType[], context?: string) => {
|
||||
close={close}
|
||||
onAttach={(result) => {
|
||||
resolved = true;
|
||||
console.log("closing");
|
||||
|
||||
resolve(result);
|
||||
close?.();
|
||||
}}
|
||||
|
||||
@@ -179,6 +179,7 @@ const ColorPicker = ({
|
||||
title: title.current,
|
||||
colorCode: selectedColor
|
||||
});
|
||||
if (!id) return;
|
||||
useRelationStore.getState().update();
|
||||
useMenuStore.getState().setColorNotes();
|
||||
setVisible(false);
|
||||
|
||||
@@ -188,7 +188,7 @@ const PDFPreview = () => {
|
||||
}}
|
||||
color={colors.static.white}
|
||||
>
|
||||
{strings.loadingWithProgress(progress.percent)}
|
||||
{strings.loadingWithProgress(progress?.percent)}
|
||||
</Paragraph>
|
||||
</Animated.View>
|
||||
) : (
|
||||
@@ -307,9 +307,7 @@ const PDFPreview = () => {
|
||||
password={password}
|
||||
maxScale={6}
|
||||
onError={onError}
|
||||
onPressLink={(uri) => {
|
||||
console.log(`Link pressed: ${uri}`);
|
||||
}}
|
||||
onPressLink={(uri) => {}}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -336,7 +336,7 @@ export class VaultDialog extends Component {
|
||||
);
|
||||
});
|
||||
}
|
||||
console.log("VAULT UPDATED EVENT");
|
||||
|
||||
eSendEvent("vaultUpdated");
|
||||
this.setState({
|
||||
loading: false
|
||||
@@ -406,7 +406,6 @@ export class VaultDialog extends Component {
|
||||
} else {
|
||||
await db.vault.add(this.state.note.id);
|
||||
|
||||
console.log("update note event...");
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
|
||||
this.close();
|
||||
@@ -460,7 +459,7 @@ export class VaultDialog extends Component {
|
||||
async _deleteNote() {
|
||||
try {
|
||||
await db.vault.remove(this.state.note.id, this.password);
|
||||
await deleteItems([this.state.note.id], "note");
|
||||
await deleteItems("note", [this.state.note.id]);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
@@ -543,7 +542,6 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("Error", e);
|
||||
this._takeErrorAction(e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,10 +78,10 @@ class ExceptionHandler extends React.Component<{
|
||||
}
|
||||
|
||||
export const withErrorBoundry = (Element: React.ElementType, name: string) => {
|
||||
return function ErrorBoundary() {
|
||||
return function ErrorBoundary(props: any) {
|
||||
return (
|
||||
<ExceptionHandler component={name}>
|
||||
<Element />
|
||||
<Element {...props} />
|
||||
</ExceptionHandler>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ export const Header = ({
|
||||
onSearch
|
||||
}: {
|
||||
onLeftMenuButtonPress?: () => void;
|
||||
renderedInRoute: RouteName;
|
||||
renderedInRoute?: RouteName;
|
||||
id?: string;
|
||||
title: string;
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
|
||||
@@ -19,9 +19,8 @@ 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-reanimated-material-menu";
|
||||
import { Menu } from "react-native-material-menu";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import {
|
||||
HeaderRightButton,
|
||||
@@ -42,7 +41,7 @@ export const RightMenus = ({
|
||||
onSearch
|
||||
}: {
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
renderedInRoute: RouteName;
|
||||
renderedInRoute?: RouteName;
|
||||
id?: string;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
search?: boolean;
|
||||
@@ -97,14 +96,16 @@ export const RightMenus = ({
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: contextMenuColors.primary.background,
|
||||
marginTop: -40
|
||||
marginTop: 35
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
}}
|
||||
anchor={
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.show();
|
||||
}}
|
||||
name="dots-vertical"
|
||||
@@ -116,9 +117,10 @@ export const RightMenus = ({
|
||||
{headerRightButtons.map((item) => (
|
||||
<Button
|
||||
style={{
|
||||
width: 150,
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 0
|
||||
borderRadius: 0,
|
||||
alignSelf: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
type="plain"
|
||||
buttonType={{
|
||||
@@ -127,6 +129,7 @@ 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();
|
||||
|
||||
@@ -41,7 +41,7 @@ export const Title = ({
|
||||
isHiddenOnRender?: boolean;
|
||||
accentColor?: string;
|
||||
isBeta?: boolean;
|
||||
renderedInRoute: string;
|
||||
renderedInRoute?: string;
|
||||
id?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
@@ -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 } from "@notesnook/core";
|
||||
import { DataURL, getFileNameWithExtension } from "@notesnook/core";
|
||||
import type { ImageAttributes } from "@notesnook/editor";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
@@ -34,6 +34,11 @@ 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");
|
||||
|
||||
@@ -41,6 +46,8 @@ 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);
|
||||
@@ -73,16 +80,86 @@ const ImagePreview = () => {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
|
||||
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 */
|
||||
});
|
||||
setImage("file://" + path);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
const close = React.useCallback(() => {
|
||||
image &&
|
||||
RNFetchBlob.fs.unlink(image.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
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 && (
|
||||
@@ -90,6 +167,7 @@ const ImagePreview = () => {
|
||||
background="black"
|
||||
animation="slide"
|
||||
visible={true}
|
||||
useSafeArea={false}
|
||||
onRequestClose={close}
|
||||
transparent
|
||||
>
|
||||
@@ -130,43 +208,25 @@ const ImagePreview = () => {
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<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
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
<ImageViewer
|
||||
enableImageZoom={true}
|
||||
renderIndicator={() => <></>}
|
||||
enableSwipeDown
|
||||
useNativeDriver
|
||||
onSwipeDown={close}
|
||||
saveToLocalByLongPress={false}
|
||||
onClick={() => {
|
||||
setShowHeader(!showHeader);
|
||||
}}
|
||||
renderHeader={renderHeader}
|
||||
imageUrls={[
|
||||
{
|
||||
url: image as string
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -128,7 +128,6 @@ 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,
|
||||
@@ -138,7 +137,7 @@ const Intro = ({ navigation }) => {
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7
|
||||
minHeight: height * 0.7 - (insets.top + insets.bottom)
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
|
||||
@@ -57,10 +57,7 @@ export const SectionHeader = React.memo<
|
||||
}: SectionHeaderProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const groupBy =
|
||||
strings.groupByStrings[
|
||||
groupOptions.groupBy as keyof typeof strings.groupByStrings
|
||||
]?.();
|
||||
const groupBy = strings.groupByStrings[groupOptions.groupBy]();
|
||||
const isCompactModeEnabled = useIsCompactModeEnabled(
|
||||
dataType as "note" | "notebook"
|
||||
);
|
||||
|
||||
@@ -76,7 +76,8 @@ const NoteItem = ({
|
||||
}: NoteItemProps) => {
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId ===
|
||||
item.id
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
|
||||
@@ -81,7 +81,8 @@ const ReminderItem = React.memo(
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap"
|
||||
flexWrap: "wrap",
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
{item.disabled ? (
|
||||
@@ -149,10 +150,8 @@ const ReminderItem = React.memo(
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
marginTop: 5
|
||||
height: 25,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -28,7 +28,8 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId ===
|
||||
item.id
|
||||
);
|
||||
|
||||
const [selected] = useIsSelected(item);
|
||||
|
||||
@@ -70,6 +70,7 @@ const SelectionWrapper = ({
|
||||
}
|
||||
|
||||
const onLongPress = () => {
|
||||
if (isSheet) return;
|
||||
if (useSelectionStore.getState().selectionMode !== item.type) {
|
||||
useSelectionStore.getState().setSelectionMode(item.type);
|
||||
}
|
||||
@@ -78,7 +79,7 @@ const SelectionWrapper = ({
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
customColor={isSheet ? colors.primary.hover : "transparent"}
|
||||
customColor={isSheet ? colors.secondary.background : "transparent"}
|
||||
testID={testID}
|
||||
onLongPress={onLongPress}
|
||||
onPress={onPress}
|
||||
|
||||
@@ -153,9 +153,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
},
|
||||
items?.cacheItem(index) ? 100 : 0
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Error", e);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
}, [index, items, refreshItem]);
|
||||
|
||||
@@ -297,7 +295,6 @@ function getDate(item: Item, groupType?: GroupingKey): number {
|
||||
groupType
|
||||
? db.settings.getGroupOptions(groupType)
|
||||
: {
|
||||
groupBy: "default",
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
},
|
||||
|
||||
@@ -119,7 +119,6 @@ export const PricingPlans = ({
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
console.log("error getting sku", e);
|
||||
}
|
||||
}, [promo?.promoCode]);
|
||||
|
||||
@@ -129,7 +128,10 @@ export const PricingPlans = ({
|
||||
if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = code;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
|
||||
skuId = await db.offers?.getCode(
|
||||
code.split(":")[0],
|
||||
Platform.OS as "ios" | "android"
|
||||
);
|
||||
}
|
||||
|
||||
const products = await PremiumService.getProducts();
|
||||
@@ -168,7 +170,6 @@ export const PricingPlans = ({
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("PROMOCODE ERROR:", code, e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -228,7 +229,6 @@ export const PricingPlans = ({
|
||||
});
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
close();
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: item,
|
||||
presistTab: true
|
||||
newTab: true
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
tabBarRef.current?.goToPage(1);
|
||||
|
||||
@@ -18,6 +18,7 @@ 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 {
|
||||
@@ -26,7 +27,7 @@ import {
|
||||
Platform,
|
||||
View
|
||||
} from "react-native";
|
||||
import Menu from "react-native-reanimated-material-menu/src/Menu";
|
||||
import { Menu } from "react-native-material-menu";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -46,7 +47,6 @@ 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,9 +101,11 @@ export const SelectionHeader = React.memo(
|
||||
};
|
||||
|
||||
const deleteItem = async () => {
|
||||
if (!type) return;
|
||||
presentDialog({
|
||||
title: strings.doActions.delete[type](selectedItemsList.length),
|
||||
paragraph: strings.actionConfirmations.delete[type](
|
||||
title: strings.doActions.delete.unknown(type, selectedItemsList.length),
|
||||
paragraph: strings.actionConfirmations.delete.unknown(
|
||||
type,
|
||||
selectedItemsList.length
|
||||
),
|
||||
positiveText: strings.delete(),
|
||||
@@ -218,14 +220,16 @@ export const SelectionHeader = React.memo(
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: contextMenuColors.primary.background,
|
||||
marginTop: -20
|
||||
marginTop: 35
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
}}
|
||||
anchor={
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.show();
|
||||
}}
|
||||
name="dots-vertical"
|
||||
@@ -292,15 +296,35 @@ export const SelectionHeader = React.memo(
|
||||
{
|
||||
title: strings.moveToTrash(),
|
||||
onPress: async () => {
|
||||
deleteItems(
|
||||
undefined,
|
||||
useSelectionStore.getState().selectionMode
|
||||
).then(() => {
|
||||
useSelectionStore.getState().clearSelection();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
});
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible: type !== "trash",
|
||||
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",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
@@ -319,9 +343,10 @@ export const SelectionHeader = React.memo(
|
||||
!item.visible ? null : (
|
||||
<Button
|
||||
style={{
|
||||
width: 150,
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 0
|
||||
borderRadius: 0,
|
||||
alignSelf: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
type="plain"
|
||||
buttonType={{
|
||||
@@ -331,6 +356,7 @@ 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();
|
||||
|
||||
@@ -100,7 +100,6 @@ export const AddNotebookSheet = ({
|
||||
eSendEvent(eOnNotebookUpdated, parent);
|
||||
if (notebook) {
|
||||
setImmediate(() => {
|
||||
console.log(parent, notebook.id);
|
||||
eSendEvent(eOnNotebookUpdated, notebook.id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,13 +90,12 @@ 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");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,8 +134,10 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
key="code-input"
|
||||
fwdRef={codeInputRef}
|
||||
placeholder={strings.verifyNewEmail()}
|
||||
placeholder={strings.code()}
|
||||
defaultValue=""
|
||||
onChangeText={(code) => {
|
||||
emailChangeData.current.code = code;
|
||||
}}
|
||||
|
||||
@@ -16,8 +16,8 @@ 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 { Note } from "@notesnook/core";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { EVENTS, Note } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
@@ -25,27 +25,18 @@ import { FlatList } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
TabItem,
|
||||
useTabStore
|
||||
} from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, presentSheet } from "../../../services/event-manager";
|
||||
import { eUnlockNote } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type TabItem = {
|
||||
id: number;
|
||||
noteId?: string;
|
||||
previewTab?: boolean;
|
||||
locked?: boolean;
|
||||
noteLocked?: boolean;
|
||||
readonly?: boolean;
|
||||
pinned?: boolean;
|
||||
};
|
||||
|
||||
const TabItemComponent = (props: {
|
||||
tab: TabItem;
|
||||
@@ -53,7 +44,7 @@ const TabItemComponent = (props: {
|
||||
close?: (ctx?: string | undefined) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [item, update] = useDBItem(props.tab.noteId, "note");
|
||||
const [item, update] = useDBItem(props.tab.session?.noteId, "note");
|
||||
|
||||
useEffect(() => {
|
||||
const syncCompletedSubscription = db.eventManager?.subscribe(
|
||||
@@ -85,11 +76,11 @@ const TabItemComponent = (props: {
|
||||
onPress={() => {
|
||||
if (!props.isFocused) {
|
||||
useTabStore.getState().focusTab(props.tab.id);
|
||||
if (props.tab.locked) {
|
||||
if (props.tab.session?.locked) {
|
||||
eSendEvent(eUnlockNote);
|
||||
}
|
||||
|
||||
if (!props.tab.noteId) {
|
||||
if (!props.tab.session?.noteId) {
|
||||
setTimeout(() => {
|
||||
editorController?.current?.commands?.focus(props.tab.id);
|
||||
}, 300);
|
||||
@@ -97,11 +88,6 @@ const TabItemComponent = (props: {
|
||||
}
|
||||
props.close?.();
|
||||
}}
|
||||
onLongPress={() => {
|
||||
useTabStore.getState().updateTab(props.tab.id, {
|
||||
previewTab: false
|
||||
});
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -112,9 +98,9 @@ const TabItemComponent = (props: {
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{props.tab.noteLocked ? (
|
||||
{props.tab.session?.noteLocked ? (
|
||||
<>
|
||||
{props.tab.locked ? (
|
||||
{props.tab.session?.locked ? (
|
||||
<Icon size={SIZE.md} name="lock" />
|
||||
) : (
|
||||
<Icon size={SIZE.md} name="lock-open-outline" />
|
||||
@@ -122,7 +108,9 @@ const TabItemComponent = (props: {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{props.tab.readonly ? <Icon size={SIZE.md} name="pencil-lock" /> : null}
|
||||
{props.tab.session?.readonly ? (
|
||||
<Icon size={SIZE.md} name="pencil-lock" />
|
||||
) : null}
|
||||
|
||||
<Paragraph
|
||||
color={
|
||||
@@ -130,15 +118,10 @@ const TabItemComponent = (props: {
|
||||
? colors.selected.paragraph
|
||||
: colors.primary.paragraph
|
||||
}
|
||||
style={{
|
||||
fontFamily: props.tab.previewTab
|
||||
? "OpenSans-Italic"
|
||||
: "OpenSans-Regular"
|
||||
}}
|
||||
numberOfLines={1}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{props.tab.noteId
|
||||
{props.tab.session?.noteId
|
||||
? item?.title || strings.untitledNote()
|
||||
: strings.newNote()}
|
||||
</Paragraph>
|
||||
@@ -157,8 +140,7 @@ const TabItemComponent = (props: {
|
||||
color={props.tab.pinned ? colors.primary.accent : colors.primary.icon}
|
||||
onPress={() => {
|
||||
useTabStore.getState().updateTab(props.tab.id, {
|
||||
pinned: !props.tab.pinned,
|
||||
previewTab: false
|
||||
pinned: !props.tab.pinned
|
||||
});
|
||||
}}
|
||||
top={0}
|
||||
@@ -197,6 +179,7 @@ export default function EditorTabs({
|
||||
}: {
|
||||
close?: (ctx?: string | undefined) => void;
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const [tabs, currentTab] = useTabStore((state) => [
|
||||
state.tabs,
|
||||
state.currentTab
|
||||
@@ -233,25 +216,13 @@ export default function EditorTabs({
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>{strings.tabs()}</Heading>
|
||||
<Button
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
useTabStore.getState().newTab();
|
||||
setTimeout(() => {
|
||||
editorController?.current?.commands?.focus(
|
||||
useTabStore.getState().currentTab
|
||||
);
|
||||
}, 500);
|
||||
close?.();
|
||||
}}
|
||||
title={strings.newTab()}
|
||||
icon="plus"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 100,
|
||||
height: 35
|
||||
}}
|
||||
iconSize={SIZE.lg}
|
||||
name="plus"
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -333,13 +333,17 @@ const ExportNotesSheet = ({
|
||||
if (Platform.OS === "ios") {
|
||||
Share.open({
|
||||
url: result?.fileDir + result.fileName
|
||||
}).catch(console.log);
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(result.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any).catch(console.log);
|
||||
} as any).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -62,7 +62,9 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
}).catch(console.log);
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -22,24 +22,22 @@ import {
|
||||
VirtualizedGrouping,
|
||||
createInternalLink
|
||||
} from "@notesnook/core";
|
||||
import type { LinkAttributes } from "@notesnook/editor";
|
||||
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import type { LinkAttributes } from "@notesnook/editor";
|
||||
import {
|
||||
EditorEvents,
|
||||
editorController
|
||||
} from "../../../screens/editor/tiptap/utils";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
@@ -194,7 +192,7 @@ export default function LinkNote(props: {
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
editorController.current?.postMessage(EditorEvents.resolve, {
|
||||
editorController.current?.postMessage(NativeEvents.resolve, {
|
||||
data: {
|
||||
href: link,
|
||||
title: selectedNote.title
|
||||
|
||||
@@ -113,11 +113,9 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -301,8 +301,8 @@ export const NotebookSheet = () => {
|
||||
}}
|
||||
onPress={async () => {
|
||||
await deleteItems(
|
||||
useItemSelectionStore.getState().getSelectedItemIds(),
|
||||
"notebook"
|
||||
"notebook",
|
||||
useItemSelectionStore.getState().getSelectedItemIds()
|
||||
);
|
||||
useSelectionStore.getState().clearSelection();
|
||||
useItemSelectionStore.setState({
|
||||
|
||||
@@ -150,7 +150,7 @@ const PublishNoteSheet = ({
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{isPublished && (
|
||||
{isPublished && publishUrl ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -204,7 +204,7 @@ const PublishNoteSheet = ({
|
||||
name="content-copy"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
|
||||
@@ -17,12 +17,17 @@ 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,
|
||||
@@ -30,8 +35,6 @@ 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";
|
||||
@@ -41,9 +44,6 @@ 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 Storage.checkAndCreateDir("/");
|
||||
path = await filesystem.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 Storage.checkAndCreateDir("/");
|
||||
path = await filesystem.checkAndCreateDir("/");
|
||||
await RNFetchBlob.fs.writeFile(path + fileName, this.state.key, "utf8");
|
||||
path = path + fileName;
|
||||
}
|
||||
@@ -191,13 +191,17 @@ class RecoveryKeySheet extends React.Component {
|
||||
Share.open({
|
||||
url: path,
|
||||
failOnCancel: false
|
||||
}).catch(console.log);
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(path, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
}).catch(console.log);
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -273,7 +273,7 @@ const ListNoteItem = ({
|
||||
listType,
|
||||
noteInternalLinks.length
|
||||
]);
|
||||
console.log(noteInternalLinks);
|
||||
|
||||
const renderBlock = React.useCallback(
|
||||
(block: ContentBlock) => (
|
||||
<ListBlockItem
|
||||
@@ -419,7 +419,6 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => {
|
||||
id={index}
|
||||
items={items}
|
||||
onSelect={(note, blockId) => {
|
||||
console.log(note.id, blockId);
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note,
|
||||
blockId: blockId
|
||||
|
||||
@@ -88,14 +88,14 @@ export const RelationsList = ({
|
||||
sortDirection: "desc"
|
||||
})
|
||||
.then((grouped) => {
|
||||
setItems(grouped);
|
||||
setTimeout(() => {
|
||||
setItems(grouped);
|
||||
}, 300);
|
||||
});
|
||||
}, [relationType, referenceType, item?.id, item?.type]);
|
||||
}, [relationType, referenceType, item?.id, item?.type, updater]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ paddingHorizontal: 12, height: hasNoRelations ? 300 : "100%" }}
|
||||
>
|
||||
<View style={{ paddingHorizontal: 12, height: "100%" }}>
|
||||
<SheetProvider context="local" />
|
||||
<DialogHeader
|
||||
title={title}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
|
||||
@@ -124,9 +124,11 @@ export default function ReminderSheet({
|
||||
const referencedItem = reference ? (reference as Note) : null;
|
||||
|
||||
const title = useRef<string | undefined>(
|
||||
reminder?.title || referencedItem?.title
|
||||
!reminder ? referencedItem?.title : reminder?.title
|
||||
);
|
||||
const details = useRef<string | undefined>(
|
||||
!reminder ? referencedItem?.headline : reminder?.description
|
||||
);
|
||||
const details = useRef<string | undefined>(reminder?.description);
|
||||
const titleRef = useRef<TextInput>(null);
|
||||
const timer = useRef<NodeJS.Timeout>();
|
||||
|
||||
@@ -543,7 +545,7 @@ export default function ReminderSheet({
|
||||
/>
|
||||
|
||||
{reminderMode === ReminderModes.Permanent ? null : (
|
||||
<ScrollView
|
||||
<RNScrollView
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginTop: 12,
|
||||
@@ -554,9 +556,9 @@ export default function ReminderSheet({
|
||||
{Object.keys(ReminderNotificationModes).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
title={strings.reminderNotificationModes[
|
||||
mode as keyof typeof strings.reminderNotificationModes
|
||||
]()}
|
||||
title={strings.reminderNotificationModes(
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
)}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
borderRadius: 100
|
||||
@@ -588,7 +590,7 @@ export default function ReminderSheet({
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</RNScrollView>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Update = ({ version: appVersion, fwdRef }) => {
|
||||
let notes = version?.notes
|
||||
? version.notes.replace("Thank you for using Notesnook!", "").split("- ")
|
||||
: ["Bug fixes and performance improvements"];
|
||||
notes = notes?.map((n) => n.replace(/\n/g, ""));
|
||||
notes = notes?.map((n) => n.replace(/\n|<br>/g, ""));
|
||||
const isGithubRelease = Config.GITHUB_RELEASE === "true";
|
||||
|
||||
const getSupportedAbi = () => {
|
||||
@@ -178,18 +178,20 @@ export const Update = ({ version: appVersion, fwdRef }) => {
|
||||
{version.body}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{notes.map((item) => (
|
||||
<Paragraph
|
||||
key={item}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginBottom: 5
|
||||
}}
|
||||
selectable
|
||||
>
|
||||
• {item}
|
||||
</Paragraph>
|
||||
))}
|
||||
{notes.map((item) =>
|
||||
item && item !== "" ? (
|
||||
<Paragraph
|
||||
key={item}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginBottom: 5
|
||||
}}
|
||||
selectable
|
||||
>
|
||||
• {item}
|
||||
</Paragraph>
|
||||
) : null
|
||||
)}
|
||||
</ScrollView>
|
||||
<Seperator />
|
||||
<Button
|
||||
@@ -215,7 +217,9 @@ export const Update = ({ version: appVersion, fwdRef }) => {
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(GITHUB_PAGE_URL).catch(console.log);
|
||||
Linking.openURL(GITHUB_PAGE_URL).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.readReleaseNotes()}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React 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>
|
||||
);
|
||||
};
|
||||
@@ -263,20 +263,22 @@ export const Pressable = ({
|
||||
const getStyle = useCallback(
|
||||
({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [
|
||||
{
|
||||
backgroundColor: pressed
|
||||
? RGB_Linear_Shade(alpha, hexToRGBA(selectedColor, opacity || 1))
|
||||
: hexToRGBA(primaryColor, opacity || 1),
|
||||
backgroundColor:
|
||||
pressed && !disabled
|
||||
? RGB_Linear_Shade(alpha, hexToRGBA(selectedColor, opacity || 1))
|
||||
: hexToRGBA(primaryColor, opacity || 1),
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
borderRadius: noborder ? 0 : br,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: 0,
|
||||
borderColor: pressed
|
||||
? customSelectedColor
|
||||
? getColorLinearShade(customSelectedColor, 0.3, false)
|
||||
: borderSelectedColor || borderColor
|
||||
: borderColor || "transparent",
|
||||
borderColor:
|
||||
pressed && !disabled
|
||||
? customSelectedColor
|
||||
? getColorLinearShade(customSelectedColor, 0.3, false)
|
||||
: borderSelectedColor || borderColor
|
||||
: borderColor || "transparent",
|
||||
borderWidth: borderWidth
|
||||
},
|
||||
style,
|
||||
@@ -298,7 +300,8 @@ export const Pressable = ({
|
||||
borderColor,
|
||||
borderWidth,
|
||||
style,
|
||||
growFactor
|
||||
growFactor,
|
||||
disabled
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ export const ReminderTime = ({
|
||||
<Button
|
||||
title={time}
|
||||
key={reminder.id}
|
||||
height={20}
|
||||
icon="bell"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
@@ -67,12 +66,10 @@ 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}
|
||||
|
||||
@@ -59,8 +59,9 @@ 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(() => {
|
||||
@@ -99,24 +100,21 @@ const SheetWrapper = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [appState, fwdRef]);
|
||||
}, [locked, fwdRef]);
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider value="sheet">
|
||||
|
||||
@@ -18,15 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { DimensionValue, View } from "react-native";
|
||||
import { SvgXml } from "./lazy";
|
||||
export const SvgView = ({
|
||||
width = 250,
|
||||
height = 250,
|
||||
src
|
||||
}: {
|
||||
width?: number | string;
|
||||
height?: number | number;
|
||||
width?: DimensionValue;
|
||||
height?: DimensionValue;
|
||||
src?: string;
|
||||
}) => {
|
||||
if (!src) return null;
|
||||
|
||||
@@ -315,7 +315,9 @@ const trialstarted: { id: string; steps: TStep[] } = {
|
||||
actionButton: {
|
||||
text: strings.joinDiscord(),
|
||||
action: () => {
|
||||
Linking.openURL("https://discord.gg/zQBK97EE22").catch(console.log);
|
||||
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,7 +368,9 @@ const Support = () => {
|
||||
width: "90%"
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL("https://discord.gg/zQBK97EE22").catch(console.log);
|
||||
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}}
|
||||
icon="discord"
|
||||
type="secondary"
|
||||
@@ -380,7 +384,9 @@ const Support = () => {
|
||||
width: "90%"
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL("https://t.me/notesnook").catch(console.log);
|
||||
Linking.openURL("https://t.me/notesnook").catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}}
|
||||
icon="telegram"
|
||||
type="secondary"
|
||||
|
||||
@@ -19,14 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/* eslint-disable no-inner-declarations */
|
||||
import {
|
||||
Color,
|
||||
createInternalLink,
|
||||
ItemReference,
|
||||
Note,
|
||||
Notebook,
|
||||
Reminder,
|
||||
Tag,
|
||||
TrashItem,
|
||||
VAULT_ERRORS,
|
||||
createInternalLink
|
||||
VAULT_ERRORS
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DisplayedNotification } from "@notifee/react-native";
|
||||
@@ -50,11 +50,11 @@ import ReminderSheet from "../components/sheets/reminder";
|
||||
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
openVault,
|
||||
presentSheet
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../services/event-manager";
|
||||
import Navigation from "../services/navigation";
|
||||
import Notifications from "../services/notifications";
|
||||
@@ -254,8 +254,8 @@ export const useActions = ({
|
||||
item.type === "color"
|
||||
) {
|
||||
presentDialog({
|
||||
title: strings.doActions.delete[item.type](1),
|
||||
paragraph: strings.actionConfirmations.delete[item.type](1),
|
||||
title: strings.doActions.delete.unknown(item.type, 1),
|
||||
paragraph: strings.actionConfirmations.delete.unknown(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.id], item.type);
|
||||
await deleteItems(item.type, [item.id]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -303,8 +303,8 @@ export const useActions = ({
|
||||
close();
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
title: strings.doActions.delete[item.itemType](1),
|
||||
paragraph: strings.actionConfirmations.delete[item.itemType](1),
|
||||
title: strings.doActions.delete.unknown(item.itemType, 1),
|
||||
paragraph: strings.actionConfirmations.delete.unknown(item.itemType, 1),
|
||||
positiveText: strings.delete(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
@@ -313,10 +313,7 @@ export const useActions = ({
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
heading:
|
||||
strings.actions.deleted[
|
||||
item.itemType as keyof typeof strings.actions.deleted
|
||||
](1),
|
||||
heading: strings.actions.deleted.unknown(item.itemType, 1),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -547,14 +544,13 @@ export const useActions = ({
|
||||
const toggleReadyOnlyMode = async () => {
|
||||
const currentReadOnly = (item as Note).readonly;
|
||||
await db.notes.readonly(!currentReadOnly, item?.id);
|
||||
|
||||
if (useTabStore.getState().hasTabForNote(item.id)) {
|
||||
const tabId = useTabStore.getState().getTabForNote(item.id);
|
||||
if (!tabId) return;
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
readonly: !currentReadOnly
|
||||
useTabStore.getState().forEachNoteTab(item.id, (tab) => {
|
||||
useTabStore.getState().updateTab(tab.id, {
|
||||
session: {
|
||||
readonly: !currentReadOnly
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
close();
|
||||
};
|
||||
@@ -957,11 +953,10 @@ export const useActions = ({
|
||||
id: "trash",
|
||||
title:
|
||||
item.type !== "notebook" && item.type !== "note"
|
||||
? strings.doActions.delete[
|
||||
item.type === "trash"
|
||||
? item.itemType
|
||||
: (item.type as keyof typeof strings.doActions.delete)
|
||||
](1)
|
||||
? strings.doActions.delete.unknown(
|
||||
item.type === "trash" ? item.itemType : item.type,
|
||||
1
|
||||
)
|
||||
: strings.moveToTrash(),
|
||||
icon: "delete-outline",
|
||||
type: "error",
|
||||
|
||||
@@ -17,9 +17,15 @@ 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 { User } from "@notesnook/core";
|
||||
import { EV, EVENTS, SYNC_CHECK_IDS, SyncStatusEvent } from "@notesnook/core";
|
||||
import { EventManagerSubscription } from "@notesnook/core";
|
||||
import {
|
||||
EV,
|
||||
EVENTS,
|
||||
EventManagerSubscription,
|
||||
SYNC_CHECK_IDS,
|
||||
SyncStatusEvent,
|
||||
User
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import notifee from "@notifee/react-native";
|
||||
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
@@ -41,17 +47,24 @@ 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 ReminderSheet from "../components/sheets/reminder";
|
||||
import { Walkthrough } from "../components/walkthroughs";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
resetTabStore,
|
||||
useTabStore
|
||||
} from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
clearAppState,
|
||||
editorController,
|
||||
editorState
|
||||
editorState,
|
||||
setAppState
|
||||
} 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,
|
||||
@@ -66,14 +79,17 @@ 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 { initAfterSync } from "../stores";
|
||||
import { clearAllStores, initAfterSync } from "../stores";
|
||||
import { refreshAllStores } from "../stores/create-db-collection-store";
|
||||
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";
|
||||
@@ -83,17 +99,13 @@ import {
|
||||
eLoginSessionExpired,
|
||||
eOnLoadNote,
|
||||
eOpenAnnouncementDialog,
|
||||
eUnlockNote,
|
||||
eUserLoggedIn,
|
||||
refreshNotesPage
|
||||
} from "../utils/events";
|
||||
import { getGithubVersion } from "../utils/github-version";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import { sleep } from "../utils/time";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { changeSystemBarColors } from "../stores/use-theme-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { endProgress, startProgress } from "../components/dialogs/progress";
|
||||
import { sleep } from "../utils/time";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -148,6 +160,7 @@ const onUserSessionExpired = async () => {
|
||||
|
||||
const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
const url = event.url;
|
||||
|
||||
try {
|
||||
if (url.startsWith("https://app.notesnook.com/account/verified")) {
|
||||
await onUserEmailVerified();
|
||||
@@ -157,6 +170,25 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
tabBarRef.current?.goToPage(1, false);
|
||||
return;
|
||||
} else if (url.startsWith("https://notesnook.com/open_note")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const note = await db.notes.note(id);
|
||||
if (note) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
tabBarRef.current?.goToPage(1, false);
|
||||
}
|
||||
}
|
||||
} else if (url.startsWith("https://notesnook.com/open_reminder")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const reminder = await db.reminders.reminder(id);
|
||||
if (reminder) ReminderSheet.present(reminder);
|
||||
}
|
||||
} else if (url.startsWith("https://notesnook.com/new_reminder")) {
|
||||
ReminderSheet.present();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -208,11 +240,20 @@ const onRequestPartialSync = async (
|
||||
};
|
||||
|
||||
const onLogout = async (reason: string) => {
|
||||
DatabaseLogger.log("User Logged Out" + reason);
|
||||
Notifications.setupReminders(true);
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
DatabaseLogger.log("User Logged Out " + reason);
|
||||
setLoginMessage();
|
||||
await PremiumService.setPremiumStatus();
|
||||
await BiometricService.resetCredentials();
|
||||
MMKV.clearStore();
|
||||
resetTabStore();
|
||||
clearAllStores();
|
||||
setImmediate(() => {
|
||||
refreshAllStores();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
SettingsService.resetSettings();
|
||||
useUserStore.getState().setUser(null);
|
||||
useUserStore.getState().setSyncing(false);
|
||||
};
|
||||
|
||||
async function checkForShareExtensionLaunchedInBackground() {
|
||||
@@ -237,9 +278,7 @@ async function checkForShareExtensionLaunchedInBackground() {
|
||||
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
|
||||
MMKV.removeItem("shareExtensionOpened");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function saveEditorState() {
|
||||
@@ -480,12 +519,14 @@ export const useAppEvents = () => {
|
||||
}
|
||||
|
||||
if (fullBackup) {
|
||||
await BackupService.run(true, undefined, "full");
|
||||
await BackupService.run(false, undefined, "full");
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsService.getProperty("offlineMode")) {
|
||||
db.attachments.cacheAttachments().catch(console.log);
|
||||
db.attachments.cacheAttachments().catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@@ -533,7 +574,6 @@ export const useAppEvents = () => {
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ToastManager.error(e as Error, "Error updating user", "global");
|
||||
}
|
||||
|
||||
@@ -581,7 +621,8 @@ export const useAppEvents = () => {
|
||||
EV.subscribe(EVENTS.migrationStarted, (name) => {
|
||||
if (
|
||||
name !== "notesnook" ||
|
||||
!SettingsService.getProperty("introCompleted")
|
||||
!SettingsService.getProperty("introCompleted") ||
|
||||
Config.isTesting === "true"
|
||||
)
|
||||
return;
|
||||
startProgress({
|
||||
@@ -594,7 +635,8 @@ export const useAppEvents = () => {
|
||||
EV.subscribe(EVENTS.migrationFinished, (name) => {
|
||||
if (
|
||||
name !== "notesnook" ||
|
||||
!SettingsService.getProperty("introCompleted")
|
||||
!SettingsService.getProperty("introCompleted") ||
|
||||
Config.isTesting === "true"
|
||||
)
|
||||
return;
|
||||
endProgress();
|
||||
@@ -602,22 +644,17 @@ export const useAppEvents = () => {
|
||||
EV.subscribe(EVENTS.vaultLocked, async () => {
|
||||
// Lock all notes in all tabs...
|
||||
for (const tab of useTabStore.getState().tabs) {
|
||||
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
|
||||
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
|
||||
if (!noteId) continue;
|
||||
const note = await db.notes.note(noteId);
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) {
|
||||
useTabStore.getState().updateTab(tab.id, {
|
||||
locked: true
|
||||
session: {
|
||||
locked: true,
|
||||
noteLocked: true
|
||||
}
|
||||
});
|
||||
if (
|
||||
tab.id === useTabStore.getState().currentTab &&
|
||||
locked &&
|
||||
!editorState().movedAway
|
||||
) {
|
||||
// Show unlock note screen.
|
||||
eSendEvent(eUnlockNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -626,6 +663,7 @@ export const useAppEvents = () => {
|
||||
|
||||
const emitterSubscriptions = [
|
||||
Linking.addEventListener("url", onAppOpenedFromURL),
|
||||
|
||||
SodiumEventEmitter.addListener(
|
||||
"onSodiumProgress",
|
||||
onFileEncryptionProgress
|
||||
@@ -711,13 +749,15 @@ export const useAppEvents = () => {
|
||||
setTimeout(() => {
|
||||
sub = AppState.addEventListener("change", onAppStateChanged);
|
||||
if (
|
||||
refValues.current.initialUrl?.startsWith(
|
||||
"https://app.notesnook.com/account/verified"
|
||||
)
|
||||
refValues.current.initialUrl &&
|
||||
!refValues.current.initialUrl?.includes("open_note")
|
||||
) {
|
||||
onUserEmailVerified();
|
||||
onAppOpenedFromURL({
|
||||
url: refValues.current.initialUrl!
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
refValues.current.removeInternetStateListener = NetInfo.addEventListener(
|
||||
onInternetStateChanged
|
||||
);
|
||||
@@ -760,9 +800,7 @@ export const useAppEvents = () => {
|
||||
useEffect(() => {
|
||||
if (!appLocked && isAppLoading) {
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.catch((e) => {})
|
||||
.finally(() => {
|
||||
//@ts-ignore
|
||||
initializeDatabase();
|
||||
|
||||
@@ -32,18 +32,11 @@ export function useAppState() {
|
||||
const subscription = AppState.addEventListener("change", onChange);
|
||||
|
||||
return () => {
|
||||
// @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);
|
||||
}
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return appState;
|
||||
}
|
||||
|
||||
export { AppStateStatus };
|
||||
export type { AppStateStatus };
|
||||
|
||||
@@ -196,7 +196,6 @@ export const useTotalNotes = (type: "notebook" | "tag" | "color") => {
|
||||
}
|
||||
setTotalNotesById(totalNotesById);
|
||||
});
|
||||
console.log("useTotalNotes.getTotalNotes");
|
||||
},
|
||||
[type]
|
||||
);
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Keyboard, KeyboardEventListener, ScreenRect } from "react-native";
|
||||
import { Keyboard, KeyboardEventListener, KeyboardMetrics } 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 | ScreenRect;
|
||||
end: ScreenRect;
|
||||
start: undefined | KeyboardMetrics;
|
||||
end: KeyboardMetrics;
|
||||
}>(initialValue);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
|
||||
|
||||
|
||||
@@ -74,10 +74,8 @@ 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);
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
@@ -44,7 +45,6 @@ import useNavigationStore from "../stores/use-navigation-store";
|
||||
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";
|
||||
|
||||
const NativeStack = createNativeStackNavigator();
|
||||
const IntroStack = createNativeStackNavigator();
|
||||
@@ -76,11 +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(() => {
|
||||
setTimeout(async () => {
|
||||
useNavigationStore.getState().update(homepage);
|
||||
}, 1000);
|
||||
}, [homepage]);
|
||||
|
||||
@@ -97,7 +97,7 @@ const _TabsHolder = () => {
|
||||
tabBarRef.current?.goToPage(1, false);
|
||||
return;
|
||||
}
|
||||
if (item.type === "notesnook.action.newnote") {
|
||||
if (item && item.type === "notesnook.action.newnote") {
|
||||
clearAppState();
|
||||
if (!tabBarRef.current) {
|
||||
await sleep(3000);
|
||||
@@ -509,7 +509,8 @@ const onChangeTab = async (event) => {
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
useTabStore.getState().getTab(useTabStore.getState().currentTab).locked
|
||||
useTabStore.getState().getTab(useTabStore.getState().currentTab).session
|
||||
?.locked
|
||||
) {
|
||||
eSendEvent(eUnlockNote);
|
||||
}
|
||||
@@ -523,13 +524,15 @@ const onChangeTab = async (event) => {
|
||||
|
||||
// Lock all tabs with locked notes...
|
||||
for (const tab of useTabStore.getState().tabs) {
|
||||
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
|
||||
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
|
||||
if (!noteId) continue;
|
||||
const note = await db.notes.note(noteId);
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) {
|
||||
useTabStore.getState().updateTab(tab.id, {
|
||||
locked: true
|
||||
session: {
|
||||
locked: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,19 @@
|
||||
"@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",
|
||||
@@ -35,15 +33,18 @@
|
||||
"@readme/data-urls": "3.0.0",
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"@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/react": "4.11.2",
|
||||
"@lingui/core": "4.11.2"
|
||||
"@lingui/core": "5.1.2",
|
||||
"@lingui/react": "5.1.2",
|
||||
"react-native-check-version": "^1.3.0",
|
||||
"react-native-material-menu": "^2.0.0",
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/react-query": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"async-mutex": "0.5.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
import { i18n } from "@lingui/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
eUnlockWithPassword
|
||||
} from "../../utils/events";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import EditorOverlay from "./loading";
|
||||
import { EDITOR_URI } from "./source";
|
||||
import { EditorProps, useEditorType } from "./tiptap/types";
|
||||
@@ -58,9 +61,6 @@ import {
|
||||
openInternalLink,
|
||||
randId
|
||||
} from "./tiptap/utils";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -203,11 +203,13 @@ const useLockedNoteHandler = () => {
|
||||
|
||||
useEffect(() => {
|
||||
for (const tab of useTabStore.getState().tabs) {
|
||||
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
|
||||
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
|
||||
if (!noteId) continue;
|
||||
if (tabRef.current && tabRef.current.noteLocked) {
|
||||
if (tabRef.current && tabRef.current.session?.noteLocked) {
|
||||
useTabStore.getState().updateTab(tabRef.current.id, {
|
||||
locked: true
|
||||
session: {
|
||||
locked: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -221,33 +223,38 @@ const useLockedNoteHandler = () => {
|
||||
biometryAvailable: !!biometry,
|
||||
biometryEnrolled: !!fingerprint
|
||||
});
|
||||
syncTabs();
|
||||
syncTabs("biometry");
|
||||
})();
|
||||
}, [tab?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const unlockWithBiometrics = async () => {
|
||||
try {
|
||||
if (!tabRef.current?.noteLocked || !tabRef.current) return;
|
||||
if (!tabRef.current?.session?.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."
|
||||
);
|
||||
|
||||
if (credentials && credentials?.password && tabRef.current.noteId) {
|
||||
if (
|
||||
credentials &&
|
||||
credentials?.password &&
|
||||
tabRef.current.session?.noteId
|
||||
) {
|
||||
const note = await db.vault.open(
|
||||
tabRef.current.noteId,
|
||||
tabRef.current.session?.noteId,
|
||||
credentials?.password
|
||||
);
|
||||
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
|
||||
useTabStore.getState().updateTab(tabRef.current.id, {
|
||||
locked: false
|
||||
item: note,
|
||||
refresh: true
|
||||
});
|
||||
} else {
|
||||
if (tabRef.current && tabRef.current.session?.locked) {
|
||||
editorController.current?.commands.focusPassInput();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -261,7 +268,7 @@ const useLockedNoteHandler = () => {
|
||||
password: string;
|
||||
biometrics?: boolean;
|
||||
}) => {
|
||||
if (!tabRef.current?.noteId || !tabRef.current) return;
|
||||
if (!tabRef.current?.session?.noteId || !tabRef.current) return;
|
||||
if (!password || password.trim().length === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordNotEntered(),
|
||||
@@ -271,7 +278,10 @@ const useLockedNoteHandler = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const note = await db.vault.open(tabRef.current?.noteId, password);
|
||||
const note = await db.vault.open(
|
||||
tabRef.current?.session?.noteId,
|
||||
password
|
||||
);
|
||||
if (enrollBiometrics && note) {
|
||||
try {
|
||||
const unlocked = await db.vault.unlock(password);
|
||||
@@ -299,13 +309,10 @@ const useLockedNoteHandler = () => {
|
||||
}
|
||||
}
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
useTabStore.getState().updateTab(tabRef.current.id, {
|
||||
locked: false
|
||||
item: note,
|
||||
refresh: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error"
|
||||
@@ -315,7 +322,7 @@ const useLockedNoteHandler = () => {
|
||||
|
||||
const unlock = () => {
|
||||
if (
|
||||
(tabRef.current?.locked,
|
||||
(tabRef.current?.session?.locked,
|
||||
useTabStore.getState().biometryAvailable &&
|
||||
useTabStore.getState().biometryEnrolled &&
|
||||
!editorState().movedAway)
|
||||
@@ -324,10 +331,9 @@ const useLockedNoteHandler = () => {
|
||||
unlockWithBiometrics();
|
||||
}, 150);
|
||||
} else {
|
||||
console.log("Biometrics unavailable.", editorState().movedAway);
|
||||
if (!editorState().movedAway) {
|
||||
setTimeout(() => {
|
||||
if (tabRef.current && tabRef.current?.locked) {
|
||||
if (tabRef.current && tabRef.current?.session?.locked) {
|
||||
editorController.current?.commands.focus(tabRef.current?.id);
|
||||
}
|
||||
}, 100);
|
||||
@@ -342,13 +348,13 @@ const useLockedNoteHandler = () => {
|
||||
}),
|
||||
eSubscribeEvent(eUnlockWithPassword, onSubmit)
|
||||
];
|
||||
if (tabRef.current?.locked && tabBarRef.current?.page() === 2) {
|
||||
if (tabRef.current?.session?.locked && tabBarRef.current?.page() === 2) {
|
||||
unlock();
|
||||
}
|
||||
return () => {
|
||||
subs.map((s) => s?.unsubscribe());
|
||||
};
|
||||
}, [tab?.id, tab?.locked]);
|
||||
}, [tab?.id, tab?.session?.locked]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -27,13 +27,14 @@ import WebView from "react-native-webview";
|
||||
import { useRef } from "react";
|
||||
import { EDITOR_URI } from "./source";
|
||||
import { EditorMessage } from "./tiptap/types";
|
||||
import { EventTypes } from "./tiptap/editor-events";
|
||||
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { EditorEvents } from "./tiptap/utils";
|
||||
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
||||
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")) {
|
||||
@@ -68,12 +69,11 @@ export function ReadonlyEditor(props: {
|
||||
const data = event.nativeEvent.data;
|
||||
const editorMessage = JSON.parse(data) as EditorMessage<any>;
|
||||
|
||||
if (editorMessage.type === EventTypes.logger) {
|
||||
if (editorMessage.type === EditorEvents.logger) {
|
||||
logger.info("[READONLY EDITOR LOG]", editorMessage.value);
|
||||
}
|
||||
|
||||
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
|
||||
console.log("Readonly editor loaded.");
|
||||
if (editorMessage.type === EditorEvents.readonlyEditorLoaded) {
|
||||
props.onLoad?.((content: { data: string; id: string }) => {
|
||||
setTimeout(() => {
|
||||
noteId.current = content.id;
|
||||
@@ -86,10 +86,9 @@ export function ReadonlyEditor(props: {
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.getAttachmentData) {
|
||||
} else if (editorMessage.type === EditorEvents.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",
|
||||
@@ -105,7 +104,7 @@ export function ReadonlyEditor(props: {
|
||||
);
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: EditorEvents.attachmentData,
|
||||
type: NativeEvents.attachmentData,
|
||||
value: {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data
|
||||
@@ -114,10 +113,9 @@ export function ReadonlyEditor(props: {
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("Error downloading attachment data");
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: EditorEvents.attachmentData,
|
||||
type: NativeEvents.attachmentData,
|
||||
data: {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data: undefined
|
||||
@@ -143,10 +141,17 @@ export function ReadonlyEditor(props: {
|
||||
ref={editorRef}
|
||||
key={"readonly-editor:" + props.editorId}
|
||||
nestedScrollEnabled
|
||||
injectedJavaScriptBeforeContentLoaded={`globalThis.readonlyEditor=true;`}
|
||||
injectedJavaScript="globalThis.readonlyEditor=true;"
|
||||
injectedJavaScript={`
|
||||
globalThis.__DEV__ = ${__DEV__}
|
||||
globalThis.readonlyEditor=true;
|
||||
globalThis.LINGUI_LOCALE = "${i18n.locale}";
|
||||
globalThis.LINGUI_LOCALE_DATA = ${JSON.stringify({
|
||||
[i18n.locale]: i18n.messages
|
||||
})};
|
||||
globalThis.loadApp();`}
|
||||
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
Reference in New Issue
Block a user