Compare commits

..

1 Commits

Author SHA1 Message Date
ammarahm-ed
74a0a117e6 editor: fix code-block ui 2023-09-11 12:35:04 +05:00
874 changed files with 121931 additions and 162624 deletions

View File

@@ -15,8 +15,6 @@ apps/mobile/e2e/
# web
apps/web/public/an.js
apps/web/src/common/sqlite/wa-sqlite-async.js
apps/web/src/common/sqlite/wa-sqlite.js
# editor
packages/editor/styles/

View File

@@ -78,13 +78,7 @@ module.exports = {
semi: ["error", "always"],
"@typescript-eslint/no-empty-function": "off",
"react/prop-types": "off",
"header/header": ["error", "block", LICENSE, 1],
"@typescript-eslint/no-empty-interface": [
"error",
{
allowSingleExtends: true
}
]
"header/header": ["error", "block", LICENSE, 1]
},
settings: {
react: {

View File

@@ -7,7 +7,7 @@ runs:
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 20.x
node-version: 16.x
cache: "npm"
cache-dependency-path: |
package-lock.json

View File

@@ -97,7 +97,6 @@ jobs:
- name: Get App Store Version
id: appstore
uses: streetwriters/appstore-connect-app-version@develop
if: inputs.publish-apple
with:
app-id: ${{ steps.app_metadata.outputs.apple_app_id }}
key-id: ${{ secrets.api_key_id }}

View File

@@ -1,3 +0,0 @@
owner: streetwriters
repo: notesnook
provider: github

View File

@@ -1,18 +1,18 @@
{
"name": "@notesnook/desktop",
"version": "2.6.13",
"version": "2.6.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.6.13",
"version": "2.6.3",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.38.3",
"@trpc/server": "10.38.3",
"@trpc/client": "10.31.0",
"@trpc/server": "10.31.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.4",
"icojs": "^0.17.1",
@@ -24,8 +24,8 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.9.8",
"electron-builder": "^24.9.1",
"electron": "25.8.0",
"electron-builder": "^24.6.4",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"tree-kill": "^1.2.2",
@@ -173,11 +173,12 @@
}
},
"node_modules/@electron/asar": {
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz",
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz",
"integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==",
"dev": true,
"dependencies": {
"chromium-pickle-js": "^0.2.0",
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
@@ -215,7 +216,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz",
"integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
@@ -236,7 +236,6 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
@@ -250,7 +249,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@@ -259,7 +257,6 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -268,7 +265,6 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
@@ -802,7 +798,6 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -814,7 +809,6 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"dependencies": {
"defer-to-connect": "^2.0.0"
},
@@ -837,20 +831,20 @@
}
},
"node_modules/@trpc/client": {
"version": "10.38.3",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.38.3.tgz",
"integrity": "sha512-hHPsElTL4sB+UGzuAZ4iwRVHjELrk/Nsl2owsYw9ITJz3EY7VWRsQ6vK48kJvlHPYPhP+efID5UOgyZMs4bFXA==",
"version": "10.31.0",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.31.0.tgz",
"integrity": "sha512-VCqbJEvFJb8C4hQFw7AD+dkQTjgEdV/QAzO4D+/cX5e93u5NpfNXI+PKS0QFXwG/zqgwQwVV6OkYc/D/MFwA6g==",
"funding": [
"https://trpc.io/sponsor"
],
"peerDependencies": {
"@trpc/server": "10.38.3"
"@trpc/server": "10.31.0"
}
},
"node_modules/@trpc/server": {
"version": "10.38.3",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.38.3.tgz",
"integrity": "sha512-9s8/kwo2IDB5hwB2SKZZrfevRhdb1f9fdXtIYd3lbQuf2jQaC/LyQuHaIQjDQoUx9updBfsHXcFFPiCP1DL6pg==",
"version": "10.31.0",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.31.0.tgz",
"integrity": "sha512-9EnRTSDE9nF11LZsvSOqNKqkRYzHqFX4ch5AJ6VIu8uta2vxVTN4FxxsNRSOluTzVYZDeaCISbwmOJ5iihCCIg==",
"funding": [
"https://trpc.io/sponsor"
]
@@ -859,7 +853,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
@@ -868,9 +861,9 @@
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
@@ -888,29 +881,26 @@
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
"dev": true
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.16.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz",
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==",
"devOptional": true
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA=="
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
@@ -932,7 +922,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -962,16 +951,15 @@
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
"dev": true,
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/7zip-bin": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz",
"integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==",
"dev": true
},
"node_modules/agent-base": {
@@ -1053,9 +1041,9 @@
"dev": true
},
"node_modules/app-builder-lib": {
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.1.tgz",
"integrity": "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==",
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.4.tgz",
"integrity": "sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg==",
"dev": true,
"dependencies": {
"@develar/schema-utils": "~2.6.5",
@@ -1064,15 +1052,15 @@
"@electron/universal": "1.4.1",
"@malept/flatpak-bundler": "^0.4.0",
"@types/fs-extra": "9.0.13",
"7zip-bin": "~5.2.0",
"7zip-bin": "~5.1.1",
"async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"chromium-pickle-js": "^0.2.0",
"debug": "^4.3.4",
"ejs": "^3.1.8",
"electron-publish": "24.8.1",
"electron-publish": "24.5.0",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"hosted-git-info": "^4.1.0",
@@ -1091,19 +1079,6 @@
"node": ">=14.0.0"
}
},
"node_modules/app-builder-lib/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1128,9 +1103,9 @@
}
},
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
"dev": true
},
"node_modules/async-exit-hook": {
@@ -1216,7 +1191,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"dev": true,
"optional": true
},
"node_modules/brace-expansion": {
@@ -1268,7 +1242,6 @@
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"engines": {
"node": "*"
}
@@ -1292,16 +1265,16 @@
"dev": true
},
"node_modules/builder-util": {
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz",
"integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==",
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.5.0.tgz",
"integrity": "sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==",
"dev": true,
"dependencies": {
"@types/debug": "^4.1.6",
"7zip-bin": "~5.2.0",
"7zip-bin": "~5.1.1",
"app-builder-bin": "4.0.0",
"bluebird-lst": "^1.0.9",
"builder-util-runtime": "9.2.3",
"builder-util-runtime": "9.2.1",
"chalk": "^4.1.2",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
@@ -1327,19 +1300,6 @@
"node": ">=12.0.0"
}
},
"node_modules/builder-util/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -1356,7 +1316,6 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"dev": true,
"engines": {
"node": ">=10.6.0"
}
@@ -1365,7 +1324,6 @@
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dev": true,
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
@@ -1438,9 +1396,9 @@
"dev": true
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"dev": true,
"funding": [
{
@@ -1485,7 +1443,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dev": true,
"dependencies": {
"mimic-response": "^1.0.0"
},
@@ -1629,7 +1586,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
@@ -1644,7 +1600,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -1656,7 +1611,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"engines": {
"node": ">=10"
}
@@ -1665,7 +1619,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
"dev": true,
"optional": true,
"dependencies": {
"has-property-descriptors": "^1.0.0",
@@ -1691,7 +1644,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true,
"optional": true
},
"node_modules/dir-compare": {
@@ -1727,14 +1679,14 @@
}
},
"node_modules/dmg-builder": {
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.1.tgz",
"integrity": "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==",
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.4.tgz",
"integrity": "sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.9.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"app-builder-lib": "24.6.4",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0"
@@ -1743,19 +1695,6 @@
"dmg-license": "^1.0.11"
}
},
"node_modules/dmg-builder/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/dmg-license": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
@@ -1812,10 +1751,9 @@
}
},
"node_modules/electron": {
"version": "25.9.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.8.tgz",
"integrity": "sha512-PGgp6PH46QVENHuAHc2NT1Su8Q1qov7qIl2jI5tsDpTibwV2zD8539AeWBQySeBU4dhbj9onIl7+1bXQ0wefBg==",
"dev": true,
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.0.tgz",
"integrity": "sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw==",
"hasInstallScript": true,
"dependencies": {
"@electron/get": "^2.0.0",
@@ -1830,16 +1768,16 @@
}
},
"node_modules/electron-builder": {
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.1.tgz",
"integrity": "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==",
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.4.tgz",
"integrity": "sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.9.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"app-builder-lib": "24.6.4",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"chalk": "^4.1.2",
"dmg-builder": "24.9.1",
"dmg-builder": "24.6.4",
"fs-extra": "^10.1.0",
"is-ci": "^3.0.0",
"lazy-val": "^1.0.5",
@@ -1901,19 +1839,6 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/electron-builder/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-notarize": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz",
@@ -1944,33 +1869,20 @@
}
},
"node_modules/electron-publish": {
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz",
"integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==",
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz",
"integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==",
"dev": true,
"dependencies": {
"@types/fs-extra": "^9.0.11",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"chalk": "^4.1.2",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
"mime": "^2.5.2"
}
},
"node_modules/electron-publish/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-trpc": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz",
@@ -2008,7 +1920,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"dependencies": {
"once": "^1.4.0"
}
@@ -2017,7 +1928,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -2041,7 +1951,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true,
"optional": true
},
"node_modules/esbuild": {
@@ -2093,7 +2002,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"optional": true,
"engines": {
"node": ">=10"
@@ -2119,7 +2027,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
@@ -2160,7 +2067,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"dependencies": {
"pend": "~1.2.0"
}
@@ -2277,7 +2183,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
"devOptional": true
},
"node_modules/get-caller-file": {
"version": "2.0.5",
@@ -2291,7 +2197,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dev": true,
"optional": true,
"dependencies": {
"function-bind": "^1.1.1",
@@ -2307,7 +2212,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"dependencies": {
"pump": "^3.0.0"
},
@@ -2376,7 +2280,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"dev": true,
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
@@ -2394,7 +2297,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
"integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
"dev": true,
"optional": true,
"dependencies": {
"define-properties": "^1.1.3"
@@ -2410,7 +2312,6 @@
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
@@ -2440,7 +2341,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"devOptional": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@@ -2461,7 +2362,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
"dev": true,
"optional": true,
"dependencies": {
"get-intrinsic": "^1.1.1"
@@ -2474,7 +2374,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"dev": true,
"optional": true,
"engines": {
"node": ">= 0.4"
@@ -2487,7 +2386,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"optional": true,
"engines": {
"node": ">= 0.4"
@@ -2511,8 +2409,7 @@
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
@@ -2532,7 +2429,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dev": true,
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
@@ -2795,8 +2691,7 @@
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
@@ -2814,7 +2709,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true,
"optional": true
},
"node_modules/json5": {
@@ -2844,7 +2738,6 @@
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
"dev": true,
"dependencies": {
"json-buffer": "3.0.1"
}
@@ -2880,7 +2773,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -2900,7 +2792,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dev": true,
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
@@ -2946,7 +2837,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -3069,7 +2959,6 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -3081,7 +2970,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"optional": true,
"engines": {
"node": ">= 0.4"
@@ -3091,7 +2979,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -3100,7 +2987,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -3180,8 +3066,7 @@
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -3220,7 +3105,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -3242,7 +3126,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -3261,7 +3144,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -3456,14 +3338,12 @@
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"dependencies": {
"lowercase-keys": "^2.0.0"
},
@@ -3499,7 +3379,6 @@
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dev": true,
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
@@ -3579,14 +3458,12 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true,
"optional": true
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dev": true,
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
@@ -3602,7 +3479,6 @@
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=10"
@@ -3723,7 +3599,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
"dev": true,
"optional": true
},
"node_modules/stat-mode": {
@@ -3796,7 +3671,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"dependencies": {
"debug": "^4.1.0"
},
@@ -4066,8 +3940,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
@@ -4120,7 +3993,6 @@
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.6.13",
"version": "2.6.3",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
@@ -12,8 +12,8 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.38.3",
"@trpc/server": "10.38.3",
"@trpc/client": "10.31.0",
"@trpc/server": "10.31.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.4",
"icojs": "^0.17.1",
@@ -25,8 +25,8 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.9.8",
"electron-builder": "^24.9.1",
"electron": "25.8.0",
"electron-builder": "^24.6.4",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"tree-kill": "^1.2.2",
@@ -191,7 +191,6 @@
"allowNativeWayland": true
},
"extraResources": [
"app-update.yml",
"./assets/**"
],
"extraMetadata": {

View File

@@ -29,8 +29,6 @@ import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
const t = initTRPC.create();
@@ -46,8 +44,6 @@ const NotificationOptions = z.object({
});
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
globalThis.window?.webContents.setZoomFactor(factor);
@@ -83,7 +79,6 @@ export const osIntegrationRouter = t.router({
AutoLaunch.disable();
}
config.desktopSettings = settings;
setupDesktopIntegration(settings);
}),
selectDirectory: t.procedure
@@ -122,14 +117,6 @@ export const osIntegrationRouter = t.router({
writeFileSync(resolvedPath, data);
}),
resolvePath: t.procedure
.input(z.object({ filePath: z.string() }))
.query(({ input }) => {
const { filePath } = input;
if (!filePath) return;
return resolvePath(filePath);
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {

View File

@@ -27,23 +27,19 @@ export type CLIOptions = {
hidden: boolean;
};
export async function parseArguments(argv: string[]): Promise<CLIOptions> {
export async function parseArguments(): Promise<CLIOptions> {
const result: CLIOptions = {
note: false,
notebook: false,
reminder: false,
hidden: false
};
const { hidden } = await yargs(hideBin(argv))
const { hidden } = await yargs(hideBin(process.argv))
.boolean("hidden")
// have to account for this flag added on Windows when launching
// via Jumplist
.boolean("allow-file-access-from-files")
.command("new", "Create a new item", (yargs) => {
return yargs
.command("note", "Create a new note", {}, () => {
result.note = true;
console.log("HERE!");
})
.command("notebook", "Create a new notebook", {}, () => {
result.notebook = true;

View File

@@ -24,6 +24,7 @@ import { configureAutoUpdater } from "./utils/autoupdater";
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
import { setupMenu } from "./utils/menu";
import { WindowState } from "./utils/window-state";
import { AutoLaunch } from "./utils/autolaunch";
import { setupJumplist } from "./utils/jumplist";
import { setupTray } from "./utils/tray";
import { CLIOptions, parseArguments } from "./cli";
@@ -33,8 +34,6 @@ import { router, api } from "./api";
import { config } from "./utils/config";
import path from "path";
import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -64,7 +63,7 @@ process.on("unhandledRejection", (reason) => {
app.commandLine.appendSwitch("lang", "en-US");
async function createWindow() {
const cliOptions = await parseArguments(process.argv);
const cliOptions = await parseArguments();
setTheme(getTheme());
const mainWindowState = new WindowState({});
@@ -77,7 +76,7 @@ async function createWindow() {
darkTheme: getTheme() === "dark",
backgroundColor: getBackgroundColor(),
opacity: 0,
autoHideMenuBar: false,
autoHideMenuBar: true,
icon: AssetManager.appIcon({
size: 512,
format: process.platform === "win32" ? "ico" : "png"
@@ -91,9 +90,7 @@ async function createWindow() {
}
});
createIPCHandler({ router, windows: [mainWindow] });
globalThis.window = mainWindow;
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
@@ -107,7 +104,8 @@ async function createWindow() {
}
await AssetManager.loadIcons();
setupDesktopIntegration(config.desktopSettings);
setupDesktopIntegration();
createIPCHandler({ router, windows: [mainWindow] });
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
@@ -148,12 +146,8 @@ app.once("window-all-closed", () => {
}
});
app.on("second-instance", async (_ev, argv) => {
app.on("second-instance", () => {
if (!globalThis.window) return;
const cliOptions = await parseArguments(argv);
if (cliOptions.note) bridge.onCreateItem("note");
if (cliOptions.notebook) bridge.onCreateItem("notebook");
if (cliOptions.reminder) bridge.onCreateItem("reminder");
bringToFront();
});
@@ -177,3 +171,49 @@ function createURL(options: CLIOptions, path = "/") {
return url;
}
function setupDesktopIntegration() {
const desktopIntegration = config.desktopSettings;
if (
desktopIntegration.closeToSystemTray ||
desktopIntegration.minimizeToSystemTray
) {
setupTray();
}
// when close to system tray is enabled, it becomes nigh impossible
// to "quit" the app. This is necessary in order to fix that.
if (desktopIntegration.closeToSystemTray) {
app.on("before-quit", () => app.exit(0));
}
globalThis.window?.once("close", (e) => {
if (config.desktopSettings.closeToSystemTray) {
e.preventDefault();
if (process.platform == "darwin") {
// on macOS window cannot be minimized/hidden if it is already fullscreen
// so we just close it.
if (globalThis.window?.isFullScreen()) app.exit(0);
else app.hide();
} else {
globalThis.window?.minimize();
globalThis.window?.hide();
}
}
});
globalThis.window?.on("minimize", () => {
if (config.desktopSettings.minimizeToSystemTray) {
if (process.platform == "darwin") {
app.hide();
} else {
globalThis.window?.hide();
}
}
});
if (desktopIntegration.autoStart) {
AutoLaunch.enable(!!desktopIntegration.startMinimized);
}
}

View File

@@ -31,6 +31,7 @@ declare global {
}
process.once("loaded", async () => {
console.log("HELLO!");
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),

View File

@@ -35,8 +35,6 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
"autostart"
);
const STARTUP_ARGS = ["--hidden"];
export class AutoLaunch {
static enable(hidden: boolean) {
if (process.platform === "linux") {
@@ -49,14 +47,10 @@ export class AutoLaunch {
LINUX_DESKTOP_ENTRY(hidden)
);
} else {
const loginItemSettings = app.getLoginItemSettings({
args: STARTUP_ARGS
});
if (loginItemSettings.openAtLogin) return;
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: hidden,
args: STARTUP_ARGS
args: ["--hidden"]
});
}
}

View File

@@ -1,74 +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 { app } from "electron";
import { DesktopIntegration, config } from "./config";
import { setupTray, destroyTray } from "./tray";
import { AutoLaunch } from "./autolaunch";
export function setupDesktopIntegration(
desktopIntegration: DesktopIntegration
) {
if (
desktopIntegration.closeToSystemTray ||
desktopIntegration.minimizeToSystemTray
) {
setupTray();
} else {
destroyTray();
}
// when close to system tray is enabled, it becomes nigh impossible
// to "quit" the app. This is necessary in order to fix that.
app.on("before-quit", () =>
desktopIntegration.closeToSystemTray ? app.exit(0) : null
);
globalThis.window?.on("close", (e) => {
if (config.desktopSettings.closeToSystemTray) {
e.preventDefault();
if (process.platform == "darwin") {
// on macOS window cannot be minimized/hidden if it is already fullscreen
// so we just close it.
if (globalThis.window?.isFullScreen()) app.exit(0);
else app.hide();
} else {
try {
globalThis.window?.minimize();
globalThis.window?.hide();
} catch (error) {
console.error(error);
}
}
}
});
globalThis.window?.on("minimize", () => {
if (config.desktopSettings.minimizeToSystemTray) {
if (process.platform == "darwin") {
app.hide();
} else {
globalThis.window?.hide();
}
}
});
if (desktopIntegration.autoStart) {
AutoLaunch.enable(!!desktopIntegration.startMinimized);
}
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { protocol } from "electron";
import { protocol, net } from "electron";
import { isDevelopment } from "./index";
import { createReadStream } from "fs";
import { extname, normalize } from "path";
@@ -62,25 +62,8 @@ function registerProtocol() {
headers: { "Content-Type": extensionToMimeType[fileExtension] }
});
} else {
if (request.headers.has("X-Content-Length")) {
request.headers.set(
"Content-Length",
request.headers.get("X-Content-Length") || "0"
);
request.headers.delete("X-Content-Length");
}
const headers = Object.fromEntries(request.headers.entries());
return await fetch(request.url, {
signal: request.signal,
mode: request.mode,
headers,
method: request.method,
body: request.body,
credentials: request.credentials,
referrer: (request as any).referrer,
duplex: "half",
redirect: "manual"
return net.fetch(request, {
bypassCustomProtocolHandlers: true
});
}
});

View File

@@ -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 "@azure/core-asynciterator-polyfill";
import SettingsService from "./services/settings";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
@@ -31,20 +32,20 @@ import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { themeTrpcClient } from "./screens/settings/theme-selector";
SettingsService.checkOrientation();
const App = () => {
const init = useAppEvents();
useEffect(() => {
const { appLockMode } = SettingsService.get();
let { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
@@ -96,10 +97,10 @@ const App = () => {
let currTheme =
useThemeStore.getState().colorScheme === "dark"
? SettingsService.getProperty("darkTheme")
: SettingsService.getProperty("lighTheme");
: SettingsService.getProperty("lightTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: () => JSX.Element) => {
export const withTheme = (Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,

View File

@@ -16,31 +16,29 @@ 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/dist/logger";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import "./logger";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
database.host(
__DEV__
? {
// 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"
API_HOST: "http://192.168.43.5:5264",
AUTH_HOST: "http://192.168.43.5:8264",
SSE_HOST: "http://192.168.43.5:7264",
SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
ISSUES_HOST: "http://192.168.43.5:2624"
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"
// API_HOST: "http://192.168.43.108:5264",
// AUTH_HOST: "http://192.168.43.108:8264",
// SSE_HOST: "http://192.168.43.108:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.108:9264",
// ISSUES_HOST: "http://192.168.43.108:2624"
}
: {
API_HOST: "https://api.notesnook.com",
@@ -51,27 +49,15 @@ database.host(
}
);
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
database.setup(
Storage,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem,
{
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({
createDriver: () => {
return new RNSqliteDriver({ async: true, dbName: name });
},
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}),
tempStore: "memory"
}
});
);
export const db = database;
export const DatabaseLogger = dbLogger;

View File

@@ -17,13 +17,13 @@ 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 { MMKVLoader } from "react-native-mmkv-storage";
import { initialize } from "@notesnook/core/dist/logger";
import { initalize } from "@notesnook/core/dist/logger";
import { KV } from "./storage";
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
initialize(new KV(LoggerStorage));
initalize(new KV(LoggerStorage));
export { LoggerStorage };
export {};

View File

@@ -1,123 +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 type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
private connectionMutex = new ConnectionMutex();
private db: QuickSQLiteConnection;
constructor(private readonly config: Config) {
this.db = open({
name: config.dbName
});
}
async init(): Promise<void> {
this.connection = new RNSqliteConnection(this.db);
}
async acquireConnection(): Promise<DatabaseConnection> {
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.connectionMutex.lock();
return this.connection!;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("BEGIN TRANSACTION"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("COMMIT"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("ROLLBACK"));
}
async releaseConnection(): Promise<void> {
this.connectionMutex.unlock();
}
async destroy(): Promise<void> {
this.db.close();
}
}
class ConnectionMutex {
private promise?: Promise<void>;
private resolve?: () => void;
async lock(): Promise<void> {
while (this.promise) {
await this.promise;
}
this.promise = new Promise((resolve) => {
this.resolve = resolve;
});
}
unlock(): void {
const resolve = this.resolve;
this.promise = undefined;
this.resolve = undefined;
resolve?.();
}
}
class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(
compiledQuery: CompiledQuery<unknown>
): Promise<QueryResult<R>> {
const { parameters, sql, query } = compiledQuery;
const mode =
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);
// console.log("SQLITE result:", result?.rows?._array);
if (mode === "query" || !result.insertId)
return {
rows: result.rows?._array || []
};
return {
insertId: BigInt(result.insertId),
numAffectedRows: BigInt(result.rowsAffected),
rows: mode === "raw" ? result.rows?._array || [] : []
};
}
}

View File

@@ -90,10 +90,6 @@ export class KV {
return this.storage.removeItem(key);
}
async removeMulti(keys) {
return this.storage.removeItems(...keys);
}
async clear() {
return this.storage.clearStore();
}
@@ -149,7 +145,6 @@ export default {
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
encrypt,
decrypt,
decryptMulti,

View File

@@ -29,12 +29,11 @@ import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { createCacheDir } from "./io";
export const FileDownloadStatus = {
Success: 1,
@@ -45,15 +44,15 @@ export const FileDownloadStatus = {
* Download all user's attachments
* @returns
*/
export async function downloadAllAttachments() {
const attachments = await db.attachments.all.ids();
export function downloadAllAttachments() {
const attachments = db.attachments.all;
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachments
* @param attachments
* @param onProgress
* @returns
*/
@@ -63,7 +62,6 @@ export async function downloadAttachments(
canceled,
groupId
) {
await createCacheDir();
if (!attachments || !attachments.length) return;
const result = new Map();
@@ -88,8 +86,8 @@ export async function downloadAttachments(
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
let attachment = attachments[i];
const hash = attachment.metadata.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -113,16 +111,15 @@ export async function downloadAttachments(
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
});
} catch (e) {
result.set(hash, {
filename: attachment.filename,
filename: attachment.metadata.filename,
status: FileDownloadStatus.Fail,
reason: e
});
@@ -185,9 +182,7 @@ export default async function downloadAttachment(
groupId: undefined
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
let attachment = db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
return;
@@ -206,29 +201,32 @@ export default async function downloadAttachment(
try {
console.log(
"starting download attachment",
attachment.hash,
attachment.metadata.hash,
options.groupId
);
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.hash}`)))
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
)
return;
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
attachment.metadata.filename,
attachment.metadata.type
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
length: attachment.length,
alg: attachment.alg,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType,
mime: attachment.metadata.type,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize
@@ -241,7 +239,7 @@ export default async function downloadAttachment(
);
if (!options.silent) {
ToastManager.show({
ToastEvent.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
@@ -250,11 +248,11 @@ export default async function downloadAttachment(
if (
attachment.dateUploaded &&
!isImage(attachment.mimeType) &&
!isDocument(attachment.mimeType)
!isImage(attachment.metadata?.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
@@ -269,7 +267,7 @@ export default async function downloadAttachment(
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.hash,
context: global ? null : attachment.metadata.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
@@ -279,13 +277,10 @@ export default async function downloadAttachment(
console.log("download attachment error: ", e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.hash);
useAttachmentStore.getState().remove(attachment.metadata.hash);
if (options.throwError) {
throw e;
}

View File

@@ -20,22 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "@notesnook/core/dist/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) {
if (!data) return false;
await createCacheDir();
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
let exists = await RNFetchBlob.fs.exists(path);
if (exists) {
return true;
}
@@ -63,55 +60,44 @@ export async function downloadFile(filename, data, cancelToken) {
console.log("downloading: ", recieved, total);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
cancelToken.cancel = request.cancel;
let response = await request;
await fileCheck(response, totalSize);
let status = response.info().status;
useAttachmentStore.getState().remove(filename);
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled") {
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
}
ToastEvent.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastEvent.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("Download file error:", e, url, headers);
console.log("download file error: ", e, url, headers);
return false;
}
}
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.user.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
return 0;
}
const contentLength = parseInt(attachmentInfo.headers?.get("content-length"));
return isNaN(contentLength) ? 0 : contentLength;
}
export async function checkAttachment(hash) {
@@ -119,7 +105,7 @@ export async function checkAttachment(hash) {
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return { success: true };
const attachment = await db.attachments.attachment(hash);
const attachment = db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {

View File

@@ -20,25 +20,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Platform } from "react-native";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "react-native-blob-util";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
import { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
console.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists =
(await RNFetchBlob.fs.exists(path)) ||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
if (!exists) {
return false;
}
const attachment = await db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.mimeType);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.mimeType);
console.log("decrypting....");
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
let output = await Sodium.decryptFile(
key,
{
@@ -52,7 +59,6 @@ export async function readEncrypted(filename, key, cipherData) {
: "base64"
: "text"
);
console.log("file decrypted...");
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
const dCachePath = `${cacheDir}/${output}`;
output = await compressToBase64(
@@ -64,7 +70,7 @@ export async function readEncrypted(filename, key, cipherData) {
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("readEncrypted", e);
console.log(e);
return false;
}
}
@@ -82,7 +88,6 @@ export async function hashBase64(data) {
}
export async function writeEncryptedBase64({ data, key }) {
await createCacheDir();
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
@@ -98,7 +103,6 @@ export async function writeEncryptedBase64({ data, key }) {
}
export async function deleteFile(filename, data) {
await createCacheDir();
let delFilePath = cacheDir + `/${filename}`;
if (!data) {
if (!filename) return;
@@ -124,89 +128,19 @@ export async function deleteFile(filename, data) {
export async function clearFileStorage() {
try {
let files = await RNFetchBlob.fs.ls(cacheDir);
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
for (let file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
try {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`);
} catch (e) {
console.log(e);
}
}
} catch (e) {
console.log("clearFileStorage", e);
console.log(e);
}
}
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
console.log("Cache directory created");
}
}
export async function migrateFilesFromCache() {
try {
await createCacheDir();
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(","));
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
.catch(console.log);
console.log("Moved", file);
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
console.log("migrateFilesFromCache", e);
}
}
const ABYTES = 17;
export async function exists(filename) {
let path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path
);
if (stat.size !== expectedFileSize) {
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log);
return false;
}
exists = true;
}
let exists = await RNFetchBlob.fs.exists(`${cacheDir}/${filename}`);
return exists;
}

View File

@@ -24,12 +24,11 @@ import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
let { url, headers } = data;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
@@ -73,10 +72,7 @@ export async function uploadFile(filename, data, cancelToken) {
);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
cancelToken.cancel = request.cancel;
let response = await request;
let status = response.info().status;
@@ -87,9 +83,12 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(
`File upload status: ${filename}, ${status}, ${text}`
);
let attachment = await db.attachments.attachment(filename);
let attachment = db.attachments.attachment(filename);
if (!attachment) return result;
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
if (
!isImage(attachment.metadata.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
} else {

View File

@@ -21,12 +21,7 @@ import * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
export const cacheDir =
Platform.OS == "ios"
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
export function getRandomId(prefix) {
return Math.random()
@@ -71,12 +66,9 @@ export function cancelable(operation) {
}
export function copyFileAsync(source, dest) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
if (e) {
reject(e);
return;
}
console.log(e, r);
resolve();
});
});

View File

@@ -34,69 +34,10 @@ import BaseDialog from "../dialog/base-dialog";
import { allowedOnPlatform, renderItem } from "./functions";
import { useCallback } from "react";
/**
* Test announcement
* {
id: "some-announcement",
type: "dialog",
body: [
{
type: "title",
text: "This is a title",
platforms: ["all"]
},
{
type: "description",
text: "Most of you are too busy to keep up to date with what's happening in Notesnook. That is unfortunate because Notesnook has come a looooong way.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "To solve this, we are launching the Notesnook Digest — a newsletter to help you stay updated about Notesnook development. And to keep things interesting I'll also sprinkle this newsletter with other interesting stuff like privacy tips & news, interesting books, things I am looking forward to etc.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "So be sure to subscribe. There won't be a proper schedule to this (yet) maybe once or twice a month. I promise no spam — only more awesomeness.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "— May privacy reign.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "callToActions",
actions: [
{
type: "promo",
title: "15% Off",
platforms: ["android"],
data: "com.streetwriters.notesnook.sub.yr.15"
}
],
platforms: ["all"]
}
]
}
*/
export const AnnouncementDialog = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [info, setInfo] = useState();
const [info, setInfo] = useState(null);
const remove = useMessageStore((state) => state.remove);
useEffect(() => {
@@ -110,9 +51,7 @@ export const AnnouncementDialog = () => {
const open = (data) => {
setInfo(data);
setImmediate(() => {
setVisible(true);
});
setVisible(true);
};
const close = useCallback(() => {

View File

@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef } from "react";
import { Platform, TextInput, View } from "react-native";
//@ts-ignore
import { Platform, View } from "react-native";
import { enabled } from "react-native-privacy-snapshot";
import { db } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
@@ -42,8 +41,8 @@ const AppLockedOverlay = () => {
const appLocked = useUserStore((state) => state.appLocked);
const lockApp = useUserStore((state) => state.lockApp);
const deviceMode = useSettingStore((state) => state.deviceMode);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>();
const passwordInputRef = useRef();
const password = useRef();
const appState = useAppState();
const biometricUnlockAwaitingUserInput = useRef(false);
@@ -56,14 +55,14 @@ const AppLockedOverlay = () => {
}
useSettingStore.getState().setRequestBiometrics(true);
const unlocked = await BiometricService.validateUser(
let unlocked = await BiometricService.validateUser(
"Unlock to access your notes",
""
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
password.current = null;
}
setTimeout(() => {
biometricUnlockAwaitingUserInput.current = false;
@@ -74,11 +73,11 @@ const AppLockedOverlay = () => {
const onSubmit = async () => {
if (!password.current) return;
try {
const unlocked = await db.user.verifyPassword(password.current);
let unlocked = await db.user.verifyPassword(password.current);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
password.current = null;
}
} catch (e) {
console.error(e);
@@ -171,9 +170,7 @@ const AppLockedOverlay = () => {
secureTextEntry
placeholder="Enter account password"
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();
}}
onSubmit={onSubmit}
/>
</>
) : null}

View File

@@ -17,13 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { RefObject, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
@@ -31,17 +27,14 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import picker from "../../screens/editor/tiptap/picker";
import {
ToastManager,
eSendEvent,
presentSheet
presentSheet,
ToastEvent
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import {
eCloseAttachmentDialog,
eCloseSheet,
eDBItemUpdate
} from "../../utils/events";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -53,33 +46,28 @@ import { Notice } from "../ui/notice";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { formatBytes } from "@notesnook/common";
const Actions = ({
attachment,
fwdRef
}: {
attachment: Attachment;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const Actions = ({ attachment, setAttachments, fwdRef }) => {
const { colors } = useThemeColors();
const contextId = attachment.hash;
const [filename, setFilename] = useState(attachment.filename);
const contextId = attachment.metadata.hash;
const [filename, setFilename] = useState(attachment.metadata.filename);
const [currentProgress] = useAttachmentProgress(attachment);
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState<{
name?: string;
}>({});
const [failed, setFailed] = useState(attachment.failed);
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState({
name: null
});
const actions = [
{
name: "Download",
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.hash, "download");
useAttachmentStore.getState().remove(attachment.hash);
await db.fs.cancel(attachment.metadata.hash, "download");
useAttachmentStore.getState().remove(attachment.metadata.hash);
}
downloadAttachment(attachment.hash, false);
downloadAttachment(attachment.metadata.hash, false);
eSendEvent(eCloseSheet, contextId);
},
icon: "download"
@@ -88,7 +76,7 @@ const Actions = ({
name: "Reupload",
onPress: async () => {
if (!PremiumService.get()) {
ToastManager.show({
ToastEvent.show({
heading: "Upgrade to pro",
type: "error",
context: "local"
@@ -97,9 +85,9 @@ const Actions = ({
}
await picker.pick({
reupload: true,
hash: attachment.hash,
hash: attachment.metadata.hash,
context: contextId,
type: attachment.type
type: attachment.metadata.type
});
},
icon: "upload"
@@ -110,28 +98,28 @@ const Actions = ({
setLoading({
name: "Run file check"
});
let res = await filesystem.checkAttachment(attachment.hash);
let res = await filesystem.checkAttachment(attachment.metadata.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastManager.show({
ToastEvent.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(undefined);
db.attachments.markAsFailed(attachment.id);
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
}
setAttachments([...db.attachments.all]);
setLoading({
name: undefined
name: null
});
},
icon: "file-check"
@@ -140,19 +128,19 @@ const Actions = ({
name: "Rename",
onPress: () => {
presentDialog({
context: contextId as any,
context: contextId,
input: true,
title: "Rename file",
paragraph: "Enter a new name for the file",
defaultValue: attachment.filename,
defaultValue: attachment.metadata.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.hash,
hash: attachment.metadata.hash,
filename: value
});
setFilename(value);
eSendEvent(eDBItemUpdate, attachment.id);
setAttachments([...db.attachments.all]);
}
},
positiveText: "Rename"
@@ -163,22 +151,34 @@ const Actions = ({
{
name: "Delete",
onPress: async () => {
await db.attachments.remove(attachment.hash, false);
eSendEvent(eDBItemUpdate, attachment.id);
await db.attachments.remove(attachment.metadata.hash, false);
setAttachments([...db.attachments.all]);
eSendEvent(eCloseSheet, contextId);
},
icon: "delete-outline"
}
];
const getNotes = useCallback(() => {
let allNotes = db.notes.all;
let attachmentNotes = attachment.noteIds?.map((id) => {
let index = allNotes?.findIndex((note) => id === note.id);
if (index !== -1) {
return allNotes[index];
} else {
return {
type: "notfound",
title: `Note with id ${id} does not exist.`,
id: id
};
}
});
return attachmentNotes;
}, [attachment.noteIds]);
useEffect(() => {
db.relations
.to(attachment, "note")
.selector.items()
.then((items) => {
setNotes(items);
});
}, [attachment]);
setNotes(getNotes());
}, [attachment, getNotes]);
return (
<ScrollView
@@ -221,7 +221,7 @@ const Actions = ({
}}
color={colors.secondary.paragraph}
>
{attachment.type}
{attachment.metadata.type}
</Paragraph>
<Paragraph
style={{
@@ -230,10 +230,10 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{formatBytes(attachment.size)}
{formatBytes(attachment.length)}
</Paragraph>
{notes.length ? (
{attachment.noteIds ? (
<Paragraph
style={{
marginRight: 10
@@ -241,14 +241,14 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notes.length} note
{notes.length > 1 ? "s" : ""}
{attachment.noteIds.length} note
{attachment.noteIds.length > 1 ? "s" : ""}
</Paragraph>
) : null}
<Paragraph
onPress={() => {
Clipboard.setString(attachment.hash);
ToastManager.show({
Clipboard.setString(attachment.metadata.hash);
ToastEvent.show({
type: "success",
heading: "Attachment hash copied",
context: "local"
@@ -257,7 +257,7 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.hash}
{attachment.metadata.hash}
</Paragraph>
</View>
@@ -286,11 +286,21 @@ const Actions = ({
{notes.map((item) => (
<PressableButton
onPress={async () => {
if (item.type === "notfound") {
ToastEvent.show({
heading: "Note not found",
message:
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
type: "error",
context: "local"
});
return;
}
eSendEvent(eCloseSheet, contextId);
await sleep(150);
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
openNote(item, (item as any).type === "trash");
openNote(item, item.type === "trash");
}}
customStyle={{
paddingVertical: 12,
@@ -311,16 +321,17 @@ const Actions = ({
<Button
key={item.name}
buttonType={{
text:
item.name === "Delete" || item.name === "PermDelete"
? colors.error.paragraph
: colors.primary.paragraph
text: item.on
? colors.primary.accent
: item.name === "Delete" || item.name === "PermDelete"
? colors.error.paragraph
: colors.primary.paragraph
}}
onPress={item.onPress}
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type="gray"
type={item.on ? "shade" : "gray"}
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -348,14 +359,12 @@ const Actions = ({
);
};
Actions.present = (
attachment: Attachment,
set: (attachments: VirtualizedGrouping<Attachment>) => void,
context?: string
) => {
Actions.present = (attachment, set, context) => {
presentSheet({
context: context,
component: (ref) => <Actions fwdRef={ref} attachment={attachment} />
component: (ref) => (
<Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
)
});
};

View File

@@ -18,58 +18,43 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
id,
attachments,
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
}: {
id: string | number;
attachments?: VirtualizedGrouping<Attachment>;
encryption?: boolean;
setAttachments: (attachments: any) => void;
pressable?: boolean;
hideWhenNotDownloading?: boolean;
context?: string;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const onPress = () => {
if (!pressable || !attachment) return;
if (!pressable) return;
Actions.present(attachment, setAttachments, context);
};
return (hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value)) ||
!attachment ? null : (
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -82,6 +67,7 @@ export const AttachmentItem = ({
borderRadius: 5,
backgroundColor: colors.secondary.background
}}
type="grayBg"
>
<View
style={{
@@ -107,7 +93,7 @@ export const AttachmentItem = ({
position: "absolute"
}}
>
{getFileExtension(attachment.filename).toUpperCase()}
{getFileExtension(attachment.metadata.filename).toUpperCase()}
</Paragraph>
</View>
@@ -127,12 +113,12 @@ export const AttachmentItem = ({
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.filename}
{attachment.metadata.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
@@ -146,8 +132,8 @@ export const AttachmentItem = ({
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs().cancel(attachment.hash, currentProgress?.type as any);
setCurrentProgress(undefined);
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
style={{
justifyContent: "center",

View File

@@ -21,10 +21,7 @@ import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import {
PresentSheetOptions,
presentSheet
} from "../../services/event-manager";
import { presentSheet } from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -32,19 +29,8 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeColors } from "@notesnook/theme";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
const DownloadAttachments = ({
close,
attachments,
isNote,
update
}: {
attachments: VirtualizedGrouping<Attachment>;
close?: ((ctx?: string | undefined) => void) | undefined;
isNote?: boolean;
update?: (props: PresentSheetOptions) => void;
}) => {
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
@@ -53,57 +39,63 @@ const DownloadAttachments = ({
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef<string>();
const groupId = useRef();
const onDownload = async () => {
update?.({
update({
disableClosing: true
} as PresentSheetOptions);
});
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
await attachments.ids(),
(progress: number, statusText: string) =>
setProgress({ value: progress, statusText }),
attachments,
(progress, statusText) => setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update?.({
update({
disableClosing: false
} as PresentSheetOptions);
});
};
const cancel = async () => {
update?.({
update({
disableClosing: false
} as PresentSheetOptions);
});
canceled.current = true;
if (!groupId.current) return;
console.log(groupId.current, "canceling groupId downloads");
await db.fs().cancel(groupId.current, "download");
await db.fs.cancel(groupId.current);
setDownloading(false);
groupId.current = undefined;
groupId.current = null;
};
const successResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 1) results.push(db.attachments.attachment(key));
}
return results;
};
const failedResults = () => {
const results = [];
for (const value of result.values()) {
if (value.status === 0) results.push(value.attachment);
for (let [key, value] of result.entries()) {
if (value.status === 0) results.push(db.attachments.attachment(key));
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments?.placeholders?.length - failedResults().length;
attachments.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments?.placeholders.length
attachments.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
@@ -165,11 +157,7 @@ const DownloadAttachments = ({
width={null}
animated={true}
useNativeDriver
progress={
progress.value
? progress.value / attachments.placeholders?.length
: 0
}
progress={progress.value ? progress.value / attachments.length : 0}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
@@ -186,7 +174,7 @@ const DownloadAttachments = ({
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.placeholders : undefined}
data={downloading ? attachments : []}
ListEmptyComponent={
<View
style={{
@@ -201,15 +189,14 @@ const DownloadAttachments = ({
</Paragraph>
</View>
}
keyExtractor={(index) => "attachment" + index}
renderItem={({ index }) => {
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<AttachmentItem
id={index}
attachment={item}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
@@ -222,9 +209,7 @@ const DownloadAttachments = ({
borderRadius: 100,
marginTop: 20
}}
onPress={() => {
close?.();
}}
onPress={close}
type="accent"
title="Done"
/>
@@ -242,9 +227,7 @@ const DownloadAttachments = ({
borderRadius: 100,
marginRight: 5
}}
onPress={() => {
close?.();
}}
onPress={close}
type="grayBg"
title="No"
/>
@@ -275,11 +258,7 @@ const DownloadAttachments = ({
);
};
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
DownloadAttachments.present = (context, attachments, isNote) => {
presentSheet({
context: context,
component: (ref, close, update) => (

View File

@@ -17,25 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Attachment,
Note,
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
@@ -43,77 +34,79 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/dist/utils/filename";
import { useSettingStore } from "../../stores/use-setting-store";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: Note }) => {
export const AttachmentDialog = ({ note }) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const searchTimer = useRef<NodeJS.Timeout>();
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const refresh = React.useCallback(() => {
if (note) {
db.attachments.ofNote(note.id, "all").sorted(DEFAULT_SORTING);
} else {
db.attachments.all
.sorted(DEFAULT_SORTING)
.then((attachments) => setAttachments(attachments));
}
}, [note]);
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
useEffect(() => {
refresh();
}, [note, refresh]);
const onChangeText = (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
refresh();
setAttachments([...attachments]);
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(async () => {
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
attachmentSearchValue.current
);
if (results.length === 0) return;
setAttachments(results);
}, 300);
};
const renderItem = ({ index }: { item: boolean; index: number }) => (
const renderItem = ({ item }) => (
<AttachmentItem
setAttachments={setAttachments}
attachments={attachments}
id={index}
attachment={item}
context="attachments-list"
/>
);
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
for (let i = 0; i < attachments.placeholders.length; i++) {
const attachment = (await attachments.item(i))?.item;
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
await db.attachments.markAsFailed(attachment.hash, result.failed);
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
await db.attachments.markAsFailed(attachment.id, undefined);
await db.attachments.markAsFailed(attachment.id, null);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
refresh();
setLoading(false);
};
@@ -140,33 +133,31 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
}
];
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
const filterAttachments = (type) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
switch (type) {
case "all":
items = db.attachments.all;
break;
return attachments;
case "images":
items = note
? db.attachments.ofNote(note.id, "images")
: db.attachments.images;
break;
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
case "video":
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.videos;
break;
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
case "audio":
items = db.attachments.all;
break;
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
case "documents":
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.documents;
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
}
return await items.sorted(DEFAULT_SORTING);
};
return (
@@ -224,7 +215,6 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
@@ -241,7 +231,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
onChangeText(attachmentSearchValue.current);
}}
/>
@@ -263,7 +253,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
style={{
borderRadius: 0,
borderBottomWidth: 1,
@@ -273,9 +266,9 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
? "transparent"
: colors.primary.accent
}}
onPress={async () => {
onPress={() => {
setCurrentFilter(item.filterBy);
setAttachments(await filterAttachments(item.filterBy));
setAttachments(filterAttachments(item.filterBy));
}}
/>
))}
@@ -306,7 +299,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
/>
}
estimatedItemSize={50}
data={attachments?.placeholders}
data={attachments}
renderItem={renderItem}
/>
@@ -329,7 +322,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
);
};
AttachmentDialog.present = (note?: Note) => {
AttachmentDialog.present = (note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});

View File

@@ -17,16 +17,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { useThemeColors } from "@notesnook/theme";
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
@@ -36,6 +34,7 @@ import { IconButton } from "../ui/icon-button";
import { hideAuth, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
export const AuthMode = {
login: 0,
@@ -69,9 +68,6 @@ const AuthModal = () => {
}
const close = () => {
useUserStore.setState({
disableAppLockRequests: false
});
actionSheetRef.current?.hide();
setCurrentAuthMode(AuthMode.login);
setVisible(false);
@@ -81,11 +77,6 @@ const AuthModal = () => {
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
onShow={() => {
useUserStore.setState({
disableAppLockRequests: true
});
}}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
@@ -94,14 +85,11 @@ const AuthModal = () => {
background={colors.primary.background}
transparent={false}
animated={false}
centered={false}
enableSheetKeyboardHandler
>
<KeyboardAwareScrollView
style={{
width: "100%"
}}
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
{currentAuthMode !== AuthMode.login ? (

View File

@@ -23,7 +23,7 @@ import { db } from "../../common/database";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
@@ -45,7 +45,7 @@ export const ChangePassword = () => {
const changePassword = async () => {
if (!user?.isEmailConfirmed) {
ToastManager.show({
ToastEvent.show({
heading: "Email not confirmed",
message: "Please confirm your email to change account password",
type: "error",
@@ -54,7 +54,7 @@ export const ChangePassword = () => {
return;
}
if (error || !oldPassword.current || !password.current) {
ToastManager.show({
ToastEvent.show({
heading: "All fields required",
message: "Fill all the fields and try again.",
type: "error",
@@ -66,7 +66,7 @@ export const ChangePassword = () => {
try {
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastManager.show({
ToastEvent.show({
heading: "Account password updated",
type: "success",
context: "global"
@@ -75,7 +75,7 @@ export const ChangePassword = () => {
eSendEvent(eCloseSheet);
} catch (e) {
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Failed to change password",
message: e.message,
type: "error",

View File

@@ -22,7 +22,7 @@ import { View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { ToastEvent } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useThemeColors } from "@notesnook/theme";
import DialogHeader from "../dialog/dialog-header";
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastManager.show({
ToastEvent.show({
heading: "Account email is required.",
type: "error",
context: "local"
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastManager.show({
ToastEvent.show({
heading: "Check your email to reset password",
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
type: "success",
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
setSent(true);
} catch (e) {
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Recovery email not sent",
message: e.message,
type: "error",

View File

@@ -24,7 +24,7 @@ import { db } from "../../common/database";
import { MMKV } from "../../common/database/mmkv";
import BiometricService from "../../services/biometrics";
import {
ToastManager,
ToastEvent,
eSendEvent,
eSubscribeEvent
} from "../../services/event-manager";
@@ -46,7 +46,6 @@ import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import BaseDialog from "../dialog/base-dialog";
function getObfuscatedEmail(email) {
if (!email) return "";
@@ -69,8 +68,7 @@ export const SessionExpired = () => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
},
true
}
);
const logout = async () => {
@@ -85,7 +83,7 @@ export const SessionExpired = () => {
clearAllStores();
setVisible(false);
} catch (e) {
ToastManager.show({
ToastEvent.show({
heading: e.message,
type: "error",
context: "local"
@@ -102,9 +100,9 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.tokenManager.getToken();
let res = await db.user.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
if (db.user.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
if (!complete) {
@@ -132,20 +130,15 @@ export const SessionExpired = () => {
return (
visible && (
<BaseDialog
transparent={false}
background={colors.primary.background}
bounce={false}
animated={false}
centered={false}
<Modal
onShow={async () => {
await sleep(300);
passwordInputRef.current?.focus();
setFocused(true);
}}
enableSheetKeyboardHandler={true}
visible={true}
>
<SheetProvider context="two_factor_verify" />
<View
style={{
width: focused ? "100%" : "99.9%",
@@ -238,9 +231,7 @@ export const SessionExpired = () => {
</View>
<Toast context="local" />
<Dialog context="session_expiry" />
<SheetProvider context="two_factor_verify" />
</BaseDialog>
</Modal>
)
);
};

View File

@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { ToastEvent } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -52,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
const { width, height } = useWindowDimensions();
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
ToastEvent.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -85,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
}
} catch (e) {
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Signup failed",
message: e.message,
type: "error",

View File

@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
@@ -138,7 +138,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
setSending(false);
} catch (e) {
setSending(false);
ToastManager.error(e, "Error sending 2FA Code", "local");
ToastEvent.error(e, "Error sending 2FA Code", "local");
}
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useRef, useState } from "react";
import { db } from "../../common/database";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import { clearMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -33,7 +33,7 @@ export const LoginSteps = {
passwordAuth: 3
};
export const useLogin = (onFinishLogin, sessionExpired = false) => {
export const useLogin = (onFinishLogin) => {
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
(!password.current && step === LoginSteps.passwordAuth) ||
(!email.current && step === LoginSteps.emailAuth)
) {
ToastManager.show({
ToastEvent.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -69,7 +69,6 @@ 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) => {
try {
@@ -102,12 +101,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
break;
}
case LoginSteps.passwordAuth: {
await db.user.authenticatePassword(
email.current,
password.current,
null,
sessionExpired
);
await db.user.authenticatePassword(email.current, password.current);
finishLogin();
break;
}
@@ -121,7 +115,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const finishWithError = async (e) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Login failed",
message: e.message,
type: "error",
@@ -135,7 +129,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastManager.show({
ToastEvent.show({
heading: "Login successful",
message: `Logged in as ${user.email}`,
type: "success",

View File

@@ -17,15 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const path = require("path");
module.exports = {
entry: ["./dist/index.global.js"],
mode: "production",
output: {
filename: "clipper.bundle.js",
path: path.resolve(
__dirname,
"../../apps/mobile/native/ios/extension.bundle"
)
}
import React, { PropsWithChildren } from "react";
import { View } from "react-native";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
export const ContainerHeader = (props: PropsWithChildren) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !selectionMode ? (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
overflow: "hidden"
}}
>
{props.children}
</View>
) : null;
};

View File

@@ -36,11 +36,10 @@ import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { PressableButton } from "../ui/pressable";
interface FloatingButtonProps {
title: string;
interface FloatingButton {
title?: string;
onPress: () => void;
color?: string;
shouldShow?: boolean;
alwaysVisible?: boolean;
}
@@ -49,7 +48,7 @@ const FloatingButton = ({
onPress,
color,
alwaysVisible = false
}: FloatingButtonProps) => {
}: FloatingButton) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
@@ -119,7 +118,7 @@ const FloatingButton = ({
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={color}
accentColor={colors.static[color as keyof typeof colors.static]}
customStyle={{
...getElevationStyle(5),
borderRadius: 100

View File

@@ -21,9 +21,15 @@ import React, { PropsWithChildren } from "react";
import { KeyboardAvoidingView, Platform } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { Header } from "../header";
import SelectionHeader from "../selection-header";
export const Container = ({ children }: PropsWithChildren) => {
const floating = useIsFloatingKeyboard();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const insets = useGlobalSafeAreaInsets();
return (
@@ -37,6 +43,13 @@ export const Container = ({ children }: PropsWithChildren) => {
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
}}
>
{!introCompleted ? null : (
<>
<SelectionHeader />
<Header />
</>
)}
{children}
</KeyboardAvoidingView>
);

View File

@@ -44,7 +44,7 @@ export default function DelayLayout({
}: IDelayLayoutProps) {
const { colors } = useThemeColors();
const loading = useDelayLayout(
!props.delay || props.delay < 300 ? 0 : props.delay
!props.delay || props.delay < 300 ? 300 : props.delay
);
const Placeholder = placeholder[props.type || "default"];

View File

@@ -17,15 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { AddTopicDialog } from "../dialogs/add-topic";
import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview";
@@ -36,14 +36,17 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => {
const { colors } = useThemeColors();
const loading = useNoteStore((state) => state.loading);
return (
<>
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -55,11 +58,10 @@ const DialogProvider = () => {
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<Expiring />
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
</>
);
};

View File

@@ -21,9 +21,11 @@ import { eSendEvent } from "../../services/event-manager";
import {
eCloseActionSheet,
eCloseAddNotebookDialog,
eCloseAddTopicDialog,
eCloseMoveNoteDialog,
eOpenActionSheet,
eOpenAddNotebookDialog,
eOpenAddTopicDialog,
eOpenMoveNoteDialog
} from "../../utils/events";
@@ -50,3 +52,9 @@ export const AddNotebookEvent = (notebook) => {
export const HideAddNotebookEvent = (notebook) => {
eSendEvent(eCloseAddNotebookDialog, notebook);
};
export const AddTopicEvent = (topic) => {
eSendEvent(eOpenAddTopicDialog, topic);
};
export const HideAddTopicEvent = (notebook) => {
eSendEvent(eCloseAddTopicDialog, notebook);
};

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
import React, { useEffect } from "react";
import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
@@ -32,28 +31,6 @@ import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { BouncingView } from "../ui/transitions/bouncing-view";
import { ScopedThemeProvider } from "@notesnook/theme";
import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { useAppState } from "../../hooks/use-app-state";
export interface BaseDialogProps extends PropsWithChildren {
animation?: "fade" | "none" | "slide" | undefined;
visible: boolean;
onRequestClose?: () => void;
onShow?: () => void;
premium?: boolean;
statusBarTranslucent?: boolean;
transparent?: boolean;
centered?: boolean;
bottom?: boolean;
background?: string | ColorValue;
animated?: boolean;
bounce?: boolean;
closeOnTouch?: boolean;
useSafeArea?: boolean;
avoidKeyboardResize?: boolean;
enableSheetKeyboardHandler?: boolean;
}
const BaseDialog = ({
visible,
@@ -66,18 +43,14 @@ const BaseDialog = ({
transparent,
centered = true,
bottom = false,
background = null,
animated = true,
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false,
enableSheetKeyboardHandler,
background
}: BaseDialogProps) => {
avoidKeyboardResize = false
}) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
const lockEvents = useRef(false);
const [internalVisible, setIntervalVisible] = useState(true);
useEffect(() => {
return () => {
@@ -85,34 +58,14 @@ const BaseDialog = ({
};
}, []);
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.get().appLockMode === "background") {
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;
});
}
});
}
}
}
}, [appState]);
const Wrapper = useSafeArea ? SafeAreaView : View;
return (
<ScopedThemeProvider value="dialog">
<Modal
visible={visible && internalVisible}
visible={visible}
transparent={true}
animated
statusBarTranslucent={statusBarTranslucent}
supportedOrientations={[
"portrait",
@@ -122,17 +75,13 @@ const BaseDialog = ({
"landscape-right"
]}
onShow={() => {
if (lockEvents.current) return;
if (onShow) {
onShow();
if (!enableSheetKeyboardHandler) {
useSettingStore.getState().setSheetKeyboardHandler(false);
}
useSettingStore.getState().setSheetKeyboardHandler(false);
}
}}
animationType={animation}
onRequestClose={() => {
if (lockEvents.current) return;
if (!closeOnTouch) return null;
useSettingStore.getState().setSheetKeyboardHandler(true);
onRequestClose && onRequestClose();
@@ -158,7 +107,6 @@ const BaseDialog = ({
style={[
styles.backdrop,
{
alignItems: centered ? "center" : undefined,
justifyContent: centered
? "center"
: bottom
@@ -168,7 +116,7 @@ const BaseDialog = ({
]}
>
<TouchableOpacity
onPress={closeOnTouch ? onRequestClose : undefined}
onPress={closeOnTouch ? onRequestClose : null}
style={styles.overlayButton}
/>
{premium}
@@ -185,7 +133,8 @@ const styles = StyleSheet.create({
backdrop: {
width: "100%",
height: "100%",
justifyContent: "center"
justifyContent: "center",
alignItems: "center"
},
overlayButton: {
width: "100%",

View File

@@ -86,9 +86,7 @@ const DialogHeader = ({
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
<Text style={{ color: colors.primary.accent }}>{titlePart}</Text>
) : null}
</Heading>
@@ -104,7 +102,7 @@ const DialogHeader = ({
title={button.title}
icon={button.icon}
type={button.type || "grayBg"}
height={30}
height={25}
/>
) : null}
</View>

View File

@@ -25,7 +25,7 @@ type DialogInfo = {
paragraph?: string;
positiveText?: string;
negativeText?: string;
positivePress?: (value: any) => void;
positivePress?: (value: unknown) => void;
onClose?: () => void;
positiveType?:
| "transparent"

View File

@@ -0,0 +1,190 @@
/*
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, { createRef } from "react";
import { View } from "react-native";
import { useMenuStore } from "../../../stores/use-menu-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddTopicDialog,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogContainer from "../../dialog/dialog-container";
import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { Toast } from "../../toast";
import { useRelationStore } from "../../../stores/use-relation-store";
export class AddTopicDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
titleFocused: false,
loading: false
};
this.ref = createRef();
this.title;
this.titleRef = createRef();
this.notebook = null;
this.toEdit = null;
}
addNewTopic = async () => {
try {
if (!this.title || this.title?.trim() === "") {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return;
}
if (!this.toEdit) {
await db.notebooks.notebook(this.notebook.id).topics.add(this.title);
} else {
let topic = this.toEdit;
topic.title = this.title;
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
}
this.close();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
} catch (e) {
console.error(e);
}
};
componentDidMount() {
eSubscribeEvent(eOpenAddTopicDialog, this.open);
eSubscribeEvent(eCloseAddTopicDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
}
open = async ({ notebookId, toEdit }) => {
let id = notebookId;
if (id) {
this.notebook = await db.notebooks.notebook(id).data;
}
this.toEdit = toEdit;
if (this.toEdit) {
this.title = this.toEdit.title;
}
this.setState({
visible: true
});
};
close = () => {
this.title = null;
this.notebook = null;
this.toEdit = null;
this.setState({
visible: false
});
};
render() {
const { visible } = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
if (this.toEdit) {
this.titleRef.current?.setNativeProps({
text: this.toEdit.title
});
}
this.ref.current?.animateNextTransition();
await sleep(300);
this.titleRef.current?.focus();
}}
bounce={false}
statusBarTranslucent={false}
visible={true}
onRequestClose={this.close}
>
<DialogContainer>
<DialogHeader
icon="book-outline"
title={this.toEdit ? "Edit topic" : "New topic"}
paragraph={
this.toEdit
? "Edit title of the topic"
: "Add a new topic in " + this.notebook.title
}
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12,
zIndex: 10
}}
>
<Input
fwdRef={this.titleRef}
testID="input-title"
onChangeText={(value) => {
this.title = value;
}}
blurOnSubmit={false}
placeholder="Enter title"
onSubmit={() => this.addNewTopic()}
returnKeyLabel="Done"
returnKeyType="done"
/>
</View>
<DialogButtons
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
/>
</DialogContainer>
<Toast context="local" />
</BaseDialog>
);
}
}

View File

@@ -17,22 +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 { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import { ActivityIndicator, FlatList, ScrollView, View } from "react-native";
import React, { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useMessageStore } from "../../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseJumpToDialog,
@@ -43,49 +36,27 @@ import { SIZE } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { useCallback } from "react";
const JumpToSectionDialog = () => {
const scrollRef = useRef<RefObject<FlatList>>();
const [data, setData] = useState<VirtualizedGrouping<Item>>();
const offsets = [];
let timeout = null;
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
const { colors } = useThemeColors();
const notes = data;
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const currentScrollPosition = useRef(0);
const [loading, setLoading] = useState(false);
const [groups, setGroups] = useState<
{
index: number;
group: GroupHeader;
}[]
>();
const offsets = useRef<number[]>([]);
const onPress = (item: { index: number; group: GroupHeader }) => {
scrollRef.current?.current?.scrollToIndex({
index: item.index,
const onPress = (item) => {
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
scrollRef.current?.scrollToIndex({
index: ind,
animated: true
});
close();
};
const open = useCallback(
({
data,
ref
}: {
data: VirtualizedGrouping<Item>;
ref: RefObject<FlatList>;
}) => {
setLoading(true);
setData(data);
scrollRef.current = ref;
setVisible(true);
},
[]
);
useEffect(() => {
eSubscribeEvent(eOpenJumpToDialog, open);
eSubscribeEvent(eCloseJumpToDialog, close);
@@ -98,46 +69,54 @@ const JumpToSectionDialog = () => {
};
}, [open]);
const onScroll = (data: { x: number; y: number }) => {
currentScrollPosition.current = data.y;
const onScroll = (data) => {
let y = data.y;
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(() => {
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
setCurrentIndex(index || 0);
}, 200);
};
const open = useCallback(
(_type) => {
if (_type !== type) return;
setVisible(true);
},
[type]
);
const close = () => {
setVisible(false);
};
const loadGroupsAndOffsets = useCallback(() => {
notes?.groups?.().then((groups) => {
setGroups(groups);
offsets.current = [];
groups.map((item, index) => {
useEffect(() => {
loadOffsets();
}, [loadOffsets, notes]);
const loadOffsets = useCallback(() => {
notes
.filter((i) => i.type === "header")
.map((item, index) => {
let offset = 35 * index;
let groupIndex = item.index;
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
groupIndex = groupIndex + 1;
groupIndex = groupIndex - (index + 1);
offset = offset + groupIndex * 100 + msgOffset;
offsets.current.push(offset);
});
const index = offsets.current?.findIndex((o, i) => {
return (
o <= currentScrollPosition.current + 100 &&
offsets.current[i + 1] - 100 > currentScrollPosition.current
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
let messageState = useMessageStore.getState().message;
let msgOffset = messageState?.visible ? 60 : 10;
ind = ind + 1;
ind = ind - (index + 1);
offset = offset + ind * 100 + msgOffset;
offsets.push(offset);
});
setCurrentIndex(index < 0 ? 0 : index);
setLoading(false);
});
}, [notes]);
return !visible ? null : (
<BaseDialog
onShow={() => {
loadGroupsAndOffsets();
loadOffsets();
}}
onRequestClose={close}
visible={true}
@@ -156,42 +135,35 @@ const JumpToSectionDialog = () => {
paddingTop: 30
}}
>
{loading ? (
<ActivityIndicator
size={SIZE.lg}
color={colors.primary.accent}
<ScrollView
style={{
maxHeight: "100%"
}}
>
<View
style={{
marginBottom: 20
}}
/>
) : (
<ScrollView
style={{
maxHeight: "100%"
flexDirection: "row",
flexWrap: "wrap",
alignSelf: "center",
justifyContent: "center",
paddingBottom: 20
}}
>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
alignSelf: "center",
justifyContent: "center",
paddingBottom: 20
}}
>
{groups?.map((item, index) => {
return (
{notes
.filter((i) => i.type === "header")
.map((item, index) => {
return item.title ? (
<PressableButton
key={item.group.id}
onPress={() => onPress(item)}
type={currentIndex === index ? "accent" : "transparent"}
key={item.title}
onPress={() => onPress(item, index)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 30,
height: 25,
marginVertical: 10
}}
>
@@ -199,21 +171,20 @@ const JumpToSectionDialog = () => {
size={SIZE.sm}
color={
currentIndex === index
? colors.static.white
: colors.primary.paragraph
? colors.selected.accent
: colors.primary.accent
}
style={{
textAlign: "center"
}}
>
{item.group.title}
{item.title}
</Paragraph>
</PressableButton>
);
) : null;
})}
</View>
</ScrollView>
)}
</View>
</ScrollView>
</View>
</BaseDialog>
);

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import {
addOrientationListener,
@@ -85,7 +85,7 @@ const PDFPreview = () => {
return () => {
eUnSubscribeEvent("PDFPreview", open);
};
}, [open]);
}, []);
const onOrientationChange = (o) => {
if (o.includes("LANDSCAPE")) {
@@ -102,26 +102,23 @@ const PDFPreview = () => {
};
}, []);
const open = useCallback(
async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
},
[snapshot]
);
const open = async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.metadata.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
setPDFSource(null);
@@ -133,7 +130,7 @@ const PDFPreview = () => {
if (error?.message === "Password required or incorrect password.") {
await sleep(300);
presentDialog({
context: attachment?.hash,
context: attachment?.metadata?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
@@ -154,8 +151,8 @@ const PDFPreview = () => {
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.hash} />
<Dialog context={attachment?.hash} />
<SheetProvider context={attachment?.metadata?.hash} />
<Dialog context={attachment?.metadata?.hash} />
<View
style={{
@@ -264,7 +261,7 @@ const PDFPreview = () => {
color={colors.static.white}
name="download"
onPress={() => {
downloadAttachment(attachment.hash, false);
downloadAttachment(attachment.metadata.hash, false);
}}
/>
</View>

View File

@@ -48,7 +48,7 @@ export const ProFeatures = ({ count = 6 }) => {
content: "Instantly sync to unlimited devices"
},
{
content: "A private vault to keep everything important always locked"
content: "A private vault to keep everything imporant always locked"
},
{
content:

View File

@@ -23,17 +23,21 @@ import { InteractionManager, View } from "react-native";
import Share from "react-native-share";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { editorController } from "../../../screens/editor/tiptap/utils";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { convertNoteToText } from "../../../utils/note-to-text";
import { getElevationStyle } from "../../../utils/elevation";
import {
eClearEditor,
eCloseActionSheet,
eCloseVaultDialog,
eOnLoadNote,
@@ -41,7 +45,6 @@ import {
} from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { tabBarRef } from "../../../utils/global-refs";
import { convertNoteToText } from "../../../utils/note-to-text";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
@@ -52,6 +55,10 @@ import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
let Keychain;
const passInputRef = createRef();
const confirmPassRef = createRef();
const changePassInputRef = createRef();
export class VaultDialog extends Component {
constructor(props) {
super(props);
@@ -81,11 +88,6 @@ export class VaultDialog extends Component {
deleteVault: false,
deleteAll: false
};
this.passInputRef = createRef();
this.confirmPassRef = createRef();
this.changePassInputRef = createRef();
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
@@ -146,6 +148,9 @@ export class VaultDialog extends Component {
* @param {import('../../../services/event-manager').vaultType} data
*/
open = async (data) => {
if (!Keychain) {
Keychain = require("react-native-keychain");
}
let biometry = await BiometricService.isBiometryAvailable();
let available = false;
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
@@ -194,7 +199,7 @@ export class VaultDialog extends Component {
close = () => {
if (this.state.loading) {
ToastManager.show({
ToastEvent.show({
heading: this.state.title,
message: "Please wait and do not close the app.",
type: "success",
@@ -207,6 +212,7 @@ export class VaultDialog extends Component {
this.password = null;
this.confirmPassword = null;
SearchService.updateAndSearch();
this.setState({
visible: false,
note: {},
@@ -230,7 +236,7 @@ export class VaultDialog extends Component {
if (this.state.loading) return;
if (!this.password) {
ToastManager.show({
ToastEvent.show({
heading: "Password not entered",
message: "Enter a password for the vault and try again.",
type: "error",
@@ -239,7 +245,7 @@ export class VaultDialog extends Component {
return;
}
if (this.password && this.password.length < 3) {
ToastManager.show({
ToastEvent.show({
heading: "Password too short",
message: "Password must be longer than 3 characters.",
type: "error",
@@ -251,7 +257,7 @@ export class VaultDialog extends Component {
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastManager.show({
ToastEvent.show({
heading: "Passwords do not match",
type: "error",
context: "local"
@@ -277,7 +283,7 @@ export class VaultDialog extends Component {
if (this.state.biometricUnlock) {
this._enrollFingerprint(this.newPassword);
}
ToastManager.show({
ToastEvent.show({
heading: "Vault password updated successfully",
type: "success",
context: "global"
@@ -289,19 +295,17 @@ export class VaultDialog extends Component {
loading: false
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
context: "local"
});
} else {
ToastManager.error(e);
}
});
} else if (this.state.locked) {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -351,7 +355,7 @@ export class VaultDialog extends Component {
});
this.close();
} else {
ToastManager.show({
ToastEvent.show({
heading: "Account password incorrect",
message: "Please enter correct password for your account.",
type: "error",
@@ -378,7 +382,7 @@ export class VaultDialog extends Component {
this.close();
eSendEvent("vaultUpdated");
} catch (e) {
ToastManager.show({
ToastEvent.show({
heading: "Vault password incorrect",
message: "Please enter correct password to clear vault.",
type: "error",
@@ -392,7 +396,7 @@ export class VaultDialog extends Component {
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -400,12 +404,11 @@ export class VaultDialog extends Component {
return;
} else {
await db.vault.add(this.state.note.id);
// if (this.state.note.id === editorController.current?.note?.id) {
// eSendEvent(eClearEditor, );
// }
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
this.close();
ToastManager.show({
ToastEvent.show({
message: "Note locked successfully",
type: "error",
context: "local"
@@ -418,7 +421,7 @@ export class VaultDialog extends Component {
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -439,7 +442,6 @@ export class VaultDialog extends Component {
if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) {
await this._enrollFingerprint(this.password);
}
if (this.state.goToEditor) {
this._openInEditor(note);
} else if (this.state.share) {
@@ -450,13 +452,14 @@ export class VaultDialog extends Component {
await this._copyNote(note);
}
} catch (e) {
console.log(e);
this._takeErrorAction(e);
}
};
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems([this.state.note.id], "note");
await deleteItems(this.state.note);
this.close();
} catch (e) {
this._takeErrorAction(e);
@@ -464,40 +467,44 @@ export class VaultDialog extends Component {
}
async _enrollFingerprint(password) {
this.setState(
{
loading: true
},
async () => {
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
this.setState({
loading: false
});
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
context: "global"
});
this.close();
} catch (e) {
this.close();
ToastManager.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
type: "error",
context: "local"
});
this.setState({
loading: false
});
try {
this.setState(
{
loading: true
},
async () => {
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
this.setState({
loading: false
});
eSendEvent("vaultUpdated");
ToastEvent.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
context: "global"
});
this.close();
} catch (e) {
ToastEvent.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
type: "error",
context: "local"
});
this.setState({
loading: false
});
return;
}
}
}
);
);
} catch (e) {
this._takeErrorAction(e);
}
}
async _createVault() {
@@ -508,14 +515,13 @@ export class VaultDialog extends Component {
}
if (this.state.note?.id) {
await db.vault.add(this.state.note.id);
// TODO
// if (this.state.note.id === editorController.current?.note?.id) {
// eSendEvent(eClearEditor);
// }
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
this.setState({
loading: false
});
ToastManager.show({
ToastEvent.show({
heading: "Note added to vault",
type: "success",
context: "global"
@@ -523,7 +529,7 @@ export class VaultDialog extends Component {
this.close();
} else {
eSendEvent("vaultUpdated");
ToastManager.show({
ToastEvent.show({
heading: "Vault created successfully",
type: "success",
context: "global"
@@ -536,8 +542,8 @@ export class VaultDialog extends Component {
db.vault
.remove(this.state.note.id, this.password)
.then(() => {
ToastManager.show({
heading: "Note permanently unlocked.",
ToastEvent.show({
heading: "Note permanantly unlocked.",
type: "success",
context: "global"
});
@@ -551,9 +557,7 @@ export class VaultDialog extends Component {
_openInEditor(note) {
this.close();
InteractionManager.runAfterInteractions(async () => {
eSendEvent(eOnLoadNote, {
item: note
});
eSendEvent(eOnLoadNote, note);
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
@@ -562,7 +566,7 @@ export class VaultDialog extends Component {
async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note));
ToastManager.show({
ToastEvent.show({
heading: "Note copied",
type: "success",
message: "Note has been copied to clipboard!",
@@ -594,7 +598,7 @@ export class VaultDialog extends Component {
visible: true
});
setTimeout(() => {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -609,13 +613,13 @@ export class VaultDialog extends Component {
try {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastManager.show({
ToastEvent.show({
heading: "Biometric unlocking disabled!",
type: "success",
context: "global"
});
} catch (e) {
ToastManager.show({
ToastEvent.show({
heading: "Failed to disable Biometric unlocking.",
message: e.message,
type: "success",
@@ -667,8 +671,7 @@ export class VaultDialog extends Component {
<BaseDialog
onShow={async () => {
await sleep(100);
this.passInputRef.current?.focus();
passInputRef.current?.focus();
}}
statusBarTranslucent={false}
onRequestClose={this.close}
@@ -703,7 +706,7 @@ export class VaultDialog extends Component {
!this.state.revokeFingerprintAccess ? (
<>
<Input
fwdRef={this.passInputRef}
fwdRef={passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
@@ -720,7 +723,7 @@ export class VaultDialog extends Component {
}
onSubmit={() => {
changePassword
? this.confirmPassRef.current?.focus()
? changePassInputRef.current?.focus()
: this.onPress;
}}
autoComplete="password"
@@ -772,7 +775,7 @@ export class VaultDialog extends Component {
<>
<Seperator half />
<Input
ref={this.confirmPassRef}
ref={changePassInputRef}
editable={!loading}
testID={notesnook.ids.dialogs.vault.changePwd}
autoCapitalize="none"
@@ -792,7 +795,7 @@ export class VaultDialog extends Component {
{!novault ? (
<View>
<Input
fwdRef={this.passInputRef}
fwdRef={passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
@@ -803,13 +806,13 @@ export class VaultDialog extends Component {
returnKeyType="next"
secureTextEntry
onSubmit={() => {
this.confirmPassRef.current?.focus();
confirmPassRef.current?.focus();
}}
placeholder="Password"
/>
<Input
fwdRef={this.confirmPassRef}
fwdRef={confirmPassRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry

View File

@@ -39,9 +39,9 @@ class ExceptionHandler extends React.Component<{
error: Error | null;
hasError: boolean;
} = {
hasError: false,
error: null
};
hasError: false,
error: null
};
static getDerivedStateFromError(error: Error) {
return { hasError: true, error: error };
}
@@ -69,7 +69,7 @@ class ExceptionHandler extends React.Component<{
this.props.component
)}
defaultTitle={this.state.error?.message}
issueTitle="An exception occurred"
issueTitle="An exception occured"
/>
<Dialog />
</SafeAreaView>

View File

@@ -17,73 +17,42 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform, StyleSheet, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { SearchBar } from "../../screens/search/search-bar";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore, {
RouteName
} from "../../stores/use-navigation-store";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
type HeaderRightButton = {
title: string;
onPress: () => void;
};
export const Header = ({
renderedInRoute,
onLeftMenuButtonPress,
title,
titleHiddenOnRender,
headerRightButtons,
id,
accentColor,
isBeta,
canGoBack,
onPressDefaultRightButton,
hasSearch,
onSearch
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute: RouteName;
id?: string;
title: string;
headerRightButtons?: HeaderRightButton[];
titleHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
canGoBack?: boolean;
onPressDefaultRightButton?: () => void;
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const _Header = () => {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true);
const [hide, setHide] = useState(true);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
const currentScreen = useNavigationStore(
(state) => state.currentScreen?.name
);
const onScroll = useCallback(
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
(data: { x: number; y: number }) => {
if (data.y > 150) {
if (!borderHidden) return;
setBorderHidden(false);
if (!hide) return;
setHide(false);
} else {
if (borderHidden) return;
setBorderHidden(true);
if (hide) return;
setHide(true);
}
},
[borderHidden, id, renderedInRoute]
[hide]
);
useEffect(() => {
@@ -91,9 +60,9 @@ export const Header = ({
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [borderHidden, onScroll]);
}, [hide, onScroll]);
return selectionMode && isFocused ? null : (
return selectionMode ? null : (
<>
<View
style={[
@@ -103,42 +72,29 @@ export const Header = ({
backgroundColor: colors.primary.background,
overflow: "hidden",
borderBottomWidth: 1,
borderBottomColor: borderHidden
borderBottomColor: hide
? "transparent"
: colors.secondary.background,
justifyContent: "space-between"
}
]}
>
<>
<View style={styles.leftBtnContainer}>
<LeftMenus
canGoBack={canGoBack}
onLeftButtonPress={onLeftMenuButtonPress}
/>
<Title
isHiddenOnRender={titleHiddenOnRender}
renderedInRoute={renderedInRoute}
id={id}
accentColor={accentColor}
title={title}
isBeta={isBeta}
/>
</View>
<RightMenus
renderedInRoute={renderedInRoute}
id={id}
headerRightButtons={headerRightButtons}
onPressDefaultRightButton={onPressDefaultRightButton}
search={hasSearch}
onSearch={onSearch}
/>
</>
{currentScreen === "Search" ? (
<SearchBar />
) : (
<>
<View style={styles.leftBtnContainer}>
<LeftMenus />
<Title />
</View>
<RightMenus />
</>
)}
</View>
</>
);
};
export const Header = React.memo(_Header, () => true);
const styles = StyleSheet.create({
container: {

View File

@@ -17,29 +17,23 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { notesnook } from "../../../e2e/test.ids";
import { DDS } from "../../services/device-detection";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { tabBarRef } from "../../utils/global-refs";
import { IconButton } from "../ui/icon-button";
export const LeftMenus = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
export const LeftMenus = () => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const canGoBack = useNavigationStore((state) => state.canGoBack);
const isTablet = deviceMode === "tablet";
const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
const onLeftButtonPress = () => {
if (!canGoBack) {
if (tabBarRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
@@ -60,17 +54,21 @@ export const LeftMenus = ({
height: 40,
width: 40,
borderRadius: 100,
marginLeft: -5,
marginRight: DDS.isLargeTablet() ? 10 : 7
}}
left={40}
top={40}
right={DDS.isLargeTablet() ? 10 : 10}
onPress={_onLeftButtonPress}
onPress={onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}
name={canGoBack ? "arrow-left" : "menu"}
color={colors.primary.paragraph}
iconStyle={{
marginLeft: canGoBack ? -5 : 0
}}
/>
);
};

View File

@@ -20,46 +20,43 @@ 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 { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import {
HeaderRightButton,
RouteName
} from "../../stores/use-navigation-store";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
export const RightMenus = ({
headerRightButtons,
renderedInRoute,
id,
onPressDefaultRightButton,
search,
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;
onSearch?: () => void;
}) => {
export const RightMenus = () => {
const { colors } = useThemeColors();
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const deviceMode = useSettingStore((state) => state.deviceMode);
const menuRef = useRef<Menu>(null);
const buttons = useNavigationStore((state) => state.headerRightButtons);
const currentScreen = useNavigationStore((state) => state.currentScreen.name);
const buttonAction = useNavigationStore((state) => state.buttonAction);
const menuRef = useRef<{
show: () => void;
hide: () => void;
}>();
return (
<View style={styles.rightBtnContainer}>
{search ? (
{!currentScreen.startsWith("Settings") ? (
<IconButton
onPress={onSearch}
onPress={async () => {
SearchService.prepareSearch();
Navigation.navigate(
{
name: "Search"
},
{}
);
}}
testID="icon-search"
name="magnify"
color={colors.primary.paragraph}
@@ -69,9 +66,9 @@ export const RightMenus = ({
{deviceMode !== "mobile" ? (
<Button
onPress={onPressDefaultRightButton}
onPress={buttonAction}
testID={notesnook.ids.default.addBtn}
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
icon={currentScreen === "Trash" ? "delete" : "plus"}
iconSize={SIZE.xl}
type="shade"
hitSlop={{
@@ -92,7 +89,7 @@ export const RightMenus = ({
/>
) : null}
{headerRightButtons && headerRightButtons.length > 0 ? (
{buttons && buttons.length > 0 ? (
<Menu
ref={menuRef}
animationDuration={200}
@@ -114,7 +111,7 @@ export const RightMenus = ({
/>
}
>
{headerRightButtons.map((item) => (
{buttons.map((item) => (
<Button
style={{
width: 150,

View File

@@ -20,76 +20,103 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
import { db } from "../../common/database";
import Notebook from "../../screens/notebook";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import { eScrollEvent } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Tag from "../ui/tag";
import Heading from "../ui/typography/heading";
export const Title = ({
title,
isHiddenOnRender,
accentColor,
isBeta,
renderedInRoute,
id
}: {
title: string;
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute: string;
id?: string;
}) => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(isHiddenOnRender);
const isTag = title.startsWith("#");
const onScroll = useCallback(
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== "Notebook") return;
const titleState: { [name: string]: boolean } = {};
if (data.route !== renderedInRoute || data.id !== id) return;
export const Title = () => {
const { colors } = useThemeColors();
const currentScreen = useNavigationStore((state) => state.currentScreen);
const isNotebook = currentScreen.name === "Notebook";
const isTopic = currentScreen?.name === "TopicNotes";
const [hide, setHide] = useState(
isNotebook
? typeof titleState[currentScreen.id as string] === "boolean"
? titleState[currentScreen.id as string]
: true
: false
);
const isHidden = titleState[currentScreen.id as string];
const notebook =
isTopic && currentScreen.notebookId
? db.notebooks?.notebook(currentScreen.notebookId)?.data
: null;
const title = currentScreen.title;
const isTag = currentScreen?.name === "TaggedNotes";
const onScroll = useCallback(
(data: { x: number; y: number }) => {
if (currentScreen.name !== "Notebook") {
setHide(false);
return;
}
if (data.y > 150) {
if (!visible) return;
setVisible(false);
if (!hide) return;
titleState[currentScreen.id as string] = false;
setHide(false);
} else {
if (visible) return;
setVisible(true);
if (hide) return;
titleState[currentScreen.id as string] = true;
setHide(true);
}
},
[id, renderedInRoute, visible]
[currentScreen.id, currentScreen.name, hide]
);
useEffect(() => {
if (currentScreen.name === "Notebook") {
const value =
typeof titleState[currentScreen.id as string] === "boolean"
? titleState[currentScreen.id as string]
: true;
setHide(value);
} else {
setHide(titleState[currentScreen.id as string]);
}
}, [currentScreen.id, currentScreen.name]);
useEffect(() => {
eSubscribeEvent(eScrollEvent, onScroll);
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [visible, onScroll]);
}, [hide, onScroll]);
function navigateToNotebook() {
if (!isTopic) return;
Notebook.navigate(notebook, true);
}
return (
<>
{!visible ? (
{!hide && !isHidden ? (
<Heading
onPress={navigateToNotebook}
numberOfLines={1}
size={SIZE.lg}
style={{
flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0
}}
color={accentColor || colors.primary.heading}
color={currentScreen.color || colors.primary.heading}
>
{isTag ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
{isTag ? title.slice(1) : title}{" "}
{title}{" "}
<Tag
visible={isBeta}
visible={currentScreen.beta}
text="BETA"
style={{
backgroundColor: "transparent"

View File

@@ -32,22 +32,23 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
const [visible, setVisible] = useState(false);
const [image, setImage] = useState<string>();
const [image, setImage] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
return () => {
eUnSubscribeEvent("ImagePreview", open);
};
}, []);
const open = async (image: ImageAttributes) => {
const open = async (image) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
@@ -62,7 +63,6 @@ const ImagePreview = () => {
});
}
if (!hash) return;
//@ts-ignore // FIX ME
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
@@ -74,7 +74,7 @@ const ImagePreview = () => {
};
const close = () => {
setImage(undefined);
setImage(null);
setVisible(false);
};
@@ -88,7 +88,7 @@ const ImagePreview = () => {
backgroundColor: "black"
}}
>
{loading || !image ? (
{loading ? (
<View
style={{
flex: 1,

View File

@@ -77,9 +77,14 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
}, 1000);
}}
style={{
@@ -101,9 +106,14 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
await sleep(1000);
eSendEvent(eCloseLoading);
}}

View File

@@ -17,39 +17,57 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { useMessageStore } from "../../../stores/use-message-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { ColorValues } from "../../../utils/colors";
import { Announcement } from "../../announcements/announcement";
import { Card } from "../../list/card";
export type ListHeaderProps = {
noAnnouncement?: boolean;
color?: string;
messageCard?: boolean;
screen?: string;
shouldShow?: boolean;
};
import Paragraph from "../../ui/typography/paragraph";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { SIZE } from "../../../utils/size";
import { useSelectionStore } from "../../../stores/use-selection-store";
export const Header = React.memo(
({
type,
messageCard = true,
color,
shouldShow = false,
noAnnouncement,
screen
}: ListHeaderProps) => {
warning
}) => {
const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements);
const selectionMode = useSelectionStore((state) => state.selectionMode);
return selectionMode ? null : (
<>
{announcements.length !== 0 && !noAnnouncement ? (
{warning ? (
<View
style={{
padding: 12,
backgroundColor: colors.error.background,
width: "95%",
alignSelf: "center",
borderRadius: 5,
flexDirection: "row",
alignItems: "center"
}}
>
<Icon
name="sync-alert"
size={SIZE.md}
color={colors.error.icon}
f
/>
<Paragraph style={{ marginLeft: 5 }} color={colors.error.icon}>
{warning.title}
</Paragraph>
</View>
) : announcements.length !== 0 && !noAnnouncement ? (
<Announcement color={color || colors.primary.accent} />
) : (screen as any) === "Search" ? null : !shouldShow ? (
) : type === "search" ? null : !shouldShow ? (
<View
style={{
marginBottom: 5,
@@ -60,7 +78,11 @@ export const Header = React.memo(
}}
>
{messageCard ? (
<Card color={color || colors.primary.accent} />
<Card
color={
ColorValues[color?.toLowerCase()] || colors.primary.accent
}
/>
) : null}
</View>
) : null}

View File

@@ -17,33 +17,27 @@ 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 { Notebook } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { useRef, useState } from "react";
import React from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { ToastManager } from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useMenuStore } from "../../../stores/use-menu-store";
import { ToastEvent } from "../../../services/event-manager";
import { getTotalNotes } from "@notesnook/common";
import { db } from "../../../common/database";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
export const NotebookHeader = ({
notebook,
onEditNotebook,
totalNotes = 0
}: {
notebook: Notebook;
onEditNotebook: () => void;
totalNotes: number;
}) => {
export const NotebookHeader = ({ notebook, onEditNotebook }) => {
const { colors } = useThemeColors();
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(notebook.id)
);
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const totalNotes = getTotalNotes(notebook);
const shortcutRef = useRef();
const onPinNotebook = async () => {
try {
@@ -56,7 +50,7 @@ export const NotebookHeader = ({
type: "notebook"
}
});
ToastManager.show({
ToastEvent.show({
heading: "Shortcut created",
type: "success"
});
@@ -82,7 +76,7 @@ export const NotebookHeader = ({
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{getFormattedDate(notebook.dateModified, "date-time")}
{new Date(notebook.dateEdited).toLocaleString()}
</Paragraph>
<View
style={{
@@ -109,6 +103,7 @@ export const NotebookHeader = ({
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
onPress={onPinNotebook}
tooltipText={"Create shortcut in side menu"}
fwdRef={shortcutRef}
customStyle={{
marginRight: 15,
width: 40,
@@ -143,15 +138,15 @@ export const NotebookHeader = ({
style={{
marginTop: 10,
fontStyle: "italic",
fontFamily: undefined
fontFamily: null
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{/* {notebook.topics.length === 1
{notebook.topics.length === 1
? "1 topic"
: `${notebook.topics.length} topics`}
,{" "} */}
,{" "}
{notebook && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1

View File

@@ -17,57 +17,65 @@ 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 { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import React, { useEffect, useRef, useState } from "react";
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { presentSheet } from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { RouteName } from "../../../stores/use-navigation-store";
import { ColorValues } from "../../../utils/colors";
import { GROUP } from "../../../utils/constants";
import { ColorValues } from "../../../utils/colors";
import { db } from "../../../common/database";
import { eOpenJumpToDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import Sort from "../../sheets/sort";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Button } from "../../ui/button";
import Sort from "../../sheets/sort";
import Heading from "../../ui/typography/heading";
import { useCallback } from "react";
type SectionHeaderProps = {
item: GroupHeader;
index: number;
dataType: ItemType;
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
onOpenJumpToDialog: () => void;
};
export const SectionHeader = React.memo<
React.FunctionComponent<SectionHeaderProps>
>(
function SectionHeader({
item,
index,
dataType,
color,
screen,
groupOptions,
onOpenJumpToDialog
}: SectionHeaderProps) {
export const SectionHeader = React.memo(
function SectionHeader({ item, index, type, color, screen }) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
const [groupOptions, setGroupOptions] = useState(
db.settings?.getGroupOptions(type)
);
let groupBy = Object.keys(GROUP).find(
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
(key) => GROUP[key] === groupOptions.groupBy
);
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
const jumpToRef = useRef();
const sortRef = useRef();
const compactModeRef = useRef();
const notebooksListMode = useSettingStore(
(state) => state.settings.notebooksListMode
);
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode = type === "notebooks" ? notebooksListMode : notesListMode;
groupBy = !groupBy
? "Default"
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
const onUpdate = useCallback(() => {
setGroupOptions({ ...db.settings?.getGroupOptions(type) });
}, [type]);
useEffect(() => {
eSubscribeEvent("groupOptionsUpdate", onUpdate);
return () => {
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
};
}, [onUpdate]);
return (
<View
style={{
@@ -85,8 +93,9 @@ export const SectionHeader = React.memo<
>
<TouchableOpacity
onPress={() => {
onOpenJumpToDialog();
eSendEvent(eOpenJumpToDialog, type);
}}
ref={jumpToRef}
activeOpacity={0.9}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
style={{
@@ -95,10 +104,7 @@ export const SectionHeader = React.memo<
}}
>
<Heading
color={
ColorValues[color?.toLowerCase() as keyof typeof ColorValues] ||
colors.primary.accent
}
color={ColorValues[color?.toLowerCase()] || colors.primary.accent}
size={SIZE.sm}
style={{
minWidth: 60,
@@ -120,13 +126,13 @@ export const SectionHeader = React.memo<
<>
<Button
onPress={() => {
console.log("Opening Sort sheet", screen, dataType);
presentSheet({
component: <Sort screen={screen} type={dataType} />
component: <Sort screen={screen} type={type} />
});
}}
hidden={screen === "Reminders"}
tooltipText="Change sorting of items in list"
fwdRef={sortRef}
title={groupBy}
icon={
groupOptions.sortDirection === "asc"
@@ -139,9 +145,7 @@ export const SectionHeader = React.memo<
paddingHorizontal: 0,
backgroundColor: "transparent",
marginRight:
dataType === "note" ||
screen === "Notes" ||
dataType === "notebook"
type === "notes" || type === "home" || type === "notebooks"
? 10
: 0
}}
@@ -155,26 +159,24 @@ export const SectionHeader = React.memo<
height: 25
}}
hidden={
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes"
type !== "notes" && type !== "notebooks" && type !== "home"
}
testID="icon-compact-mode"
tooltipText={
isCompactModeEnabled
listMode == "compact"
? "Switch to normal mode"
: "Switch to compact mode"
}
fwdRef={compactModeRef}
color={colors.secondary.icon}
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
name={listMode == "compact" ? "view-list" : "view-list-outline"}
onPress={() => {
SettingsService.set({
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: !isCompactModeEnabled
? "compact"
: "normal"
});
let settings = {};
settings[
type !== "notebooks" ? "notesListMode" : "notebooksListMode"
] = listMode === "normal" ? "compact" : "normal";
SettingsService.set(settings);
}}
size={SIZE.lg - 2}
/>
@@ -186,10 +188,6 @@ export const SectionHeader = React.memo<
},
(prev, next) => {
if (prev.item.title !== next.item.title) return false;
if (prev.groupOptions?.groupBy !== next.groupOptions.groupBy) return false;
if (prev.groupOptions?.sortDirection !== next.groupOptions.sortDirection)
return false;
if (prev.groupOptions?.sortBy !== next.groupOptions.sortBy) return false;
return true;
}

View File

@@ -17,31 +17,21 @@ 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 {
BaseTrashItem,
Color,
Note,
Reminder,
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { EntityLevel, decode } from "entities";
import { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
import { decode, EntityLevel } from "entities";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import NotebookScreen from "../../../screens/notebook";
import { db } from "../../../common/database";
import Notebook from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../../utils/colors";
import { SIZE } from "../../../utils/size";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../../stores/resolve-items";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -49,41 +39,81 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useEditorStore } from "../../../stores/use-editor-store";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isTrash?: boolean;
noOpen?: boolean;
function navigateToTag(item) {
const tag = db.tags.tag(item.id);
if (!tag) return;
TaggedNotes.navigate(tag, true);
}
const showActionSheet = (item) => {
Properties.present(item);
};
function getNotebook(item) {
const isTrash = item.type === "trash";
const currentId = useNavigationStore.getState().currentScreen.id;
if (isTrash) return [];
const items = [];
const notebooks = db.relations.to(item, "notebook") || [];
for (let notebook of notebooks) {
if (items.length > 1) break;
if (notebook.id === currentId) continue;
items.push(notebook);
}
if (item.notebooks) {
for (let nb of item.notebooks) {
if (items.length > 1) break;
const notebook = db.notebooks?.notebook(nb.id)?.data;
if (!notebook) continue;
for (let topicId of nb.topics) {
if (items.length > 1) break;
if (topicId === currentId) continue;
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) continue;
items.push(topic);
}
}
}
return items;
}
function getTags(item) {
const noteTags = item.tags?.slice(0, 3) || [];
const tags = [];
for (const tagName of noteTags) {
const tag = db.tags.tag(tagName);
if (!tag) continue;
tags.push(tag);
}
return tags;
}
const NoteItem = ({
item,
isTrash,
date,
color,
notebooks,
reminder,
tags,
attachmentsCount,
dateBy = "dateCreated",
noOpen = false
}: NoteItemProps) => {
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
}) => {
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
const _update = useRelationStore((state) => state.updater);
const primaryColors = isEditingNote ? colors.selected : colors.primary;
// eslint-disable-next-line react-hooks/exhaustive-deps
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
const reminders = db.relations.from(item, "reminder");
const reminder = getUpcomingReminder(reminders);
const noteColor = ColorValues[item.color?.toLowerCase()];
const tags = getTags(item);
const primaryColors =
currentEditingNote === item.id ? colors.selected : colors.primary;
return (
<>
@@ -104,45 +134,44 @@ const NoteItem = ({
flexWrap: "wrap"
}}
>
{notebooks?.items
?.filter(
(item) =>
item.id !== useNavigationStore.getState().focusedRouteId
)
.map((item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
{notebooks?.map((item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
}
tooltipText={item.title}
key={item.id}
height={25}
icon={item.type === "topic" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
textStyle={{
marginRight: 0
}}
style={{
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => {
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
Notebook.navigate(item);
}
tooltipText={item.title}
key={item.id}
height={25}
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
textStyle={{
marginRight: 0
}}
style={{
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => {
NotebookScreen.navigate(item, true);
}}
/>
))}
}}
/>
))}
<ReminderTime
reminder={reminder}
color={color?.colorCode}
color={noteColor}
onPress={() => {
Properties.present(reminder);
}}
@@ -156,22 +185,26 @@ const NoteItem = ({
{compactMode ? (
<Paragraph
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={SIZE.sm}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{
paddingRight: 10
flexWrap: "wrap"
}}
size={SIZE.sm}
>
{item.title}
</Paragraph>
) : (
<Heading
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={SIZE.md}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{
paddingRight: 10
flexWrap: "wrap"
}}
size={SIZE.md}
>
{item.title}
</Heading>
@@ -220,11 +253,13 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
/>
{attachmentsCount > 0 ? (
{attachmentCount > 0 ? (
<View
style={{
flexDirection: "row",
@@ -241,7 +276,7 @@ const NoteItem = ({
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{attachmentsCount}
{attachmentCount}
</Paragraph>
</View>
) : null}
@@ -254,7 +289,10 @@ const NoteItem = ({
style={{
marginRight: 6
}}
color={color?.colorCode || primaryColors.accent}
color={
ColorValues[item.color?.toLowerCase()] ||
primaryColors.accent
}
/>
) : null}
@@ -283,10 +321,10 @@ const NoteItem = ({
) : null}
{!isTrash && !compactMode && tags
? tags.items?.map((item) =>
? tags.map((item) =>
item.id ? (
<Button
title={"#" + item.title}
title={"#" + item.alias}
key={item.id}
height={23}
type="gray"
@@ -300,9 +338,9 @@ const NoteItem = ({
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.items?.length > 1 ? 130 : null
maxWidth: tags.length > 1 ? 130 : null
}}
onPress={() => TaggedNotes.navigate(item, true)}
onPress={() => navigateToTag(item)}
/>
) : null
)
@@ -330,8 +368,7 @@ const NoteItem = ({
marginRight: 6
}}
>
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
)}
@@ -387,8 +424,8 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
time={item[dateBy]}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
/>
</>
) : null}
@@ -398,7 +435,7 @@ const NoteItem = ({
color={primaryColors.paragraph}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && Properties.present(item)}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
customStyle={{
justifyContent: "center",
height: 35,

View File

@@ -0,0 +1,135 @@
/*
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 NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
eSendEvent,
hideSheet,
openVault,
presentSheet
} from "../../../services/event-manager";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { presentDialog } from "../../dialog/functions";
import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper";
const present = () =>
presentDialog({
title: "Note not synced",
negativeText: "Ok",
paragraph: "Please sync again to open this note for editing"
});
export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
let _note = item;
if (isSheet) hideSheet();
if (!isTrash) {
_note = db.notes.note(item.id).data;
if (!db.notes.note(item.id)?.synced()) {
present();
return;
}
} else {
if (!db.trash.synced(item.id)) {
present();
return;
}
}
const { selectedItemsList, selectionMode, clearSelection } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem && setSelectedItem(_note);
return;
} else {
clearSelection();
}
if (_note.conflicted) {
eSendEvent(eShowMergeDialog, _note);
return;
}
if (_note.locked) {
openVault({
item: _note,
novault: true,
locked: true,
goToEditor: true,
title: "Open note",
description: "Unlock note to open it in editor."
});
return;
}
if (isTrash) {
const content = await db.content.get(item.contentId);
presentSheet({
component: (
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
)
});
} else {
useEditorStore.getState().setReadonly(_note?.readonly);
eSendEvent(eOnLoadNote, _note);
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
}
};
export const NoteWrapper = React.memo(
function NoteWrapper({ item, index, dateBy, isSheet }) {
const isTrash = item.type === "trash";
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
return (
<SelectionWrapper
index={index}
height={100}
testID={notesnook.ids.note.get(index)}
onPress={() => openNote(item, isTrash, setSelectedItem, isSheet)}
isSheet={isSheet}
item={item}
>
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (prev.item !== next.item) {
return false;
}
return true;
}
);

View File

@@ -1,156 +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 { BaseTrashItem, Color, Note, Reminder } from "@notesnook/core";
import React from "react";
import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
eSendEvent,
hideSheet,
openVault,
presentSheet
} from "../../../services/event-manager";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../../stores/resolve-items";
import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper";
export const openNote = async (
item: Note,
isTrash?: boolean,
isSheet?: boolean
) => {
let note: Note = item;
if (isSheet) hideSheet();
if (!isTrash) {
note = (await db.notes.note(item.id)) as Note;
}
if (useSelectionStore.getState().selectionMode === item.type) {
const {
selectedItemsList,
selectionMode,
clearSelection,
setSelectedItem
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(note.id);
} else {
clearSelection();
}
return;
}
if (note.conflicted) {
eSendEvent(eShowMergeDialog, note);
return;
}
if (isTrash) {
if (!note.contentId) return;
const content = await db.content.get(note.contentId as string);
presentSheet({
component: (
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
)
});
} else {
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
}
};
type NoteWrapperProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isRenderedInActionSheet: boolean;
};
export const NoteWrapper = React.memo<
React.FunctionComponent<NoteWrapperProps>
>(
function NoteWrapper({
item,
index,
isRenderedInActionSheet,
...restProps
}: NoteWrapperProps) {
const isTrash = item.type === "trash";
return (
<SelectionWrapper
testID={notesnook.ids.note.get(index)}
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
isSheet={isRenderedInActionSheet}
item={item}
color={restProps.color?.colorCode}
>
<NoteItem {...restProps} item={item} index={index} isTrash={isTrash} />
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.date !== next.date) {
return false;
}
if (
prev.tags?.dateEdited !== next.tags?.dateEdited ||
prev.tags?.items?.length !== next.tags?.items?.length
)
return false;
if (
prev.notebooks?.dateEdited !== next.notebooks?.dateEdited ||
prev.notebooks?.items?.length !== next.notebooks?.items?.length
)
return false;
if (prev.color !== next.color) return false;
if (prev.reminder?.id !== next.reminder?.id) return false;
if (prev.attachmentsCount !== next.attachmentsCount) return false;
if (prev.item?.dateModified !== next.item?.dateModified) {
return false;
}
if (prev.item?.id !== next.item?.id) return false;
return true;
}
);

View File

@@ -17,38 +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 { getFormattedDate } from "@notesnook/common";
import { BaseTrashItem, Notebook, TrashItem } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
type NotebookItemProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
isTrash: boolean;
const showActionSheet = (item) => {
Properties.present(item);
};
const navigateToTopic = (topic) => {
if (useSelectionStore.getState().selectedItemsList.length > 0) return;
TopicNotes.navigate(topic, true);
};
export const NotebookItem = ({
item,
isTopic = false,
isTrash,
date,
dateBy,
totalNotes
}: NotebookItemProps) => {
}) => {
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
const topics = item.topics?.slice(0, 3) || [];
return (
<>
@@ -80,7 +83,7 @@ export const NotebookItem = ({
</Heading>
)}
{!item.description || compactMode ? null : (
{isTopic || !item.description || compactMode ? null : (
<Paragraph
size={SIZE.sm}
numberOfLines={2}
@@ -92,6 +95,42 @@ export const NotebookItem = ({
</Paragraph>
)}
{isTopic || compactMode ? null : (
<View
style={{
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap"
}}
>
{topics.map((topic) => (
<Button
title={topic.title}
key={topic.id}
height={null}
textStyle={{
marginRight: 0
}}
type="grayBg"
fontSize={SIZE.xs}
icon="bookmark-outline"
iconSize={SIZE.sm}
style={{
borderRadius: 5,
maxWidth: 120,
borderWidth: 0.5,
paddingVertical: 2.5,
borderColor: colors.primary.border,
paddingHorizontal: 6,
marginVertical: 5,
marginRight: 5
}}
onPress={() => navigateToTopic(topic)}
/>
))}
</View>
)}
{!compactMode ? (
<View
style={{
@@ -113,9 +152,7 @@ export const NotebookItem = ({
}}
>
{"Deleted on " +
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)}
new Date(item.dateDeleted).toISOString().slice(0, 10)}
</Paragraph>
<Paragraph
color={colors.primary.accent}
@@ -125,8 +162,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
) : (
@@ -137,7 +173,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{getFormattedDate(date, "date")}
{getFormattedDate(item[dateBy], "date")}
</Paragraph>
)}
<Paragraph
@@ -197,7 +233,7 @@ export const NotebookItem = ({
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => Properties.present(item)}
onPress={() => showActionSheet(item)}
customStyle={{
justifyContent: "center",
height: 35,

View File

@@ -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 { BaseTrashItem, Notebook } from "@notesnook/core";
import React from "react";
import { NotebookItem } from ".";
import { db } from "../../../common/database";
import NotebookScreen from "../../../screens/notebook";
import { ToastManager } from "../../../services/event-manager";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { ToastEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { db } from "../../../common/database";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper from "../selection-wrapper";
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
const navigateToNotebook = (item, canGoBack) => {
if (!item) return;
Navigation.navigate(
{
title: item.title,
name: "Notebook",
id: item.id,
type: "notebook"
},
{
title: item.title,
item: item,
canGoBack
}
);
};
export const openNotebookTopic = (item) => {
const isTrash = item.type === "trash";
const { selectedItemsList, setSelectedItem, selectionMode, clearSelection } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem(item);
return;
} else {
clearSelection();
@@ -42,25 +59,25 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
if (isTrash) {
presentDialog({
title: `Restore notebook`,
paragraph: `Restore or delete notebook forever`,
title: `Restore ${item.itemType}`,
paragraph: `Restore or delete ${item.itemType} forever`,
positiveText: "Restore",
negativeText: "Delete",
positivePress: async () => {
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Restore successful",
type: "success"
});
},
onClose: async () => {
await db.trash.delete(item.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Permanently deleted items",
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Permanantly deleted items",
type: "success",
context: "local"
});
@@ -68,30 +85,29 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
});
return;
}
NotebookScreen.navigate(item, true);
};
type NotebookWrapperProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
navigateToNotebook(item, true);
}
};
export const NotebookWrapper = React.memo(
function NotebookWrapper({
item,
index,
date,
totalNotes
}: NotebookWrapperProps) {
function NotebookWrapper({ item, index, dateBy, totalNotes }) {
const isTrash = item.type === "trash";
return (
<SelectionWrapper onPress={() => openNotebook(item)} item={item}>
<SelectionWrapper
pinned={item.pinned}
index={index}
onPress={() => openNotebookTopic(item)}
height={item.type === "topic" ? 80 : 110}
item={item}
>
<NotebookItem
isTopic={item.type === "topic"}
item={item}
date={date}
dateBy={dateBy}
index={index}
isTrash={isTrash}
totalNotes={totalNotes}
@@ -101,9 +117,17 @@ export const NotebookWrapper = React.memo(
},
(prev, next) => {
if (prev.totalNotes !== next.totalNotes) return false;
if (prev.date !== next.date) return false;
if (prev.item?.dateModified !== next.item?.dateModified) return false;
if (prev.item?.id !== next.item?.id) return false;
if (prev.item.title !== next.item.title) return false;
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
return false;
}
return true;
}

View File

@@ -16,21 +16,20 @@ 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 { Reminder } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { useSelectionStore } from "../../../stores/use-selection-store";
import type { Reminder } from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import ReminderSheet from "../../sheets/reminder";
import { IconButton } from "../../ui/icon-button";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ReminderSheet from "../../sheets/reminder";
import { ReminderTime } from "../../ui/reminder-time";
const ReminderItem = React.memo(
({
@@ -44,23 +43,17 @@ const ReminderItem = React.memo(
}) => {
const { colors } = useThemeColors();
const openReminder = () => {
const {
selectedItemsList,
setSelectedItem,
selectionMode,
clearSelection
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
}
ReminderSheet.present(item, undefined, isSheet);
};
return (
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
<SelectionWrapper
//@ts-ignore
index={index}
height={100}
onPress={openReminder}
item={item}
isSheet={isSheet}
>
<View
style={{
flexShrink: 1,
@@ -138,11 +131,7 @@ const ReminderItem = React.memo(
height: 30
}}
>
<Icon
name="reload"
size={SIZE.md}
color={colors.primary.accent}
/>
<Icon name="reload" size={SIZE.md} color={colors.primary.accent} />
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}

View File

@@ -17,23 +17,21 @@ 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 { Item } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import useIsSelected from "../../../hooks/use-selected";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { useEditorStore } from "../../../stores/use-editor-store";
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
export const Filler = ({ item }) => {
const { colors } = useThemeColors();
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
);
const [selected] = useIsSelected(item);
return isEditingNote || selected ? (
return currentEditingNote === item.id || selected ? (
<View
style={{
position: "absolute",
@@ -41,11 +39,12 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
height: "150%",
backgroundColor: colors.selected.background,
borderLeftWidth: 5,
borderLeftColor: isEditingNote
? color
? color
: colors.selected.accent
: "transparent"
borderLeftColor:
currentEditingNote === item.id
? item.color
? colors.static[item.color]
: colors.selected.accent
: "transparent"
}}
collapsable={false}
/>

View File

@@ -18,45 +18,34 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { PropsWithChildren, useRef } from "react";
import React, { useRef } from "react";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { PressableButton } from "../../ui/pressable";
import { Filler } from "./back-fill";
import { SelectionIcon } from "./selection";
import { Item, TrashItem } from "@notesnook/core";
type SelectionWrapperProps = PropsWithChildren<{
item: Item;
onPress: () => void;
testID?: string;
isSheet?: boolean;
color?: string;
}>;
const SelectionWrapper = ({
children,
item,
background,
onPress,
testID,
isSheet,
children,
color
}: SelectionWrapperProps) => {
isSheet
}) => {
const itemId = useRef(item.id);
const { colors, isDark } = useThemeColors();
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
if (item.id !== itemId.current) {
itemId.current = item.id;
}
const onLongPress = () => {
if (useSelectionStore.getState().selectionMode !== item.type) {
useSelectionStore.getState().setSelectionMode(item.type);
if (!useSelectionStore.getState().selectionMode) {
useSelectionStore.getState().setSelectionMode(true);
}
useSelectionStore.getState().setSelectedItem(item.id);
useSelectionStore.getState().setSelectedItem(item);
};
return (
@@ -80,8 +69,14 @@ const SelectionWrapper = ({
marginBottom: isSheet ? 12 : undefined
}}
>
{item.type === "note" ? <Filler item={item} color={color} /> : null}
<SelectionIcon item={item} />
{item.type === "note" ? (
<Filler background={background} item={item} />
) : null}
<SelectionIcon
compactMode={compactMode}
item={item}
onLongPress={onLongPress}
/>
{children}
</PressableButton>
);

View File

@@ -25,16 +25,13 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab
import useIsSelected from "../../../hooks/use-selected";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { Item, TrashItem } from "@notesnook/core";
export const SelectionIcon = ({ item }: { item: Item }) => {
export const SelectionIcon = ({ item }) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
return selectionMode ? (
<View

View File

@@ -17,58 +17,44 @@ 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 { Tag } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
const TagItem = React.memo(
({
item,
index,
totalNotes
}: {
item: Tag;
index: number;
totalNotes: number;
}) => {
const { colors } = useThemeColors();
({ item, index }) => {
const { colors, isDark } = useThemeColors();
const onPress = () => {
const {
selectedItemsList,
setSelectedItem,
selectionMode,
clearSelection
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
}
TaggedNotes.navigate(item, true);
};
return (
<SelectionWrapper
<PressableButton
onPress={onPress}
item={item}
selectedColor={colors.secondary.background}
testID={notesnook.ids.tag.get(index)}
alpha={!isDark ? -0.02 : 0.02}
opacity={1}
customStyle={{
paddingHorizontal: 12,
flexDirection: "row",
paddingVertical: 12,
alignItems: "center",
width: "100%",
justifyContent: "space-between"
}}
>
<View
style={{
flexGrow: 1,
flexShrink: 1
maxWidth: "92%"
}}
>
<Heading size={SIZE.md}>
@@ -80,7 +66,7 @@ const TagItem = React.memo(
>
#
</Heading>
{item.title}
{item.alias}
</Heading>
<Paragraph
color={colors.secondary.paragraph}
@@ -89,10 +75,10 @@ const TagItem = React.memo(
marginTop: 5
}}
>
{totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
{item && item.noteIds.length && item.noteIds.length > 1
? item.noteIds.length + " notes"
: item.noteIds.length === 1
? item.noteIds.length + " note"
: null}
</Paragraph>
</View>
@@ -113,11 +99,14 @@ const TagItem = React.memo(
alignItems: "center"
}}
/>
</SelectionWrapper>
</PressableButton>
);
},
(prev, next) => {
if (prev.item?.dateModified !== next.item?.dateModified) {
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
return false;
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Dimensions, View } from "react-native";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useMessageStore } from "../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
@@ -27,15 +27,13 @@ import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Card = ({ color }: { color?: string }) => {
export const Card = ({ color, warning }) => {
const { colors } = useThemeColors();
color = color ? color : colors.primary.accent;
const messageBoardState = useMessageStore((state) => state.message);
const announcements = useMessageStore((state) => state.announcements);
const fontScale = Dimensions.get("window").fontScale;
const announcement = useMessageStore((state) => state.announcement);
return !messageBoardState.visible ||
(announcements && announcements.length) ? null : (
return !messageBoardState.visible || announcement || warning ? null : (
<View
style={{
width: "95%"
@@ -46,6 +44,7 @@ export const Card = ({ color }: { color?: string }) => {
type="gray"
customStyle={{
paddingVertical: 12,
width: "95%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
@@ -55,18 +54,17 @@ export const Card = ({ color }: { color?: string }) => {
<View
style={{
flexDirection: "row",
alignItems: "center",
flexShrink: 1
alignItems: "center"
}}
>
<View
style={{
width: 40 * fontScale,
width: 40,
backgroundColor:
messageBoardState.type === "error"
? hexToRGBA(colors.static.red, 0.15)
: hexToRGBA(color, 0.15),
height: 40 * fontScale,
height: 40,
borderRadius: 100,
alignItems: "center",
justifyContent: "center"
@@ -77,7 +75,6 @@ export const Card = ({ color }: { color?: string }) => {
color={
messageBoardState.type === "error" ? colors.error.icon : color
}
allowFontScaling
name={messageBoardState.icon}
/>
</View>
@@ -94,7 +91,7 @@ export const Card = ({ color }: { color?: string }) => {
</Paragraph>
<Paragraph
style={{
flexWrap: "nowrap",
flexWrap: "wrap",
flexShrink: 1
}}
color={colors.primary.heading}
@@ -104,24 +101,22 @@ export const Card = ({ color }: { color?: string }) => {
</View>
</View>
{fontScale > 1 ? null : (
<View
style={{
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name="chevron-right"
color={
messageBoardState.type === "error" ? colors.error.icon : color
}
size={SIZE.lg}
/>
</View>
)}
<View
style={{
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name="chevron-right"
color={
messageBoardState.type === "error" ? colors.error.icon : color
}
size={SIZE.lg}
/>
</View>
</PressableButton>
</View>
);

View File

@@ -0,0 +1,167 @@
/*
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, useWindowDimensions, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useTip } from "../../services/tip-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const Empty = React.memo(
function Empty({
loading = true,
placeholderData,
headerProps,
type,
screen
}) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholderData.type || type,
screen === "Notes" ? "notes" : null
);
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={
ColorValues[headerProps.color?.toLowerCase()]
? headerProps.color
: "accent"
}
tip={tip || { text: placeholderData.paragraph }}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholderData.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholderData.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholderData.action}
buttonType={{
text:
colors.static[headerProps.color] || colors.primary.accent
}}
style={{
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholderData.heading}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholderData.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={
ColorValues[headerProps.color?.toLowerCase()] ||
colors.primary.accent
}
/>
</View>
</>
)}
</View>
);
},
(prev, next) => {
if (prev.loading === next.loading) return true;
return false;
}
);
/**
* Make a tips manager.
* The tip manager stores many tips. Each tip has following values
* 1. Text
* 2. contexts: An array of context strings. // Places where the tip can be shown
* 3. Button if any.
* 4. Image/Gif asset.
*
* Tip manager adds the following methods -> get(context). Returns a random tip for the following context.
*
* Tips can be shown in a sheet or in a list. For sheets, GeneralSheet can be used to
* render tips.
*
* Where can the tips be shown and how?
* 1. When transitioning, show tips in a sheet. Make sure its useful
* 2. Replace placeholders with tips.
* 3. Show tips in editor placeholder.
* 4. Show tips between list items?
*
* Tooltips.
* Small tooltips can be shown in initial render first time.
* Especially for items that are not shown on blank page. Should be
* in places where it makes sense and does not interrupt the user.
*
* Can also be shown when first time entering a screen that
* has something that the user might not know of. Like sorting and side menu.
*
* Todo:
* 1. Make a tip manager.
* 2. Make a list of tips.
* 3. Add images for those tips.
* 4. Show tips
*/

View File

@@ -1,140 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { TTip, useTip } from "../../services/tip-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { SIZE } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export type PlaceholderData = {
title: string;
paragraph: string;
button?: string;
action?: () => void;
loading?: string;
type?: string;
};
type EmptyListProps = {
loading?: boolean;
placeholder?: PlaceholderData;
title?: string;
color?: string;
dataType: string;
screen?: string;
};
export const Empty = React.memo(function Empty({
loading = true,
placeholder,
title,
color,
dataType,
screen
}: EmptyListProps) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={color}
tip={
screen !== "Search"
? tip || ({ text: placeholder?.paragraph } as TTip)
: ({ text: placeholder?.paragraph } as TTip)
}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
buttonType={{
text: color || colors.primary.accent
}}
style={{
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={color || colors.primary.accent}
/>
</View>
</>
)}
</View>
);
});

View File

@@ -0,0 +1,241 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef } from "react";
import { RefreshControl, View } from "react-native";
import { FlashList } from "@shopify/flash-list";
import Animated, { FadeInDown } from "react-native-reanimated";
import { notesnook } from "../../../e2e/test.ids";
import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { db } from "../../common/database";
import { eScrollEvent } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import TagItem from "../list-items/tag";
import { Empty } from "./empty";
import { getTotalNotes } from "@notesnook/common";
import { useSettingStore } from "../../stores/use-setting-store";
import ReminderItem from "../list-items/reminder";
const renderItems = {
note: NoteWrapper,
notebook: NotebookWrapper,
topic: NotebookWrapper,
tag: TagItem,
section: SectionHeader,
header: SectionHeader,
reminder: ReminderItem
};
const RenderItem = ({ item, index, type, ...restArgs }) => {
if (!item) return <View />;
const Item = renderItems[item.itemType || item.type] || View;
const totalNotes = getTotalNotes(item);
return (
<Item
item={item}
index={index}
type={type}
totalNotes={totalNotes}
{...restArgs}
/>
);
};
/**
*
* @param {any} param0
* @returns
*/
const List = ({
listData,
type,
refreshCallback,
placeholderData,
loading,
headerProps = {
heading: "Home",
color: null
},
screen,
ListHeader,
warning,
isSheet = false,
onMomentumScrollEnd,
handlers,
ScrollComponent
}) => {
const { colors } = useThemeColors();
const scrollRef = useRef();
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
state.settings.notesListMode,
state.settings.notebooksListMode
]);
const isCompactModeEnabled =
(type === "notes" && notesListMode === "compact") ||
type === "notebooks" ||
notebooksListMode === "compact";
const groupType =
screen === "Notes" ? "home" : screen === "Favorites" ? "favorites" : type;
const groupOptions = db.settings?.getGroupOptions(groupType);
const dateBy =
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
const renderItem = React.useCallback(
({ item, index }) => (
<RenderItem
item={item}
index={index}
color={headerProps?.color}
title={headerProps?.heading}
dateBy={dateBy}
type={
screen === "Notes"
? "home"
: screen === "Favorites"
? "favorites"
: type
}
screen={screen}
isSheet={isSheet}
/>
),
[headerProps?.color, headerProps?.heading, screen, type, isSheet, dateBy]
);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
if (refreshCallback) {
refreshCallback();
}
});
};
const _onScroll = React.useCallback(
(event) => {
if (!event) return;
let y = event.nativeEvent.contentOffset.y;
eSendEvent(eScrollEvent, {
y,
screen
});
},
[screen]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
screen
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = ScrollComponent ? ScrollComponent : FlashList;
return (
<>
<Animated.View
style={{
flex: 1
}}
entering={type === "search" ? undefined : FadeInDown}
>
<ListView
{...handlers}
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={listData}
renderItem={renderItem}
onScroll={_onScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
onMomentumScrollEnd?.();
}}
getItemType={(item) => item.itemType || item.type}
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
refreshControl={
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
/>
}
ListEmptyComponent={
placeholderData ? (
<Empty
loading={loading}
placeholderData={placeholderData}
headerProps={headerProps}
type={type}
screen={screen}
/>
) : null
}
ListFooterComponent={<Footer />}
ListHeaderComponent={
<>
{ListHeader ? (
ListHeader
) : !headerProps ? null : (
<Header
title={headerProps.heading}
color={headerProps.color}
type={type}
screen={screen}
warning={warning}
/>
)}
</>
}
/>
</Animated.View>
{listData ? (
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Notes" ? "home" : type}
scrollRef={scrollRef}
/>
) : null}
</>
);
};
export default List;

View File

@@ -1,210 +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 { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useRef } from "react";
import {
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl
} from "react-native";
import Animated, { FadeInDown } from "react-native-reanimated";
import { notesnook } from "../../../e2e/test.ids";
import { useGroupOptions } from "../../hooks/use-group-options";
import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync";
import { RouteName } from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { eScrollEvent } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header";
import { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
type ListProps = {
data: VirtualizedGrouping<Item> | undefined;
dataType: Item["type"];
onRefresh?: () => void;
loading?: boolean;
headerTitle?: string;
customAccentColor?: string;
renderedInRoute?: RouteName;
CustomLisHeader?: React.JSX.Element;
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
id?: string;
};
export default function List(props: ListProps) {
const { colors } = useThemeColors();
const scrollRef = useRef();
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
state.settings.notesListMode,
state.settings.notebooksListMode
]);
const isCompactModeEnabled =
(props.dataType === "note" && notesListMode === "compact") ||
props.dataType === "notebook" ||
notebooksListMode === "compact";
const groupType =
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
props.onRefresh?.();
});
};
const renderItem = React.useCallback(
({ index }: { index: number }) => {
return (
<ListItemWrapper
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
scrollRef={scrollRef}
/>
);
},
[
groupOptions,
groupType,
props.customAccentColor,
props.data,
props.dataType,
props.isRenderedInActionSheet,
props.renderedInRoute
]
);
const onListScroll = React.useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!event) return;
eSendEvent(eScrollEvent, {
y: event.nativeEvent.contentOffset.y,
route: props.renderedInRoute,
id: props.id || props.renderedInRoute
});
},
[props.renderedInRoute, props.id]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
route: props.renderedInRoute,
id: props.id || props.renderedInRoute
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = props.CustomListComponent
? props.CustomListComponent
: FlashList;
return (
<>
<Animated.View
style={{
flex: 1
}}
entering={props.renderedInRoute === "Search" ? undefined : FadeInDown}
>
<ListView
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={props.data?.placeholders || []}
renderItem={renderItem}
onScroll={onListScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
}}
getItemType={(item: number, index: number) => {
return props.data?.type(index);
}}
estimatedItemSize={isCompactModeEnabled ? 60 : 120}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
refreshControl={
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
/>
}
ListEmptyComponent={
props.placeholder ? (
<Empty
loading={props.loading}
title={props.headerTitle}
dataType={props.dataType}
color={props.customAccentColor}
placeholder={props.placeholder}
screen={props.renderedInRoute}
/>
) : null
}
ListFooterComponent={<Footer />}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (
<Header
color={props.customAccentColor}
screen={props.renderedInRoute}
/>
)}
</>
}
/>
</Animated.View>
</>
);
}

View File

@@ -1,277 +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 {
Color,
GroupHeader,
GroupOptions,
GroupingKey,
Item,
ItemType,
Note,
Notebook,
Reminder,
Tag,
TrashItem,
VirtualizedGrouping
} from "@notesnook/core";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager";
import {
NotebooksWithDateEdited,
TagsWithDateEdited,
isNoteResolvedData,
resolveItems
} from "../../stores/resolve-items";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
isSheet: boolean;
index: number;
renderedInRoute?: RouteName;
customAccentColor?: string;
dataType: string;
scrollRef: any;
groupOptions: GroupOptions;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { items, group, isSheet, index, groupOptions } = props;
const [item, setItem] = useState<Item>();
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
const reminder = useRef<Reminder>();
const color = useRef<Color>();
const totalNotes = useRef<number>(0);
const attachmentsCount = useRef(0);
const [groupHeader, setGroupHeader] = useState<GroupHeader>();
const previousIndex = useRef<number>();
const refreshTimeout = useRef<NodeJS.Timeout>();
const currentItemId = useRef<string>();
const refreshItem = useCallback((resolvedItem: any) => {
if (!resolvedItem || !resolvedItem.data) {
tags.current = undefined;
notebooks.current = undefined;
reminder.current = undefined;
color.current = undefined;
attachmentsCount.current = 0;
totalNotes.current = 0;
}
if (resolvedItem && resolvedItem.item) {
const data = resolvedItem.data;
if (resolvedItem.item.type === "note" && isNoteResolvedData(data)) {
tags.current = data.tags;
notebooks.current = data.notebooks;
reminder.current = data.reminder;
color.current = data.color;
attachmentsCount.current = data.attachmentsCount || 0;
} else if (
resolvedItem.item.type === "notebook" &&
typeof data === "number"
) {
totalNotes.current = data;
} else if (resolvedItem.item.type === "tag" && typeof data === "number") {
totalNotes.current = data;
}
currentItemId.current = resolvedItem.item.id;
setItem(resolvedItem.item);
setGroupHeader(resolvedItem.group);
}
}, []);
if (previousIndex.current !== index) {
previousIndex.current = index;
const resolvedItem = items?.cacheItem(index);
refreshItem(resolvedItem);
}
useEffect(() => {
(async function () {
try {
clearTimeout(refreshTimeout.current);
const idx = index;
refreshTimeout.current = setTimeout(async () => {
if (idx !== previousIndex.current) return;
const resolvedItem = await items?.item(idx, resolveItems);
if (idx !== previousIndex.current) return;
refreshItem(resolvedItem);
}, 100);
} catch (e) {
console.log("Error", e);
}
})();
}, [index, items, refreshItem]);
if (!item)
return (
<View
style={{
height: 120,
width: "100%"
}}
/>
);
const type = ((item as TrashItem).itemType || item.type) as ItemType;
switch (type) {
case "note": {
return (
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NoteWrapper
item={item as Note}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
/>
</>
);
}
case "notebook":
return (
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
</>
);
case "reminder":
return (
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<ReminderItem
item={item as Reminder}
index={index}
isSheet={isSheet}
/>
</>
);
case "tag":
return (
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
</>
);
default:
return null;
}
}
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},
item
) || 0
);
}

View File

@@ -17,11 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedDate } from "@notesnook/common";
import { useThemeColors } from "@notesnook/theme";
import KeepAwake from "@sayem314/react-native-keep-awake";
import React, { useEffect, useRef, useState } from "react";
import { SafeAreaView, Text, View } from "react-native";
import { Modal, SafeAreaView, Text, View } from "react-native";
import Animated from "react-native-reanimated";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
@@ -35,7 +33,7 @@ import {
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
@@ -47,7 +45,8 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { getFormattedDate } from "@notesnook/common";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -63,7 +62,7 @@ const MergeConflicts = () => {
const applyChanges = async () => {
let _content = keep;
let note = await db.notes.note(_content.noteId);
let note = db.notes.note(_content.noteId).data;
await db.notes.add({
id: note.id,
conflicted: false,
@@ -89,11 +88,10 @@ const MergeConflicts = () => {
});
}
Navigation.queueRoutesForUpdate();
if (useTabStore.getState().getCurrentNoteId() === note.id) {
if (editorController.current?.note?.id === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
item: editorController.current.note.current[note.id],
...editorController.current?.note,
forced: true
});
}
@@ -102,7 +100,7 @@ const MergeConflicts = () => {
};
const show = async (item) => {
let noteContent = await db.content.get(item.contentId);
let noteContent = await db.content.raw(item.contentId);
content.current = { ...noteContent };
if (__DEV__) {
if (!noteContent.conflicted) {
@@ -247,17 +245,13 @@ const MergeConflicts = () => {
};
return !visible ? null : (
<BaseDialog
<Modal
statusBarTranslucent
transparent={false}
animationType="slide"
animated={false}
bounce={false}
onRequestClose={() => {
close();
}}
centered={false}
background={colors?.primary.background}
supportedOrientations={[
"portrait",
"portrait-upside-down",
@@ -322,16 +316,14 @@ const MergeConflicts = () => {
readonly
editorId=":conflictPrimary"
onLoad={async () => {
const note = await db.notes.note(content.current?.noteId);
const note = db.notes.note(content.current?.noteId)?.data;
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictPrimary", {
item: {
...note,
content: {
...content.current,
isPreview: true
}
...note,
content: {
...content.current,
isPreview: true
}
});
}}
@@ -359,21 +351,19 @@ const MergeConflicts = () => {
readonly
editorId=":conflictSecondary"
onLoad={async () => {
const note = await db.notes.note(content.current?.noteId);
const note = db.notes.note(content.current?.noteId)?.data;
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictSecondary", {
item: {
...note,
content: { ...content.current.conflicted, isPreview: true }
}
...note,
content: { ...content.current.conflicted, isPreview: true }
});
}}
/>
</Animated.View>
</View>
</SafeAreaView>
</BaseDialog>
</Modal>
);
};

View File

@@ -17,29 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import Editor from "../../screens/editor";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import EditorOverlay from "../../screens/editor/loading";
import { editorController } from "../../screens/editor/tiptap/utils";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useEditorStore } from "../../stores/use-editor-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { useTrashStore } from "../../stores/use-trash-store";
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import DialogHeader from "../dialog/dialog-header";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({ session, content, note }) {
const { colors } = useThemeColors();
const editorId = ":noteHistory";
@@ -49,7 +46,7 @@ export default function NotePreview({ session, content, note }) {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
ToastEvent.show({
heading: "Restore successful",
type: "success"
});
@@ -57,11 +54,10 @@ export default function NotePreview({ session, content, note }) {
return;
}
await db.noteHistory.restore(session.id);
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
const note = editorController.current.note.current[session?.noteId];
if (note) {
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
if (editorController.current?.note) {
eSendEvent(eOnLoadNote, {
item: note,
...editorController.current?.note,
forced: true
});
}
@@ -70,7 +66,7 @@ export default function NotePreview({ session, content, note }) {
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastManager.show({
ToastEvent.show({
heading: "Note restored successfully",
type: "success"
});
@@ -85,10 +81,10 @@ export default function NotePreview({ session, content, note }) {
context: "local",
positivePress: async () => {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: "Permanently deleted items",
ToastEvent.show({
heading: "Permanantly deleted items",
type: "success",
context: "local"
});
@@ -121,12 +117,10 @@ export default function NotePreview({ session, content, note }) {
onLoad={async () => {
const _note = note || db.notes.note(session?.noteId)?.data;
eSendEvent(eOnLoadNote + editorId, {
item: {
..._note,
content: {
...content,
isPreview: true
}
..._note,
content: {
...content,
isPreview: true
}
});
}}

View File

@@ -56,7 +56,7 @@ export const CompactFeatures = ({
icon: "shield"
},
{
highlight: "Daily, weekly & monthly",
highlight: "Daily, weekly & montly",
content: "recurring reminders",
icon: "bell"
},

View File

@@ -165,12 +165,7 @@ export const Component = ({ close, promo }) => {
}}
size={SIZE.md}
>
(
{Platform.OS === "android"
? pricing.product?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: pricing.product?.localizedPrice}{" "}
/ mo)
({pricing?.product?.localizedPrice} / mo)
</Paragraph>
)}

View File

@@ -48,23 +48,23 @@ export const Expiring = () => {
const [visible, setVisible] = useState(false);
const [status, setStatus] = useState({
title: "Your trial is ending soon",
offer: "Get 30% off",
offer: null,
extend: true
});
const pricing = usePricing("yearly");
const promo =
status.offer && pricing?.info
? {
promoCode:
pricing?.info?.discount > 30
? pricing.info.sku
: "com.streetwriters.notesnook.sub.yr.trialoffer",
text: `GET ${
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}% OFF on yearly`,
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}
: null;
const promo = status.offer
? {
promoCode:
pricing?.info?.discount > 30
? pricing.info.sku
: "com.streetwriters.notesnook.sub.yr.trialoffer",
text: `GET ${
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}% OFF on yearly`,
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}
: null;
useEffect(() => {
eSubscribeEvent(eOpenTrialEndingDialog, open);

View File

@@ -98,7 +98,7 @@ export const features = [
icon: "bell"
},
{
highlight: "Daily, weekly & monthly",
highlight: "Daily, weekly & montly",
content: "reminders",
icon: "refresh",
pro: true
@@ -194,7 +194,7 @@ export const features = [
"Having the right tool at the right time is crucial for note taking. Lists, tables, codeblocks — you name it, we have it.",
features: [
{
highlight: "Basic formatting",
highlight: "Basic formating",
content: "and lists",
icon: "format-bold"
},

View File

@@ -18,29 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Platform, View } from "react-native";
import { View } from "react-native";
import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import RNIap from "react-native-iap";
export const PricingItem = ({
product,
onPress,
compact,
strikethrough
}: {
product: {
type: "yearly" | "monthly";
data?: RNIap.Subscription;
info: string;
offerType?: "yearly" | "monthly";
};
strikethrough?: boolean;
onPress?: () => void;
compact?: boolean;
}) => {
export const PricingItem = ({ product, onPress, compact }) => {
return (
<PressableButton
onPress={onPress}
@@ -52,10 +36,8 @@ export const PricingItem = ({
paddingHorizontal: 12,
paddingVertical: compact ? 15 : 10,
width: compact ? null : "100%",
minWidth: 150,
opacity: strikethrough ? 0.7 : 1
minWidth: 150
}}
disabled={strikethrough}
>
{!compact && (
<View>
@@ -71,25 +53,8 @@ export const PricingItem = ({
)}
<View>
<Paragraph
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={SIZE.sm}
>
<Heading
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={SIZE.lg - 2}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid | undefined)
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS | undefined)
?.localizedPrice}
</Heading>
<Paragraph size={SIZE.sm}>
<Heading size={SIZE.lg - 2}>{product?.data?.localizedPrice}/</Heading>
{product?.type === "yearly" || product?.offerType === "yearly"
? "/year"
: "/month"}

View File

@@ -20,12 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Platform, Text, View } from "react-native";
import * as RNIap from "react-native-iap";
import { DatabaseLogger, db } from "../../common/database";
import { db } from "../../common/database";
import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -47,29 +47,11 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { useSettingStore } from "../../stores/use-setting-store";
const UUID_PREFIX = "0bdaea";
const UUID_VERSION = "4";
const UUID_VARIANT = "a";
function toUUID(str: string) {
return [
UUID_PREFIX + str.substring(0, 2), // 6 digit prefix + first 2 oid digits
str.substring(2, 6), // # next 4 oid digits
UUID_VERSION + str.substring(6, 9), // # 1 digit version(0x4) + next 3 oid digits
UUID_VARIANT + str.substring(9, 12), // # 1 digit variant(0b101) + 1 zero bit + next 3 oid digits
str.substring(12)
].join("-");
}
const promoCyclesMonthly = {
1: "first month",
2: "first 2 months",
3: "first 3 months",
4: "first 4 months",
5: "first 5 months",
6: "first 3 months"
3: "first 3 months"
};
const promoCyclesYearly = {
@@ -83,24 +65,10 @@ export const PricingPlans = ({
marginTop,
heading = true,
compact = false
}: {
promo?: {
promoCode: string;
};
marginTop?: any;
heading?: boolean;
compact?: boolean;
}) => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
const [product, setProduct] = useState<{
type: string;
offerType: "monthly" | "yearly";
data: RNIap.Subscription;
cycleText: string;
info: string;
}>();
const [product, setProduct] = useState(null);
const [buying, setBuying] = useState(false);
const [loading, setLoading] = useState(false);
const userCanRequestTrial =
@@ -122,40 +90,27 @@ export const PricingPlans = ({
}
}, [promo?.promoCode]);
const getPromo = async (code: string) => {
const getPromo = async (code) => {
try {
let skuId: string;
let productId;
if (code.startsWith("com.streetwriters.notesnook")) {
skuId = code;
productId = code;
} else {
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
productId = await db.offers.getCode(code.split(":")[0], Platform.OS);
}
const products = await PremiumService.getProducts();
const product = products.find((p) => p.productId === skuId);
let products = await PremiumService.getProducts();
let product = products.find((p) => p.productId === productId);
if (!product) return false;
const isMonthly = product.productId.indexOf(".mo") > -1;
const cycleText = isMonthly
let isMonthly = product.productId.indexOf(".mo") > -1;
let cycleText = isMonthly
? promoCyclesMonthly[
(Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid)
.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].billingCycleCount
: parseInt(
(product as RNIap.SubscriptionIOS)
.introductoryPriceNumberOfPeriodsIOS as string
)) as keyof typeof promoCyclesMonthly
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
]
: promoCyclesYearly[
(Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid)
.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].billingCycleCount
: parseInt(
(product as RNIap.SubscriptionIOS)
.introductoryPriceNumberOfPeriodsIOS as string
)) as keyof typeof promoCyclesYearly
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
];
setProduct({
@@ -163,7 +118,7 @@ export const PricingPlans = ({
offerType: isMonthly ? "monthly" : "yearly",
data: product,
cycleText: cycleText,
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
info: "Pay monthly, cancel anytime"
});
return true;
} catch (e) {
@@ -176,41 +131,22 @@ export const PricingPlans = ({
getSkus();
}, [getSkus]);
const buySubscription = async (product: RNIap.Subscription) => {
if (buying || !product) return;
const buySubscription = async (product) => {
if (buying) return;
setBuying(true);
try {
if (!user) {
setBuying(false);
return;
}
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
const androidOfferToken =
Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
.offerToken
: null;
DatabaseLogger.info(
`Subscription Requested initiated for user ${toUUID(user.id)}`
await RNIap.requestSubscription(
product?.productId,
false,
null,
-1,
user.id,
user.id
);
await RNIap.requestSubscription({
sku: product?.productId,
obfuscatedAccountIdAndroid: user.id,
obfuscatedProfileIdAndroid: user.id,
appAccountToken: toUUID(user.id),
andDangerouslyFinishTransactionAutomaticallyIOS: false,
subscriptionOffers: androidOfferToken
? [
{
offerToken: androidOfferToken,
sku: product?.productId
}
]
: undefined
});
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
setBuying(false);
eSendEvent(eCloseSheet);
eSendEvent(eClosePremiumDialog);
@@ -231,35 +167,6 @@ export const PricingPlans = ({
}
};
function getStandardPrice() {
if (!product) return;
const productType = product.offerType;
if (Platform.OS === "android") {
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
if (!pricingPhaseListItem) {
const product =
productType === "monthly"
? monthlyPlan?.product
: yearlyPlan?.product;
return (product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
?.formattedPrice;
}
return pricingPhaseListItem?.formattedPrice;
} else {
const productDefault =
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
return (
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
);
}
}
return loading ? (
<View
style={{
@@ -291,14 +198,7 @@ export const PricingPlans = ({
}}
size={SIZE.lg}
>
{(Platform.OS === "android"
? (monthlyPlan?.product as RNIap.SubscriptionAndroid | undefined)
?.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (monthlyPlan?.product as RNIap.SubscriptionIOS | undefined)
?.localizedPrice) ||
(PremiumService.getMontlySub() as any)?.localizedPrice}
/ mo
{PremiumService.getMontlySub().localizedPrice} / mo
</Paragraph>
<Button
onPress={() => {
@@ -318,7 +218,7 @@ export const PricingPlans = ({
<Button
onPress={async () => {
try {
await db.user?.activateTrial();
await db.user.activateTrial();
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
await sleep(300);
@@ -339,91 +239,33 @@ export const PricingPlans = ({
) : (
<>
{product?.type === "promo" ? (
<View
<Heading
style={{
paddingVertical: 15,
alignItems: "center"
alignSelf: "center",
textAlign: "center"
}}
size={SIZE.lg - 4}
>
{product?.offerType === "monthly" ? (
<PricingItem
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay once a month, cancel anytime."
}}
strikethrough={true}
/>
) : (
<PricingItem
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay once a year, cancel anytime."
}}
strikethrough={true}
/>
)}
<Heading
{product.data.introductoryPrice}
<Paragraph
style={{
paddingTop: 15,
fontSize: SIZE.lg
textDecorationLine: "line-through",
color: colors.secondary.paragraph
}}
size={SIZE.sm}
>
Special offer for you
</Heading>
<View
style={{
paddingVertical: 20,
paddingBottom: 10
}}
>
<Heading
style={{
alignSelf: "center",
textAlign: "center"
}}
size={SIZE.xxl}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS)
?.introductoryPrice ||
(product.data as RNIap.SubscriptionIOS)
?.localizedPrice}{" "}
{product?.cycleText
? `for ${product.cycleText}`
: product?.offerType}
</Heading>
{product?.cycleText ? (
<Paragraph
style={{
color: colors.secondary.paragraph,
alignSelf: "center",
textAlign: "center"
}}
size={SIZE.md}
>
then {getStandardPrice()} {product?.offerType}.
</Paragraph>
) : null}
</View>
</View>
({product.data.localizedPrice})
</Paragraph>{" "}
for {product.cycleText}
</Heading>
) : null}
{user && !product ? (
<>
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
{heading || monthlyPlan?.info?.discount > 0 ? (
<>
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
{monthlyPlan && monthlyPlan?.info?.discount > 0 ? (
<View
style={{
alignSelf: "center",
@@ -431,12 +273,7 @@ export const PricingPlans = ({
marginBottom: 20
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.accent}
>
<Heading color={colors.primary.accent}>
Get {monthlyPlan?.info?.discount}% off in{" "}
{monthlyPlan?.info?.country}
</Heading>
@@ -463,15 +300,12 @@ export const PricingPlans = ({
}}
>
<PricingItem
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
onPress={() => buySubscription(monthlyPlan?.product)}
compact={compact}
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay once a month, cancel anytime."
info: "Pay monthly, cancel anytime."
}}
/>
@@ -485,15 +319,12 @@ export const PricingPlans = ({
)}
<PricingItem
onPress={() => {
if (!yearlyPlan?.product) return;
buySubscription(yearlyPlan?.product);
}}
onPress={() => buySubscription(yearlyPlan?.product)}
compact={compact}
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay once a year, cancel anytime."
info: "Pay yearly"
}}
/>
</View>
@@ -515,9 +346,9 @@ export const PricingPlans = ({
eSendEvent(eCloseSimpleDialog);
setBuying(true);
try {
if (!(await getPromo(value as string)))
if (!(await getPromo(value)))
throw new Error("Error applying promo code");
ToastManager.show({
ToastEvent.show({
heading: "Discount applied!",
type: "success",
context: "local"
@@ -525,9 +356,9 @@ export const PricingPlans = ({
setBuying(false);
} catch (e) {
setBuying(false);
ToastManager.show({
ToastEvent.show({
heading: "Promo code invalid or expired",
message: (e as Error).message,
message: e.message,
type: "error",
context: "local"
});
@@ -565,7 +396,7 @@ export const PricingPlans = ({
width={250}
style={{
paddingHorizontal: 12,
marginTop: product?.type === "promo" ? 0 : 30,
marginTop: 30,
marginBottom: 10
}}
/>
@@ -596,10 +427,7 @@ export const PricingPlans = ({
) : (
<>
<Button
onPress={() => {
if (!product?.data) return;
buySubscription(product.data);
}}
onPress={() => buySubscription(product.data)}
height={40}
width="50%"
type="accent"
@@ -608,7 +436,7 @@ export const PricingPlans = ({
<Button
onPress={() => {
setProduct(undefined);
setProduct(null);
}}
style={{
marginTop: 5
@@ -672,7 +500,7 @@ export const PricingPlans = ({
textAlign: "center"
}}
>
By subscribing, you will be charged on your Google Account, and
By subscribing, your will be charged on your Google Account, and
your subscription will automatically renew until you cancel prior
to the end of the then current period.
</Paragraph>
@@ -695,7 +523,7 @@ export const PricingPlans = ({
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos")
openLinkInBrowser("https://notesnook.com/tos", colors)
.catch(() => {})
.then(() => {});
}}
@@ -710,7 +538,7 @@ export const PricingPlans = ({
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy")
openLinkInBrowser("https://notesnook.com/privacy", colors)
.catch(() => {})
.then(() => {});
}}

View File

@@ -23,11 +23,6 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import Paragraph from "../ui/typography/paragraph";
/**
*
* @param {any} param0
* @returns
*/
export const ProTag = ({ width, size, background }) => {
const { colors } = useThemeColors();

View File

@@ -17,10 +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 { DefaultColors } from "@notesnook/core/dist/collections/colors";
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
@@ -28,56 +25,45 @@ import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { ColorValues } from "../../utils/colors";
import { refreshNotesPage } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import { useThemeColors } from "@notesnook/theme";
export const ColorTags = ({ item }: { item: Note }) => {
export const ColorTags = ({ item }) => {
const { colors } = useThemeColors();
const [note, setNote] = useState(item);
const setColorNotes = useMenuStore((state) => state.setColorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const updater = useRelationStore((state) => state.updater);
const getColorInfo = async (colorCode: string) => {
const dbColor = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", colorCode)])
);
let isLinked = false;
if (dbColor) {
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
if (hasRelation) {
isLinked = true;
}
const changeColor = async (color) => {
if (note.color === color.name) {
await db.notes.note(note.id).uncolor();
} else {
await db.notes.note(note.id).color(color.name);
}
return {
linked: isLinked,
item: dbColor
};
let _note = db.notes.note(note.id).data;
setNote({ ..._note });
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
const color = DefaultColors[name];
const [colorInfo, setColorInfo] = useState<{
linked: boolean;
item: Color | undefined;
}>();
useEffect(() => {
getColorInfo(color).then((info) => setColorInfo(info));
}, [color]);
const _renderColor = (c) => {
const color = {
name: c,
value: ColorValues[c?.toLowerCase()]
};
return (
<PressableButton
type="accent"
accentColor={color}
accentColor={colors.static[color.name?.toLowerCase()]}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(name)}
key={color}
onPress={() => changeColor(name)}
testID={notesnook.ids.dialogs.actionsheet.color(c)}
key={color.value}
onPress={() => changeColor(color)}
customStyle={{
width: 30,
height: 30,
@@ -87,40 +73,13 @@ export const ColorTags = ({ item }: { item: Note }) => {
marginRight: isTablet ? 10 : undefined
}}
>
{colorInfo?.linked ? (
{note.color?.toLowerCase() === color.name ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
const changeColor = async (color: string) => {
const colorInfo = await getColorInfo(DefaultColors[color]);
if (colorInfo.item) {
if (colorInfo.linked) {
await db.relations.unlink(colorInfo.item, item);
} else {
await db.relations.add(colorInfo.item, item);
}
} else {
const colorId = await db.colors.add({
title: color,
colorCode: DefaultColors[color]
});
const dbColor = await db.colors.color(colorId);
if (dbColor) {
await db.relations.add(dbColor as unknown as ItemReference, item);
}
}
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
return (
<View
style={{
@@ -133,9 +92,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
justifyContent: isTablet ? "center" : "space-between"
}}
>
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
return <ColorItem key={name} name={name} />;
})}
{Object.keys(ColorValues).map(_renderColor)}
</View>
);
};

View File

@@ -53,24 +53,23 @@ export const DateMeta = ({ item }) => {
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key) =>
!item[key] ? null : (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}
</Paragraph>
</View>
);
const renderItem = (key) => (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}
</Paragraph>
</View>
);
return (
<View

View File

@@ -16,13 +16,14 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { presentSheet } from "../../services/event-manager";
import SearchService from "../../services/search";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
@@ -33,8 +34,7 @@ import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { TagStrip, Tags } from "./tags";
import { Tags, TagStrip } from "./tags";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
return (
@@ -52,7 +52,9 @@ const Line = ({ top = 6, bottom = 6 }) => {
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
const { colors } = useThemeColors();
const alias = item.alias || item.title;
const isColor = !!ColorValues[item.title];
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
@@ -60,7 +62,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
</Paragraph>
);
}
return (
<FlatList
keyboardShouldPersistTaps="always"
@@ -71,7 +72,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%"
}}
bounces={false}
data={[0]}
keyExtractor={() => "properties-scroll-item"}
renderItem={() => (
@@ -90,13 +90,9 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
#
</Heading>
) : null}
{item.title}
{alias}
</Heading>
{item.type === "notebook" && item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "note" ? (
<TagStrip close={close} item={item} />
) : null}
@@ -121,24 +117,24 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
{item.type === "note" ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
) : null}
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
<Items
item={item}
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
/>
<Synced item={item} close={close} />
{DDS.isTab ? (
@@ -155,7 +151,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
);
};
Properties.present = async (item, buttons = [], isSheet) => {
Properties.present = (item, buttons = [], isSheet) => {
if (!item) return;
let type = item?.type;
let props = [];
@@ -167,7 +163,7 @@ Properties.present = async (item, buttons = [], isSheet) => {
break;
case "note":
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
props[0] = await db.notes.note(item.id);
props[0] = db.notes.note(item.id).data;
props.push([
"notebooks",
"add-reminder",
@@ -187,41 +183,38 @@ Properties.present = async (item, buttons = [], isSheet) => {
"reminders",
"local-only",
"duplicate",
"copy-link",
"references",
...android,
...buttons
]);
break;
case "notebook":
props[0] = await db.notebooks.notebook(item.id);
props[0] = db.notebooks.notebook(item.id).data;
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook",
"add-notebook"
"default-notebook"
]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push([
"move-notes",
"edit-topic",
"add-shortcut",
"trash",
"default-topic"
]);
break;
// case "topic":
// props[0] = db.notebooks
// .notebook(item.notebookId)
// .topics.topic(item.id)._topic;
// props.push([
// "move-notes",
// "edit-topic",
// "add-shortcut",
// "trash",
// "default-topic"
// ]);
// break;
case "tag":
props[0] = await db.tags.tag(item.id);
props[0] = db.tags.tag(item.id);
props.push(["add-shortcut", "trash", "rename-tag"]);
break;
case "reminder": {
props[0] = await db.reminders.reminder(item.id);
props[0] = db.reminders.reminder(item.id);
props.push(["edit-reminder", "trash", "disable-reminder"]);
break;
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Dimensions, FlatList, ScrollView, View } from "react-native";
import { FlatList, ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useActions } from "../../hooks/use-actions";
import { DDS } from "../../services/device-detection";
@@ -28,23 +28,21 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Items = ({ item, buttons, close }) => {
const { colors } = useThemeColors();
const dimensions = useSettingStore((state) => state.dimensions);
const actions = useActions({ item, close });
const data = actions.filter((i) => buttons.indexOf(i.id) > -1 && !i.hidden);
let width = dimensions.width > 600 ? 600 : dimensions.width;
const shouldShrink =
Dimensions.get("window").fontScale > 1 &&
Dimensions.get("window").width < 450;
let columnItemsCount = DDS.isLargeTablet() ? 7 : shouldShrink ? 4 : 5;
let columnItemsCount = DDS.isLargeTablet() ? 7 : 5;
let columnItemWidth = DDS.isTab
? (width - 12) / columnItemsCount
: (width - 12) / columnItemsCount;
const _renderRowItem = ({ item }) => (
<View
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
style={{
@@ -68,7 +66,6 @@ export const Items = ({ item, buttons, close }) => {
}}
>
<Icon
allowFontScaling
name={item.icon}
size={DDS.isTab ? SIZE.xxl : SIZE.lg}
color={
@@ -81,11 +78,7 @@ export const Items = ({ item, buttons, close }) => {
/>
</PressableButton>
<Paragraph
size={SIZE.xs}
textBreakStrategy="simple"
style={{ textAlign: "center" }}
>
<Paragraph size={SIZE.xs} style={{ textAlign: "center" }}>
{item.title}
</Paragraph>
</View>
@@ -116,6 +109,7 @@ export const Items = ({ item, buttons, close }) => {
);
const renderTopBarItem = (item, index) => {
const isLast = index === topBarItems.length;
return (
<PressableButton
onPress={item.func}
@@ -124,6 +118,7 @@ export const Items = ({ item, buttons, close }) => {
activeOpacity={1}
customStyle={{
alignSelf: "flex-start",
marginRight: isLast ? 0 : 10,
paddingHorizontal: 0,
width: topBarItemWidth
}}
@@ -144,7 +139,6 @@ export const Items = ({ item, buttons, close }) => {
>
<Icon
name={item.icon}
allowFontScaling
size={DDS.isTab ? SIZE.xxl : SIZE.md + 4}
color={
item.on
@@ -156,11 +150,7 @@ export const Items = ({ item, buttons, close }) => {
/>
</PressableButton>
<Paragraph
textBreakStrategy="simple"
size={SIZE.xxs + 1}
style={{ textAlign: "center" }}
>
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
{item.title}
</Paragraph>
</PressableButton>
@@ -173,41 +163,16 @@ export const Items = ({ item, buttons, close }) => {
"copy",
"share",
"export",
"lock-unlock"
"lock-unlock",
"publish"
];
const topBarItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) > -1
);
if (!shouldShrink) {
topBarItemsList.push("publish");
}
const topBarItems = data
.filter((item) => topBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
topBarItemsList.indexOf(a.id) > topBarItemsList.indexOf(b.id) ? 1 : -1
);
const bottomBarItemsList = [
"notebooks",
"add-reminder",
"pin-to-notifications",
"duplicate",
"read-only",
"local-only",
"history",
"reminders",
"attachments",
"references",
"copy-link",
"trash"
];
const bottomGridItems = data
.filter((item) => bottomBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
bottomBarItemsList.indexOf(a.id) > bottomBarItemsList.indexOf(b.id)
? 1
: -1
);
const bottomGridItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) === -1
);
const topBarItemWidth =
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
@@ -220,9 +185,6 @@ export const Items = ({ item, buttons, close }) => {
paddingHorizontal: 12,
paddingVertical: 12
}}
contentContainerStyle={{
gap: 10
}}
>
{topBarItems.map(renderTopBarItem)}
</ScrollView>

View File

@@ -17,46 +17,76 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import React from "react";
import { ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eClearEditor } from "../../utils/events";
import Notebook from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useNotebookStore } from "../../stores/use-notebook-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { PressableButton } from "../ui/pressable";
import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
async function getNotebooks() {
let filteredNotebooks = await db.relations.to(note, "notebook").resolve();
return filteredNotebooks || [];
}
const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
});
const notebooks = useNotebookStore((state) => state.notebooks);
function getNotebooks(item) {
let filteredNotebooks = [];
const relations = db.relations.to(note, "notebook");
filteredNotebooks.push(
...relations.map((notebook) => ({
...notebook,
topics: []
}))
);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
const navigateNotebook = async (id) => {
let item = await db.notebooks.notebook(id);
for (let notebookReference of item.notebooks) {
let notebook = {
...(notebooks.find((item) => item.id === notebookReference.id) || {})
};
if (notebook.id) {
notebook.topics = notebook.topics.filter((topic) => {
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
});
const index = filteredNotebooks.findIndex(
(item) => item.id === notebook.id
);
if (index > -1) {
filteredNotebooks[index].topics = notebook.topics;
} else {
filteredNotebooks.push(notebook);
}
}
}
return filteredNotebooks;
}
const noteNotebooks = getNotebooks(note);
const navigateNotebook = (id) => {
let item = db.notebooks.notebook(id)?.data;
if (!item) return;
NotebookScreen.navigate(item, true);
Notebook.navigate(item, true);
};
const navigateTopic = (id, notebookId) => {
let item = db.notebooks.notebook(notebookId)?.topics?.topic(id)?._topic;
if (!item) return;
TopicNotes.navigate(item, true);
};
const renderItem = (item) => (
<PressableButton
<View
key={item.id}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
type={full ? "transparent" : "grayBg"}
customStyle={{
style={{
justifyContent: "flex-start",
paddingHorizontal: 12,
flexDirection: "row",
@@ -64,7 +94,10 @@ export default function Notebooks({ note, close, full }) {
flexShrink: 1,
flexGrow: 1,
padding: 6,
borderWidth: full ? 0 : 1,
borderColor: colors.primary.background,
borderRadius: 10,
backgroundColor: full ? "transparent" : colors.secondary.background,
minHeight: 42
}}
>
@@ -82,10 +115,65 @@ export default function Notebooks({ note, close, full }) {
maxWidth: "50%"
}}
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
>
{item.title}
</Heading>
</PressableButton>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
style={{
flexDirection: "row",
marginLeft: 8,
borderLeftColor: colors.primary.hover,
borderLeftWidth: 1,
paddingLeft: 8
}}
>
{item.topics.map((topic) => (
<Button
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
eSendEvent(eClearEditor);
close();
}}
onLongPress={async () => {
await db.notes.removeFromNotebook(
{
id: item.id,
topic: topic.id
},
note
);
useNotebookStore.getState().setNotebooks();
Navigation.queueRoutesForUpdate();
ToastEvent.show({
heading: "Note removed from topic",
context: "local",
type: "success"
});
}}
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs}
icon="bookmark-outline"
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
))}
<View style={{ width: 10 }} />
</ScrollView>
</View>
);
return noteNotebooks.length === 0 ? null : (
@@ -102,7 +190,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`View all linked notebooks`}
title={`See all linked notebooks`}
fontSize={SIZE.xs}
style={{
alignSelf: "flex-end",

View File

@@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View, useWindowDimensions } from "react-native";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
@@ -33,8 +33,6 @@ export const Synced = ({ item, close }) => {
const user = useUserStore((state) => state.user);
const lastSynced = useUserStore((state) => state.lastSynced);
const dimensions = useWindowDimensions();
const shouldShrink = dimensions.fontScale > 1 && dimensions.width < 450;
return user && lastSynced >= item.dateModified ? (
<View
style={{
@@ -51,11 +49,7 @@ export const Synced = ({ item, close }) => {
borderTopColor: colors.secondary.background
}}
>
<Icon
name="shield-key-outline"
color={colors.primary.accent}
size={shouldShrink ? SIZE.xxl : SIZE.xxxl}
/>
<Icon name="shield-key-outline" color={colors.primary.accent} size={SIZE.xxxl} />
<View
style={{
@@ -73,17 +67,15 @@ export const Synced = ({ item, close }) => {
>
Encrypted and synced
</Heading>
{shouldShrink ? null : (
<Paragraph
style={{
flexWrap: "wrap"
}}
size={SIZE.xs}
color={colors.primary.paragraph}
>
No one can view this {item.itemType || item.type} except you.
</Paragraph>
)}
<Paragraph
style={{
flexWrap: "wrap"
}}
size={SIZE.xs}
color={colors.primary.paragraph}
>
No one can view this {item.itemType || item.type} except you.
</Paragraph>
</View>
<Button
@@ -99,8 +91,8 @@ export const Synced = ({ item, close }) => {
console.error(e);
}
}}
title="Learn more"
fontSize={SIZE.xs}
title="Learn more"
height={30}
type="grayAccent"
/>

View File

@@ -27,7 +27,6 @@ import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -48,7 +47,7 @@ export const Tags = ({ item, close }) => {
>
<Button
onPress={async () => {
ManageTagsSheet.present([item.id]);
ManageTagsSheet.present([item]);
}}
buttonType={{
text: colors.primary.accent

View File

@@ -0,0 +1,70 @@
/*
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 { View } from "react-native";
import { TopicNotes } from "../../screens/notes/topic-notes";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
export const Topics = ({ item, close }) => {
const open = (topic) => {
close();
TopicNotes.navigate(topic, true);
};
const renderItem = (topic) => (
<Button
key={topic.id}
title={topic.title}
type="grayBg"
// buttonType={{
// text: colors.primary.accent
// }}
height={30}
onPress={() => open(topic)}
icon="bookmark-outline"
fontSize={SIZE.xs}
style={{
marginRight: 5,
paddingHorizontal: 8,
borderRadius: 100,
marginVertical: 5
}}
/>
);
return item &&
item.type === "notebook" &&
item.topics &&
item.topics.length > 0 ? (
<View
style={{
flexDirection: "row",
marginTop: 5,
width: "100%",
flexWrap: "wrap"
}}
>
{item.topics
.sort((a, b) => a.dateEdited - b.dateEdited)
.slice(0, 6)
.map(renderItem)}
</View>
) : null;
};

View File

@@ -0,0 +1,380 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect } from "react";
import { BackHandler, Platform, View } from "react-native";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import ManageTagsSheet from "../sheets/manage-tags";
export const SelectionHeader = React.memo(() => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const screen = currentScreen.name;
const insets = useGlobalSafeAreaInsets();
SearchService.prepareSearch?.();
const allItems = SearchService.getSearchInformation()?.get() || [];
const allSelected = allItems.length === selectedItemsList.length;
useEffect(() => {
if (selectionMode) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
}, [selectionMode]);
const addToFavorite = async () => {
if (selectedItemsList.length > 0) {
selectedItemsList.forEach((item) => {
db.notes.note(item.id).favorite();
});
Navigation.queueRoutesForUpdate();
clearSelection();
}
};
const restoreItem = async () => {
if (selectedItemsList.length > 0) {
let noteIds = [];
selectedItemsList.forEach((item) => {
noteIds.push(item.id);
});
await db.trash.restore(...noteIds);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastEvent.show({
heading: "Restore successful",
type: "success"
});
}
};
const deleteItem = async () => {
presentDialog({
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
if (selectedItemsList.length > 0) {
let noteIds = [];
selectedItemsList.forEach((item) => {
noteIds.push(item.id);
});
await db.trash.delete(...noteIds);
Navigation.queueRoutesForUpdate();
clearSelection();
}
},
positiveType: "errorShade"
});
};
const onBackPress = useCallback(() => {
clearSelection();
return true;
}, [clearSelection]);
useEffect(() => {
if (selectionMode) {
BackHandler.addEventListener("hardwareBackPress", onBackPress);
} else {
BackHandler.removeEventListener("hardwareBackPress", onBackPress);
}
}, [onBackPress, selectionMode]);
return !selectionMode ? null : (
<View
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "android" ? insets.top : null,
backgroundColor: colors.primary.background,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
borderRadius: 100
}}
>
<IconButton
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,
width: 40,
borderRadius: 100,
marginRight: 10
}}
type="grayBg"
onPress={() => {
setSelectionMode(!selectionMode);
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
justifyContent: "center",
flexDirection: "row",
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center"
}}
>
<IconButton
onPress={async () => {
useSelectionStore
.getState()
.setAll(allSelected ? [] : [...allItems]);
}}
tooltipText="Select all"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="select-all"
size={SIZE.xl}
/>
{screen === "Trash" ||
screen === "Notebooks" ||
screen === "Notebook" ||
screen === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.primary.icon}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
//setSelectionMode(false);
await sleep(100);
MoveNoteSheet.present();
}}
customStyle={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
</>
)}
{screen === "TopicNotes" || screen === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
const currentScreen =
useNavigationStore.getState().currentScreen;
if (screen === "Notebook") {
for (const item of selectedItemsList) {
await db.relations.unlink(
{ type: "notebook", id: currentScreen.id },
item
);
}
} else {
await db.notes.removeFromNotebook(
{
id: currentScreen.notebookId,
topic: currentScreen.id
},
...selectedItemsList.map((item) => item.id)
);
}
Navigation.queueRoutesForUpdate();
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from ${
screen === "Notebook" ? "notebook" : "topic"
}`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{screen === "Favorites" ? (
<IconButton
onPress={addToFavorite}
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{screen === "Trash" ? null : (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={async () => {
presentDialog({
title: `Delete ${
selectedItemsList.length > 1 ? "items" : "item"
}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1 ? "these items?" : "this item?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: () => {
deleteItems();
},
positiveType: "errorShade"
});
return;
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{screen === "Trash" ? (
<>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
) : null}
</View>
</View>
);
});
SelectionHeader.displayName = "SelectionHeader";
export default SelectionHeader;

View File

@@ -1,354 +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 { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import {
BackHandler,
NativeEventSubscription,
Platform,
View
} from "react-native";
import Animated, { FadeInUp } from "react-native-reanimated";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { updateNotebook } from "../../utils/notebooks";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
export const SelectionHeader = React.memo(
({
items,
type,
id,
renderedInRoute
}: {
items?: VirtualizedGrouping<Item>;
id?: string;
type?: ItemType;
renderedInRoute?: string;
}) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const insets = useGlobalSafeAreaInsets();
const allSelected =
items?.placeholders?.length === selectedItemsList.length;
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
useEffect(() => {
if (selectionMode) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
}, [selectionMode]);
const addToFavorite = async () => {
if (!selectedItemsList.length) return;
db.notes.favorite(true, ...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
};
const restoreItem = async () => {
if (!selectedItemsList.length) return;
await db.trash.restore(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastManager.show({
heading: "Restore successful",
type: "success"
});
};
const deleteItem = async () => {
presentDialog({
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
if (!selectedItemsList.length) return;
await db.trash.delete(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
},
positiveType: "errorShade"
});
};
useEffect(() => {
const onBackPress = () => {
clearSelection();
return true;
};
let sub: NativeEventSubscription | undefined;
if (selectionMode) {
sub = BackHandler.addEventListener("hardwareBackPress", onBackPress);
}
return () => {
sub?.remove();
};
}, [clearSelection, selectionMode]);
return selectionMode !== type || focusedRouteId !== id ? null : (
<Animated.View
entering={FadeInUp}
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "android" ? insets.top : null,
backgroundColor: colors.primary.background,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
borderRadius: 100
}}
>
<IconButton
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,
width: 40,
borderRadius: 100,
marginRight: 10
}}
type="grayBg"
onPress={() => {
clearSelection();
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
justifyContent: "center",
flexDirection: "row",
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center"
}}
>
<IconButton
onPress={async () => {
useSelectionStore
.getState()
.setAll(allSelected ? [] : [...((await items?.ids()) || [])]);
}}
tooltipText="Select all"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={
allSelected ? colors.primary.accent : colors.primary.paragraph
}
name="select-all"
size={SIZE.xl}
/>
{type !== "note" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.primary.icon}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
MoveNoteSheet.present();
}}
customStyle={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
</>
)}
{renderedInRoute === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from Notebook`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{focusedRouteId === "Favorites" ? (
<IconButton
onPress={addToFavorite}
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{type === "trash" ? null : (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={() => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
);
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{type === "trash" ? (
<>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
) : null}
</View>
</Animated.View>
);
}
);
SelectionHeader.displayName = "SelectionHeader";
export default SelectionHeader;

View File

@@ -198,6 +198,7 @@ const SheetProvider = ({ context = "global" }) => {
key={data.actionText}
title={data.actionText}
accentColor={data.iconColor}
accentText="light"
type="accent"
height={40}
width={250}

View File

@@ -0,0 +1,553 @@
/*
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, { createRef } from "react";
import {
Keyboard,
StyleSheet,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
let refs = [];
export class AddNotebookSheet extends React.Component {
constructor(props) {
super(props);
refs = [];
this.state = {
notebook: props.notebook,
topics:
props.notebook?.topics?.map((item) => {
return item.title;
}) || [],
description: null,
titleFocused: false,
descFocused: false,
count: 0,
topicInputFocused: false,
editTopic: false,
loading: false
};
this.title = props.notebook?.title;
this.description = props.notebook?.description;
this.listRef;
this.prevItem = null;
this.prevIndex = null;
this.currentSelectedInput = null;
this.id = props.notebook?.id;
this.backPressCount = 0;
this.currentInputValue = null;
this.titleRef;
this.descriptionRef;
this.topicsToDelete = [];
this.hiddenInput = createRef();
this.topicInputRef = createRef();
this.addingTopic = false;
this.actionSheetRef = props.actionSheetRef;
}
componentWillUnmount() {
refs = [];
}
componentDidMount() {
sleep(300).then(() => {
!this.state.notebook && this.titleRef?.focus();
});
}
close = () => {
refs = [];
this.props.close(true);
};
onDelete = (index) => {
let { topics } = this.state;
let prevTopics = topics;
refs = [];
prevTopics.splice(index, 1);
let edit = this.state.notebook;
if (edit && edit.id) {
let topicToDelete = edit.topics[index];
if (topicToDelete) {
this.topicsToDelete.push(topicToDelete.id);
}
}
let nextTopics = [...prevTopics];
if (this.prevIndex === index) {
this.prevIndex = null;
this.prevItem = null;
this.currentInputValue = null;
this.topicInputRef.current?.setNativeProps({
text: null
});
}
this.setState({
topics: nextTopics
});
};
addNewNotebook = async () => {
this.setState({
loading: true
});
let { topics, notebook } = this.state;
if (!this.title || this.title?.trim().length === 0) {
ToastEvent.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
this.setState({
loading: false
});
return;
}
let toEdit = null;
if (notebook) {
toEdit = db.notebooks.notebook(notebook.id).data;
}
let prevTopics = [...topics];
if (this.currentInputValue && this.currentInputValue.trim().length !== 0) {
if (this.prevItem != null) {
prevTopics[this.prevIndex] = this.currentInputValue;
} else {
prevTopics.push(this.currentInputValue);
this.currentInputValue = null;
}
}
let newNotebookId = null;
if (notebook) {
if (this.topicsToDelete?.length > 0) {
await db.notebooks
.notebook(toEdit.id)
.topics.delete(...this.topicsToDelete);
toEdit = db.notebooks.notebook(toEdit.id).data;
}
await db.notebooks.add({
title: this.title,
description: this.description,
id: notebook.id
});
let nextTopics = toEdit.topics.map((topic, index) => {
let copy = { ...topic };
copy.title = prevTopics[index];
return copy;
});
prevTopics.forEach((title, index) => {
if (!nextTopics[index]) {
nextTopics.push(title);
}
});
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
this.close();
} else {
newNotebookId = await db.notebooks.add({
title: this.title,
description: this.description,
topics: prevTopics,
id: null
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
};
onSubmit = (forward = true) => {
this.hiddenInput.current?.focus();
let willFocus = true;
let { topics } = this.state;
if (!this.currentInputValue || this.currentInputValue?.trim().length === 0)
return;
let prevTopics = [...topics];
if (this.prevItem === null) {
prevTopics.push(this.currentInputValue);
this.setState({
topics: prevTopics
});
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
this.currentInputValue = null;
} else {
prevTopics[this.prevIndex] = this.currentInputValue;
this.setState({
topics: prevTopics
});
this.currentInputValue = null;
if (this.state.editTopic) {
this.topicInputRef.current?.blur();
Keyboard.dismiss();
this.setState({
editTopic: false
});
willFocus = false;
}
this.prevItem = null;
this.prevIndex = null;
this.currentInputValue = null;
if (forward) {
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
}
}
this.topicInputRef.current?.setNativeProps({
text: ""
});
willFocus && this.topicInputRef.current?.focus();
};
renderTopicItem = ({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={this.props.colors}
/>
);
render() {
const { colors } = this.props;
const { topics, topicInputFocused, notebook } = this.state;
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<TextInput
ref={this.hiddenInput}
style={{
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"}
</Heading>
<Button
title="Save"
type="accent"
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24
}}
onPress={this.addNewNotebook}
/>
</View>
<Seperator />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.primary.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused
? colors.primary.accent
: colors.secondary.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={
topics.length === 0 ? null : <View style={{ height: 50 }} />
}
renderItem={this.renderTopicItem}
/>
</View>
);
}
}
AddNotebookSheet.present = (notebook) => {
presentSheet({
component: (ref, close, _update, colors) => (
<AddNotebookSheet
actionSheetRef={ref}
notebook={notebook}
close={close}
colors={colors}
/>
)
});
};
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
const topicRef = (ref) => (refs[index] = ref);
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.secondary.background,
borderRadius: 5,
marginVertical: 5
}}
>
<TouchableOpacity
style={{
width: "80%",
backgroundColor: "transparent",
zIndex: 10,
position: "absolute",
height: 30
}}
onPress={() => {
onPress(item, index);
}}
/>
<TextInput
ref={topicRef}
editable={false}
style={[
styles.topicInput,
{
color: colors.primary.paragraph
}
]}
defaultValue={item}
placeholderTextColor={colors.primary.placeholder}
/>
<View
style={{
width: 80,
position: "absolute",
right: 0,
alignItems: "center",
flexDirection: "row",
justifyContent: "flex-end"
}}
>
<IconButton
onPress={() => {
onPress(item, index);
}}
name="pencil"
size={SIZE.lg - 5}
color={colors.secondary.icon}
/>
<IconButton
onPress={() => {
onDelete(index);
}}
name="minus"
size={SIZE.lg}
color={colors.secondary.icon}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.3)",
justifyContent: "center",
alignItems: "center"
},
overlay: {
width: "100%",
height: "100%",
position: "absolute"
},
headingContainer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
},
headingText: {
marginLeft: 5,
fontSize: SIZE.xl
},
input: {
paddingRight: 12,
paddingHorizontal: 0,
borderRadius: 0,
minHeight: 45,
fontSize: SIZE.md,
padding: pv - 2,
borderBottomWidth: 1,
marginTop: 10,
marginBottom: 5
},
addBtn: {
width: "12%",
minHeight: 45,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
},
buttonContainer: {
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
width: "100%",
marginTop: 20
},
topicContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10
},
topicInput: {
padding: pv - 5,
fontSize: SIZE.sm,
//fontFamily: "sans-serif",
paddingHorizontal: ph,
paddingRight: 40,
paddingVertical: 10,
width: "100%",
maxWidth: "100%"
},
topicBtn: {
borderRadius: 5,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
}
});

View File

@@ -1,189 +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 { Notebook } from "@notesnook/core";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
export const AddNotebookSheet = ({
notebook,
parentNotebook,
close
}: {
notebook?: Notebook;
parentNotebook?: Notebook;
close: ((context?: string | undefined) => void) | undefined;
}) => {
const title = useRef(notebook?.title);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const onSaveChanges = async () => {
if (loading) return;
setLoading(true);
if (!title.current || title?.current.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
setLoading(false);
return;
}
const id = await db.notebooks.add({
title: title.current,
description: description.current,
id: notebook?.id
});
if (parentNotebook) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
const parent =
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
setImmediate(() => {
console.log(parent, notebook.id);
eSendEvent(eOnNotebookUpdated, notebook.id);
});
}
if (!notebook) {
MoveNotes.present(await db.notebooks.notebook(id));
} else {
close?.();
}
};
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook ? "Edit Notebook" : "New Notebook"}
</Heading>
<Button
title={notebook ? "Save" : "Add"}
type="accent"
height={40}
style={{
paddingHorizontal: 24
}}
onPress={onSaveChanges}
/>
</View>
<Seperator />
<Input
fwdRef={titleInput}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
title.current = value;
}}
onLayout={() => {
setImmediate(() => {
titleInput?.current?.focus();
});
}}
placeholder="Enter a title"
onSubmit={() => {
descriptionInput.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : ""}
/>
<Input
fwdRef={descriptionInput}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
description.current = value;
}}
placeholder="Describe your notebook."
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
</View>
);
};
AddNotebookSheet.present = (
notebook?: Notebook,
parentNotebook?: Notebook,
context?: string
) => {
presentSheet({
context: context,
component: (ref, close) => (
<AddNotebookSheet
notebook={notebook}
parentNotebook={parentNotebook}
close={close}
/>
)
});
};

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