Compare commits

..

109 Commits

Author SHA1 Message Date
Ammar Ahmed
dd94d9685c mobile: fix cannot read property 'groupBy' of undefined 2024-05-11 11:09:30 +05:00
Abdullah Atta
5028bb1275 web: update snapshots 2024-05-10 15:30:35 +05:00
Abdullah Atta
27ae5bb5a2 desktop: use npx prebuildify instead of yarn 2024-05-10 15:24:39 +05:00
Abdullah Atta
83316fe299 web: update snapshots for linux 2024-05-10 15:20:57 +05:00
Ammar Ahmed
8869224de2 mobile: fix image preview not rendered on iOS 2024-05-10 15:04:28 +05:00
Ammar Ahmed
c4152f0fc0 mobile: set editor reset time to 10 minutes 2024-05-10 14:53:10 +05:00
Ammar Ahmed
d803005637 mobile: release v3.0.3 2024-05-10 14:47:27 +05:00
Ammar Ahmed
53d896be2b mobile: fix opening exported files after export 2024-05-10 14:44:12 +05:00
Abdullah Atta
8c37c6409a ci: use actions/upload-artifact@v4 2024-05-10 12:36:56 +05:00
Abdullah Atta
9195244890 web: fix tests 2024-05-10 12:31:55 +05:00
Ammar Ahmed
152794ccd0 mobile: fix image filename in picker 2024-05-10 12:24:40 +05:00
Ammar Ahmed
2e8b234b63 mobile: fix editor content update on realtime sync 2024-05-10 12:24:40 +05:00
Abdullah Atta
055c647698 web: add Audios tab in attachments dialog 2024-05-10 12:08:38 +05:00
Abdullah Atta
2b7bcfe7c6 web: bump version to 3.0.3 2024-05-10 12:00:06 +05:00
Abdullah Atta
3c8b8a7bc9 web: fix real time sync test 2024-05-10 11:58:56 +05:00
Abdullah Atta
fde45fb6de web: saving timeout notice shown every time 2024-05-10 11:58:44 +05:00
Abdullah Atta
4a616a3e67 web: fix tab not updating when note is locked/unlocked 2024-05-10 11:27:26 +05:00
Abdullah Atta
bd0a338dd6 core: fix dateEdited not updating on saving locked content 2024-05-10 10:46:11 +05:00
Abdullah Atta
5ab721e3e2 core: fix async iterator on sql collection only returning dateCreated & id 2024-05-10 10:22:03 +05:00
Ammar Ahmed
cb70bb9429 mobile: fix errors 2024-05-10 09:22:23 +05:00
Abdullah Atta
82cadd473c web: show error if saving takes more than 30 seconds 2024-05-10 09:22:23 +05:00
Ammar Ahmed
0e8ea3ddeb mobile: fix editor loading 2024-05-10 09:22:23 +05:00
Ammar Ahmed
511acc5fca mobile: handle editor not responding 2024-05-10 09:22:23 +05:00
Ammar Ahmed
001cf8ee92 mobile: add editor lock safety 2024-05-10 09:22:23 +05:00
Muhammad Ali
a965cd694b web: fix color not updating on note (#5573) 2024-05-10 09:16:59 +05:00
Ammar Ahmed
d3c1c1959b mobile: fix wording of push/pull sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9a866a552a web: replace force sync with force push/pull 2024-05-09 17:39:35 +05:00
Ammar Ahmed
837c296821 mobile: push/pull force sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9aed6faed8 common: fix locked notes not getting exported 2024-05-09 17:39:16 +05:00
Abdullah Atta
734f491e8d editor: fix crash due to undefined is not an object (evaluating 'a.nodeSize') (#5553) 2024-05-09 17:38:18 +05:00
Ammar Ahmed
0ea13024e9 mobile: fix editor crash due to position (#5519)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-09 15:55:33 +05:00
Abdullah Atta
cb27039245 web: close shared service on window close 2024-05-09 10:30:46 +05:00
Abdullah Atta
714ac2deac core: do not use auth access tokens as normal access tokens 2024-05-09 10:30:25 +05:00
Abdullah Atta
d605bc29bf desktop: fix crash on toggling custom dns (#5538) 2024-05-09 10:15:14 +05:00
Abdullah Atta
080101bc45 core: fix database disk image malformed error (#5525) 2024-05-09 10:08:44 +05:00
Abdullah Atta
4819e17da3 core: rerun sync if there are unsynced changes after sync complete 2024-05-08 16:48:59 +05:00
Ammar Ahmed
69a5d5a1b6 global: update .gitignore 2024-05-08 10:43:35 +05:00
Ammar Ahmed
68a6d6c313 mobile: fix crash when taking camera picture 2024-05-08 09:48:28 +05:00
Ammar Ahmed
c51ef4d5f6 mobile: fix camera permission 2024-05-08 09:48:28 +05:00
Ammar Ahmed
d9c5c1f893 mobile: remove margin in editor 2024-05-08 09:47:56 +05:00
Ammar Ahmed
5a75ca73a9 mobile: fix editor scrolling on update 2024-05-08 09:47:56 +05:00
Abdullah Atta
626d393376 desktop: use yarn instead of npx for build scripts 2024-05-08 09:14:55 +05:00
Abdullah Atta
68d06ff3f8 core: fix legacy backup restoration 2024-05-07 15:57:22 +05:00
Abdullah Atta
79f20d100c core: fix File already exists error on duplicate downloads 2024-05-07 12:57:51 +05:00
Ammar Ahmed
9d5bec3bfb ci: upload sourcemaps 2024-05-07 11:49:58 +05:00
Abdullah Atta
ac96354b4c core: handle case when updating item while a push is ongoing 2024-05-06 22:04:04 +05:00
Abdullah Atta
63f44d6fbc core: set synced to false on removing content by note id 2024-05-06 15:45:45 +05:00
Abdullah Atta
f9bfa88c83 core: set synced to false on unlinking relations 2024-05-06 15:45:45 +05:00
Ammar Ahmed
ef192f3d22 mobile: release v3.0.2 2024-05-05 01:14:20 +05:00
Abdullah Atta
c9a8134265 core: fix notes & notebooks sorting 2024-05-05 00:00:49 +05:00
Ammar Ahmed
cd7a5d2ec0 mobile: fix error not rendered correctly 2024-05-04 23:52:47 +05:00
Abdullah Atta
b92bc03fa9 web: handle errors when saving attachment 2024-05-04 23:51:42 +05:00
Abdullah Atta
5f5e472800 web: bump version to 3.0.2 2024-05-04 23:35:49 +05:00
Ammar Ahmed
0951906cac mobile: do not check for .nnbackup file in zip 2024-05-04 23:29:59 +05:00
Abdullah Atta
6f0fe03760 core: do not throw if user not found during legacy backup 2024-05-04 23:28:37 +05:00
Muhammad Ali
159ce97e42 core: fix crash due to unsanitized sort options (#5324)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-05-04 23:26:58 +05:00
Ammar Ahmed
8a9848634c mobile: fix note not opening from notifications 2024-05-04 23:26:07 +05:00
Ammar Ahmed
282705a683 mobile: add logging 2024-05-04 23:11:30 +05:00
Ammar Ahmed
c91b706ed0 mobile: correctly show mime type 2024-05-04 23:11:30 +05:00
Ammar Ahmed
16afc07b2f mobile: fix all attachments show in audios 2024-05-04 23:11:30 +05:00
Ammar Ahmed
04f8be5a62 core: add missing audios attachments filter 2024-05-04 23:11:30 +05:00
Ammar Ahmed
0e8a5ca1fa editor: fix n.startsWith is not a function 2024-05-04 23:03:54 +05:00
Ammar Ahmed
1491bf771e Fix image and file uploads on mobile (#5304)
* mobile: fix image file formats support

* mobile: fix image and file uploads

---------

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-04 23:02:07 +05:00
Abdullah Atta
0f5ce465c3 web: add logging related to attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
647ff7ae0e web: prevent overwriting uploaded attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
45a552adef web: handle errors during uploaded file size check 2024-05-04 22:51:01 +05:00
Abdullah Atta
8e207d787d web: delete file before reuploading 2024-05-04 22:51:01 +05:00
Abdullah Atta
00754593ac web: fix attachment not getting marked as failed on error 2024-05-04 22:51:01 +05:00
Abdullah Atta
b22770f1ae core: do not reset attachment dateUploaded when marking as failed 2024-05-04 22:51:01 +05:00
Abdullah Atta
523da4e91f core: only delete attachment file (not the metadata) on sync conflict 2024-05-04 22:51:01 +05:00
Abdullah Atta
de6c8154a3 web: fix password recovery not working 2024-05-04 22:50:44 +05:00
Abdullah Atta
9ef32e8081 web: do not hide attachment manager for logged out user 2024-05-01 20:14:20 +05:00
Abdullah Atta
5f53593a26 web: do not throw if backup doesn't contain .nnbackup file 2024-05-01 20:12:23 +05:00
Abdullah Atta
1f404944d9 web: re-enable service worker 2024-05-01 16:38:09 +05:00
Ammar Ahmed
23dca36475 mobile: release v3.0.1 2024-05-01 16:34:01 +05:00
Ammar Ahmed
3dd8e20e9a mobile: add logging when loading attachments 2024-05-01 16:29:37 +05:00
Abdullah Atta
8f412f9487 editor: fix padding of react node views on mobile 2024-05-01 16:29:12 +05:00
Abdullah Atta
bf790219db web: bump version to 3.0.1 2024-05-01 16:16:08 +05:00
Ammar Ahmed
c6af310b2d mobile: add notebook search 2024-05-01 16:10:26 +05:00
Abdullah Atta
8d89b5dca9 web: bring back notebook search in move note dialog 2024-05-01 16:10:26 +05:00
Abdullah Atta
fa35244607 editor: update test snapshosts 2024-05-01 16:09:06 +05:00
Abdullah Atta
d96815fa36 ci: remove beta publish workflows 2024-05-01 15:23:43 +05:00
Abdullah Atta
4a9f5b1d8b core: remove .only from trash tests 2024-05-01 15:19:57 +05:00
Ammar Ahmed
43fac79011 mobile: fix moving notebook on ios 2024-05-01 15:18:55 +05:00
Ammar Ahmed
f2c62c8d8b mobile: fix moving notebook on ios 2024-05-01 15:18:55 +05:00
Ammar Ahmed
70c9dd0083 mobile: reduce font size in notebook header 2024-05-01 15:15:37 +05:00
Ammar Ahmed
ae6d8c5f2b mobile: hide breadcrumbs in top level notebooks 2024-05-01 15:15:37 +05:00
Abdullah Atta
38e2250fd1 web: log attachment on failure 2024-05-01 14:55:49 +05:00
Ammar Ahmed
17e2c5a8a2 desktop: fix app icon size for macos 2024-05-01 14:53:32 +05:00
Ammar Ahmed
2e226f0f72 mobile: fix cannot read property 'sortDirection' of undefined 2024-05-01 13:34:48 +05:00
Ammar Ahmed
167c54578b mobile: fix scrolling broken in reminder -> repeat -> month 2024-05-01 13:34:37 +05:00
Abdullah Atta
fb656af5ca web: sort tags by title in editor header 2024-05-01 13:34:20 +05:00
Ammar Ahmed
ddfab272d2 common: sort tags by title in resolved items 2024-05-01 13:34:20 +05:00
Ammar Ahmed
28958311c1 mobile: fix share extention crash on sharing from some apps 2024-05-01 13:34:06 +05:00
Ammar Ahmed
f1b8275b4b mobile: add support for setting note title form quick note widget 2024-05-01 13:34:06 +05:00
Ammar Ahmed
c8c75d40dd mobile: fix theme search returns empty results 2024-05-01 13:33:50 +05:00
Ammar Ahmed
bace90808d mobile: fix footer size for notebook screen 2024-05-01 13:33:31 +05:00
Ammar Ahmed
fe7c7e8db4 mobile: fix notebook-sheet position 2024-05-01 13:32:02 +05:00
Ammar Ahmed
519a569366 mobile: fix notebook sheet flicker 2024-05-01 13:32:02 +05:00
Ammar Ahmed
89d602a40f mobile: do not lock app when requesting permission 2024-05-01 13:31:38 +05:00
Abdullah Atta
a8db65b5af editor: fix performance issues due to using portals for react node views 2024-05-01 13:29:55 +05:00
Ammar Ahmed
716239abfd mobile: fix selection header 2024-05-01 13:21:31 +05:00
Ammar Ahmed
62dd5a5416 mobile: fix vault unlock with biometrics 2024-05-01 13:19:00 +05:00
Ammar Ahmed
96558c5df5 mobile: fix search bar position on ios 2024-05-01 13:18:21 +05:00
Ammar Ahmed
45997dd2e4 editor: fix scroll into view should bring heading to top 2024-05-01 13:16:54 +05:00
Ammar Ahmed
bde190f01c mobile: render settings icon if no profile pic is set 2024-05-01 13:15:24 +05:00
Ammar Ahmed
042c97688f mobile: add quick theme switch button 2024-05-01 13:15:24 +05:00
Ammar Ahmed
95b5114070 mobile: fix confusing settings/sync button 2024-05-01 13:15:24 +05:00
Ammar Ahmed
20742e2a1c mobile: fix back navigation from editor (#4867)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-01 13:13:44 +05:00
172 changed files with 3170 additions and 2199 deletions

View File

@@ -147,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -133,7 +133,6 @@ jobs:
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -148,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -98,7 +98,10 @@ jobs:
api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Notesnook.ipa
path: Notesnook.ipa
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -1,69 +0,0 @@
name: Publish @notesnook/web Beta
on:
workflow_dispatch:
push:
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
pull_request:
types:
- closed
- ready_for_review
- reopened
- opened
- synchronize
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate beta build
run: npm run build:beta:web
- name: Publish to Cloudflare Pages
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
id: pages
with:
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-project-name: notesnook-beta-app
directory: ./apps/web/build
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}

View File

@@ -40,7 +40,7 @@ jobs:
run: npm run build:test:web
- name: Archive build artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
@@ -56,7 +56,7 @@ jobs:
uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
@@ -88,7 +88,7 @@ jobs:
working-directory: apps/web
- name: Upload test results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.shard }}

View File

@@ -1,32 +0,0 @@
name: Publish @notesnook/web v3 Beta
on:
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Build
run: npm run build:web
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-v3-beta ./apps/web/build/

Binary file not shown.

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.0.0",
"version": "3.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.0.0",
"version": "3.0.3",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.0",
"version": "3.0.3",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",

View File

@@ -38,7 +38,7 @@ await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(
"npx nx build:desktop @notesnook/web",
"yarn nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
}
@@ -58,15 +58,15 @@ await fs.cp(path.join(webAppPath, "build"), "build", {
});
if (args.variant === "mas") {
await exec(`npm run bundle:mas`);
await exec(`yarn run bundle:mas`);
} else {
await exec(`npm run bundle`);
await exec(`yarn run bundle`);
}
await exec(`npx tsc`);
await exec(`yarn tsc`);
if (args.run) {
await exec(`npx electron-builder --dir --x64`);
await exec(`yarn electron-builder --dir --x64`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {

View File

@@ -53,11 +53,11 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npx electron-builder install-app-deps");
await exec("yarn electron-builder install-app-deps");
}
await exec(`npm run bundle`);
execAsync(`npx`, [`tsc`]);
await exec(`yarn run bundle`);
execAsync(`yarn`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -66,7 +66,7 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["npx", "nx", "start:desktop", "@notesnook/web"],
["yarn", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
@@ -78,7 +78,7 @@ async function onChange(first) {
}
execAsync(
"npx",
"yarn",
["electron", path.join("build", "electron.js")],
true,
cleanup

View File

@@ -58,11 +58,11 @@ export const osIntegrationRouter = t.router({
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean())
.input(z.boolean().optional())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = customDns;
config.customDns = !!customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules),

View File

@@ -218,12 +218,7 @@ export default async function downloadAttachment(
}
if (options.base64 || options.text) {
console.log(
"starting decrypt base64 file...",
options.base64,
options.text,
attachment.hash
);
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
@@ -292,7 +287,7 @@ export default async function downloadAttachment(
return fileUri;
} catch (e) {
console.log("download attachment error: ", e);
DatabaseLogger.error(e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)

View File

@@ -22,21 +22,24 @@ import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { DatabaseLogger, db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) {
if (!data) return false;
if (!data) {
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
return false;
}
console.log("Downloading", filename);
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
console.log("Exists already", filename);
DatabaseLogger.log(`File Exists already: ${filename}`);
return true;
}
@@ -44,13 +47,24 @@ export async function downloadFile(filename, data, cancelToken) {
method: "GET",
headers
});
if (!res.ok)
if (!res.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
);
throw new Error(`${res.status}: Unable to resolve download url`);
}
const downloadUrl = await res.text();
if (!downloadUrl) throw new Error("Unable to resolve download url");
if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error("Unable to resolve download url");
}
let totalSize = 0;
console.log("Download starting");
DatabaseLogger.log(`Download starting: ${filename}`);
let request = RNFetchBlob.config({
path: path,
IOSBackgroundTask: true
@@ -61,13 +75,15 @@ export async function downloadFile(filename, data, cancelToken) {
.getState()
.setProgress(0, total, filename, recieved, "download");
totalSize = total;
console.log("downloading: ", recieved, total);
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
DatabaseLogger.log(`Download cancelled: ${filename}`);
};
let response = await request;
await fileCheck(response, totalSize);
let status = response.info().status;
@@ -91,7 +107,10 @@ export async function downloadFile(filename, data, cancelToken) {
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("Download file error:", e, url, headers);
DatabaseLogger.error(e, {
url,
headers
});
return false;
}
}
@@ -100,18 +119,17 @@ export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
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;
DatabaseLogger.error(e);
return -1;
}
}
@@ -125,7 +143,9 @@ export async function checkAttachment(hash) {
try {
const size = await getUploadedFileSize(hash);
if (size <= 0) return { failed: "File length is 0." };
if (size === -1) return { success: true };
if (size === 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e?.message };
}

View File

@@ -21,12 +21,12 @@ import Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { IOS_APPGROUPID } from "../../utils/constants";
import { db } from "../database";
import { DatabaseLogger, db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
console.log("Read encrypted file...");
DatabaseLogger.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
@@ -34,8 +34,6 @@ export async function readEncrypted(filename, key, cipherData) {
return false;
}
const attachment = await db.attachments.attachment(filename);
console.log("decrypting....");
let output = await Sodium.decryptFile(
key,
{
@@ -45,12 +43,13 @@ export async function readEncrypted(filename, key, cipherData) {
},
cipherData.outputType === "base64" ? "base64" : "text"
);
console.log("file decrypted...", attachment?.mimeType);
DatabaseLogger.log("File decrypted...");
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("readEncrypted", e);
DatabaseLogger.error(e);
return false;
}
}
@@ -128,7 +127,7 @@ export async function clearFileStorage() {
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
console.log("Cache directory created");
DatabaseLogger.log("Cache directory created");
}
}
@@ -188,6 +187,9 @@ export async function exists(filename) {
);
if (stat.size !== expectedFileSize) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log);

View File

@@ -25,6 +25,7 @@ import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
import { getUploadedFileSize } from "./download";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -33,6 +34,18 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
DatabaseLogger.log("Upload verification failed.");
return false;
}
const isUploaded = uploadedFileSize !== 0;
if (isUploaded) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
method: "PUT",
headers
@@ -50,7 +63,15 @@ export async function uploadFile(filename, data, cancelToken) {
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({

View File

@@ -20,15 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Linking, View } from "react-native";
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans";
import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions";
@@ -56,13 +54,6 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
/>
)
});
} else if (item.type === "force-sync") {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
eSendEvent(eCloseSheet);
});
}
};
return (

View File

@@ -56,6 +56,7 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
const Actions = ({
attachment,
@@ -218,6 +219,7 @@ const Actions = ({
}}
>
<Dialog context={contextId} />
<SheetProvider context={contextId} />
<View
style={{
borderBottomWidth: 1,
@@ -238,36 +240,20 @@ const Actions = ({
style={{
flexDirection: "row",
marginBottom: 10,
paddingHorizontal: 12
paddingHorizontal: 12,
marginTop: 6,
gap: 10
}}
>
<Paragraph
size={SIZE.xs}
style={{
marginRight: 10
}}
color={colors.secondary.paragraph}
>
{attachment.type}
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{attachment.mimeType}
</Paragraph>
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{formatBytes(attachment.size)}
</Paragraph>
{notes.length ? (
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{notes.length} note
{notes.length > 1 ? "s" : ""}
</Paragraph>
@@ -366,7 +352,7 @@ const Actions = ({
{failed ? (
<Notice
type="alert"
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
size="small"
/>
) : null}

View File

@@ -177,7 +177,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
case "audio":
items = note
? db.attachments.ofNote(note.id, "audio")
: db.attachments.all;
: db.attachments.audios;
break;
case "documents":
items = note

View File

@@ -67,7 +67,7 @@ export const Login = ({ changeMode }) => {
Progress.present();
setTimeout(() => {
if (!useUserStore.getState().syncing) {
Sync.run("global", false, true);
Sync.run("global", false, "full");
}
}, 5000);
});

View File

@@ -112,7 +112,7 @@ export const SessionExpired = () => {
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
Sync.run("global", false, "full", async (complete) => {
if (!complete) {
let user = await db.user.getUser();
if (!user) return;

View File

@@ -24,7 +24,7 @@ import {
setAppLockVerificationCipher,
validateAppLockPassword
} from "../../../common/database/encryption";
import BiometicService from "../../../services/biometrics";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
@@ -323,7 +323,7 @@ export const AppLockPassword = () => {
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometicService.isBiometryAvailable()) ||
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);

View File

@@ -17,8 +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 React, { useState } from "react";
import { Image, ScrollView, TouchableOpacity, View } from "react-native";
import { ImagePickerResponse } from "react-native-image-picker";
import {
Image,
Platform,
ScrollView,
TouchableOpacity,
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
@@ -32,7 +38,7 @@ export default function AttachImage({
onAttach,
close
}: {
response: ImagePickerResponse;
response: ImageType[];
onAttach: ({ compress }: { compress: boolean }) => void;
close: ((ctx?: string | undefined) => void) | undefined;
}) {
@@ -58,14 +64,17 @@ export default function AttachImage({
}}
>
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
Attaching {response.assets?.length} image(s):
Attaching {response?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response.assets?.map((item) => (
<TouchableOpacity key={item.fileName} activeOpacity={0.9}>
{response?.map?.((item) => (
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
<Image
source={{
uri: item.uri
uri:
Platform.OS === "ios"
? item.sourceURL || item.path
: item.path
}}
style={{
width: 100,
@@ -142,7 +151,7 @@ export default function AttachImage({
<Button
title={`${
(response.assets?.length || 0) > 1 ? "Attach Images" : "Attach Image"
(response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`}
type="accent"
width="100%"
@@ -156,10 +165,16 @@ export default function AttachImage({
);
}
AttachImage.present = (response: ImagePickerResponse) => {
return new Promise((resolve) => {
AttachImage.present = (response: ImageType[], context?: string) => {
return new Promise<
| {
compress: boolean;
}
| undefined
>((resolve) => {
let resolved = false;
presentSheet({
context: context,
component: (ref, close, update) => (
<AttachImage
response={response}

View File

@@ -128,7 +128,7 @@ export class VaultDialog extends Component {
: this.state.copyNote
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.goToEditor
? "Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault."
? "Unlock note to open it in editor."
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
}

View File

@@ -33,6 +33,7 @@ import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
import { useNavigation } from "@react-navigation/native";
type HeaderRightButton = {
title: string;
@@ -66,6 +67,8 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const navigation = useNavigation();
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true);

View File

@@ -19,10 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium";
import dataurl from "@notesnook/core/dist/utils/dataurl";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
import ImageViewer from "react-native-image-zoom-viewer";
import { db } from "../../common/database";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { cacheDir } from "../../common/filesystem/utils";
import {
@@ -32,13 +34,13 @@ 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 [loading, setLoading] = useState(false);
const imageRef = useRef<ImageAttributes>();
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
@@ -48,6 +50,7 @@ const ImagePreview = () => {
}, []);
const open = async (image: ImageAttributes) => {
imageRef.current = image;
setVisible(true);
setLoading(true);
setTimeout(async () => {
@@ -60,6 +63,9 @@ const ImagePreview = () => {
type: "base64",
uri: ""
});
if (imageRef.current) {
imageRef.current.hash = hash;
}
}
if (!hash) return;
//@ts-ignore // FIX ME
@@ -80,7 +86,13 @@ const ImagePreview = () => {
return (
visible && (
<BaseDialog animation="slide" visible={true} onRequestClose={close}>
<BaseDialog
background="black"
animation="slide"
visible={true}
onRequestClose={close}
transparent
>
<View
style={{
width: "100%",
@@ -101,6 +113,21 @@ const ImagePreview = () => {
color={colors.primary.accent}
borderColor="transparent"
/>
<IconButton
onPress={() => {
if (imageRef.current?.hash) {
db.fs().cancel(imageRef.current?.hash);
}
close();
}}
style={{
position: "absolute",
top: Platform.OS === "android" ? 35 : 0,
right: 12
}}
color={colors.static.white}
name="close"
/>
</View>
) : (
<ImageViewer
@@ -117,13 +144,12 @@ const ImagePreview = () => {
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 80,
marginTop: 0,
paddingHorizontal: 12,
height: 50,
paddingHorizontal: 24,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
paddingTop: Platform.OS === "android" ? 30 : 0
marginTop: Platform.OS === "android" ? 30 : 0
}}
>
<IconButton
@@ -137,7 +163,7 @@ const ImagePreview = () => {
)}
imageUrls={[
{
url: image
url: image as string
}
]}
/>

View File

@@ -19,6 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { View } from "react-native";
import React from "react";
export const Footer = () => {
return <View style={{ height: 150 }} />;
export const Footer = ({ height = 150 }) => {
return <View style={{ height: height }} />;
};

View File

@@ -95,7 +95,7 @@ export const NotebookHeader = ({
style={{
flexShrink: 1
}}
size={SIZE.xxl}
size={SIZE.lg}
>
{notebook.title}
</Heading>
@@ -114,7 +114,7 @@ export const NotebookHeader = ({
width: 40,
height: 40
}}
type={isPinnedToMenu ? "secondary" : "secondary"}
type="transparent"
color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg}
/>
@@ -123,7 +123,7 @@ export const NotebookHeader = ({
onPress={onEditNotebook}
tooltipText="Edit this notebook"
name="pencil"
type="secondary"
type="transparent"
color={colors.primary.icon}
style={{
width: 40,
@@ -134,7 +134,7 @@ export const NotebookHeader = ({
</View>
{notebook.description ? (
<Paragraph size={SIZE.md} color={colors.primary.paragraph}>
<Paragraph size={SIZE.sm} color={colors.primary.paragraph}>
{notebook.description}
</Paragraph>
) : null}

View File

@@ -80,7 +80,7 @@ export default function List(props: ListProps) {
const groupOptions = useGroupOptions(groupType);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
Sync.run("global", false, "full", () => {
props.onRefresh?.();
});
};
@@ -192,7 +192,9 @@ export default function List(props: ListProps) {
/>
) : null
}
ListFooterComponent={<Footer />}
ListFooterComponent={
<Footer height={props.renderedInRoute === "Notebook" ? 300 : 150} />
}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (

View File

@@ -19,14 +19,14 @@ 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 React, { useEffect, useRef } from "react";
import {
BackHandler,
NativeEventSubscription,
Platform,
View
} from "react-native";
import Animated, { FadeInUp } from "react-native-reanimated";
import Menu from "react-native-reanimated-material-menu/src/Menu";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
@@ -42,9 +42,10 @@ import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags";
import { MoveNotebookSheet } from "../sheets/move-notebook";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import { MoveNotebookSheet } from "../sheets/move-notebook";
export const SelectionHeader = React.memo(
({
@@ -58,6 +59,8 @@ export const SelectionHeader = React.memo(
type?: ItemType;
renderedInRoute?: string;
}) => {
const menuRef = useRef<Menu>(null);
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
@@ -132,8 +135,7 @@ export const SelectionHeader = React.memo(
}, [clearSelection, selectionMode]);
return selectionMode !== type || focusedRouteId !== id ? null : (
<Animated.View
entering={FadeInUp}
<View
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
@@ -143,8 +145,7 @@ export const SelectionHeader = React.memo(
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
paddingHorizontal: 12
}}
>
<View
@@ -164,18 +165,15 @@ export const SelectionHeader = React.memo(
borderRadius: 100,
marginRight: 10
}}
type="secondary"
onPress={() => {
clearSelection();
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
@@ -184,11 +182,12 @@ export const SelectionHeader = React.memo(
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
<Heading size={SIZE.lg} color={colors.primary.paragraph}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
@@ -211,159 +210,142 @@ export const SelectionHeader = React.memo(
allSelected ? colors.primary.accent : colors.primary.paragraph
}
name="select-all"
size={SIZE.xl}
/>
{renderedInRoute === "Notebooks" && selectedItemsList.length ? (
<IconButton
{selectedItemsList.length ? (
<Menu
ref={menuRef}
animationDuration={200}
style={{
marginLeft: 10
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: -20
}}
onPress={async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
MoveNotebookSheet.present(notebooks);
onRequestClose={() => {
menuRef.current?.hide();
}}
tooltipText="Move notebooks"
tooltipPosition={1}
name="arrow-right-bold-box-outline"
size={SIZE.xl}
/>
) : null}
{type !== "note" || !selectedItemsList.length ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
style={{
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}
style={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
MoveNoteSheet.present();
}}
style={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
</>
)}
{renderedInRoute === "Notebook" && selectedItemsList.length ? (
<IconButton
onPress={async () => {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
}}
style={{
marginLeft: 10
}}
tooltipText={`Remove from Notebook`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{focusedRouteId === "Favorites" && selectedItemsList.length ? (
<IconButton
onPress={addToFavorite}
style={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{type === "trash" || !selectedItemsList.length ? null : (
<IconButton
style={{
marginLeft: 10
}}
onPress={() => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{type === "trash" && selectedItemsList.length ? (
<>
<IconButton
style={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
style={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
anchor={
<IconButton
onPress={() => {
menuRef.current?.show();
}}
name="dots-vertical"
color={colors.primary.paragraph}
/>
}
>
{[
{
title:
selectedItemsList.length > 1
? "Move notebooks"
: "Move notebook",
onPress: async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
MoveNotebookSheet.present(notebooks);
},
visible: renderedInRoute === "Notebooks",
icon: "arrow-right-bold-box-outline"
},
{
title: "Manage tags",
onPress: async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: "Export",
onPress: async () => {
await sleep(100);
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: "Link notebook",
onPress: async () => {
await sleep(100);
MoveNoteSheet.present();
},
visible: type === "note",
icon: "plus"
},
{
title: "Unlink notebook",
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: "Unfavorite",
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: `Move to trash`,
onPress: async () => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
},
visible: type !== "trash",
icon: "delete"
},
{
title: `Restore`,
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: `Delete`,
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
].map((item) =>
!item.visible ? null : (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0
}}
type="plain"
buttonType={{
text: contextMenuColors.primary.paragraph
}}
icon={item.icon}
key={item.title}
title={item.title}
onPress={async () => {
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
</Menu>
) : null}
</View>
</Animated.View>
</View>
);
}
);

View File

@@ -45,14 +45,16 @@ export const AddNotebookSheet = ({
notebook,
parentNotebook,
close,
showMoveNotesOnComplete
showMoveNotesOnComplete,
defaultTitle
}: {
notebook?: Notebook;
parentNotebook?: Notebook;
close?: (didAddNotebook: boolean) => void;
showMoveNotesOnComplete: boolean;
defaultTitle?: string;
}) => {
const title = useRef(notebook?.title);
const title = useRef(notebook?.title || defaultTitle);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null);
@@ -139,7 +141,7 @@ export const AddNotebookSheet = ({
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : ""}
defaultValue={notebook ? notebook.title : title.current}
/>
<Input
@@ -174,7 +176,8 @@ AddNotebookSheet.present = (
parentNotebook?: Notebook,
context?: string,
onClose?: (didAddNotebook: boolean) => void,
showMoveNotesOnComplete = true
showMoveNotesOnComplete = true,
defaultTitle?: string
) => {
presentSheet({
context: context,
@@ -187,6 +190,7 @@ AddNotebookSheet.present = (
onClose?.(didAddNotebook);
}}
showMoveNotesOnComplete={showMoveNotesOnComplete || false}
defaultTitle={defaultTitle}
/>
)
});

View File

@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect } from "react";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import {
ActivityIndicator,
Keyboard,
@@ -47,6 +53,8 @@ import Paragraph from "../../ui/typography/paragraph";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
import { AddNotebookSheet } from "../add-notebook";
import Input from "../../ui/input";
import { presentDialog } from "../../dialog/functions";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -93,7 +101,11 @@ const MoveNoteSheet = ({
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const [notebooks, loading] = useNotebooks();
const [rootNotebooks, loading] = useNotebooks();
const searchQuery = useRef("");
const searchTimer = useRef<NodeJS.Timeout>();
const [notebooks, setNotebooks] = useState(rootNotebooks);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
@@ -103,6 +115,12 @@ const MoveNoteSheet = ({
(state) => state.multiSelect
);
useEffect(() => {
if (!loading) {
setNotebooks(rootNotebooks);
}
}, [loading, rootNotebooks]);
useEffect(() => {
const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items);
@@ -199,7 +217,6 @@ const MoveNoteSheet = ({
{hasSelected() ? (
<IconButton
name="restore"
type="secondaryAccented"
color={colors.primary.icon}
onPress={() => {
const items = note ? [note.id] : selectedItemsList;
@@ -239,33 +256,48 @@ const MoveNoteSheet = ({
style={{
width: "100%"
}}
keyboardShouldPersistTaps="handled"
ListHeaderComponent={
<View
style={{
paddingHorizontal: 12,
width: "100%"
width: "100%",
paddingTop: 12
}}
>
<Button
title="Add new notebook"
style={{
alignSelf: "flex-start",
paddingHorizontal: 12,
justifyContent: "space-between"
<Input
placeholder="Search notebooks...."
button={{
icon: "plus",
onPress: () => {
AddNotebookSheet.present(
undefined,
undefined,
"link-notebooks",
undefined,
false,
searchQuery.current
);
},
color: colors.primary.icon
}}
onPress={() => {
AddNotebookSheet.present(
undefined,
undefined,
"link-notebooks",
undefined,
false
);
onChangeText={(value) => {
searchQuery.current = value;
if (!searchQuery.current) {
setNotebooks(rootNotebooks);
return;
}
searchTimer.current = setTimeout(() => {
db.lookup
.notebooks(searchQuery.current)
.sorted()
.then((result) => {
if (searchQuery.current === value) {
setNotebooks(result);
}
});
}, 300);
}}
icon="plus"
iconPosition="right"
type="secondaryAccented"
width="100%"
/>
</View>
}

View File

@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { Fragment, useState } from "react";
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Linking,
Platform,
StyleSheet,
View
} from "react-native";
import FileViewer from "react-native-file-viewer";
import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -29,14 +35,12 @@ import { requestInAppReview } from "../../../services/app-review";
import {
PresentSheetOptions,
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { getElevationStyle } from "../../../utils/elevation";
import { eCloseSheet } from "../../../utils/events";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
@@ -65,6 +69,7 @@ const ExportNotesSheet = ({
filePath: string;
name: string;
type: string;
fileDir: string;
}
| undefined
>();
@@ -295,7 +300,9 @@ const ExportNotesSheet = ({
successfully as {result?.fileName}
</Paragraph>
<Button
title="Open"
title={
Platform.OS === "android" ? "Open file location" : "Open"
}
type="accent"
width={250}
fontSize={SIZE.md}
@@ -305,20 +312,23 @@ const ExportNotesSheet = ({
}}
onPress={async () => {
if (!result?.filePath) return;
eSendEvent(eCloseSheet);
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
console.log(e);
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
context: "local"
if (Platform.OS === "android") {
Linking.openURL(result.fileDir).catch((e) => {
ToastManager.error(e as Error);
});
});
} else {
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
context: "local"
});
});
}
}}
/>
<Button
@@ -331,10 +341,10 @@ const ExportNotesSheet = ({
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
if (!result) return;
if (Platform.OS === "ios") {
Share.open({
url: result.filePath
url: result?.fileDir + result.fileName
}).catch(console.log);
} else {
FileViewer.open(result.filePath, {
@@ -347,7 +357,7 @@ const ExportNotesSheet = ({
/>
<Button
title="Export in another format"
type="secondaryAccented"
type="inverted"
width={250}
fontSize={SIZE.md}
style={{

View File

@@ -35,7 +35,10 @@ import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
import { editorController } from "../../../screens/editor/tiptap/utils";
import {
EditorEvents,
editorController
} from "../../../screens/editor/tiptap/utils";
const ListNoteItem = ({
id,
@@ -190,13 +193,13 @@ export default function LinkNote(props: {
}
: undefined
);
editorController.current.commands.createInternalLink(
{
editorController.current?.postMessage(EditorEvents.resolve, {
data: {
href: link,
title: selectedNote.title
},
props.resolverId
);
resolverId: props.resolverId
});
};
const onSelectNote = async (note: Note) => {

View File

@@ -96,10 +96,12 @@ export default function Migrate() {
});
setLoading(true);
await sleep(1);
const { error } = await BackupService.run(false, "local");
const { error, report } = await BackupService.run(false, "local");
if (error) {
ToastManager.error(error, "Backup failed");
reportError(error);
if (report) {
reportError(error);
}
setLoading(false);
return;
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import create from "zustand";
@@ -72,6 +72,20 @@ export const MoveNotebookSheet = ({
}) => {
const [notebooks] = useNotebooks();
const { colors } = useThemeColors();
const [moveToTop, setMoveToTop] = useState(false);
useEffect(() => {
(async () => {
for (const notebook of selectedNotebooks) {
const root = await findRootNotebookId(notebook.id);
if (root !== notebook.id) {
setMoveToTop(true);
return;
}
}
})();
}, [selectedNotebooks]);
const renderItem = useCallback(
({ index }: { index: number }) => {
return (
@@ -93,6 +107,7 @@ export const MoveNotebookSheet = ({
: selectedNotebooks[0].title
} to ${selectedNotebook.title}?`,
positiveText: "Move",
context: "move-notebook",
positivePress: async () => {
for (const notebook of selectedNotebooks) {
const parent = await getParentNotebookId(notebook.id);
@@ -157,36 +172,38 @@ export const MoveNotebookSheet = ({
paddingHorizontal: 12
}}
>
<Button
title="Move to top"
style={{
alignSelf: "flex-start",
width: "100%",
justifyContent: "space-between"
}}
icon="arrow-up-bold"
iconPosition="right"
type="secondaryAccented"
onPress={async () => {
for (const notebook of selectedNotebooks) {
const parent = await getParentNotebookId(notebook.id);
const root = await findRootNotebookId(notebook.id);
if (root !== notebook.id) {
await db.relations.unlink(
{
type: "notebook",
id: parent
},
notebook
);
eSendEvent(eOnNotebookUpdated, parent);
eSendEvent(eOnNotebookUpdated, notebook.id);
{moveToTop ? (
<Button
title="Move to top"
style={{
alignSelf: "flex-start",
width: "100%",
justifyContent: "space-between"
}}
icon="arrow-up-bold"
iconPosition="right"
type="secondaryAccented"
onPress={async () => {
for (const notebook of selectedNotebooks) {
const parent = await getParentNotebookId(notebook.id);
const root = await findRootNotebookId(notebook.id);
if (root !== notebook.id) {
await db.relations.unlink(
{
type: "notebook",
id: parent
},
notebook
);
eSendEvent(eOnNotebookUpdated, parent);
eSendEvent(eOnNotebookUpdated, notebook.id);
}
}
}
useNotebookStore.getState().refresh();
close?.();
}}
/>
useNotebookStore.getState().refresh();
close?.();
}}
/>
) : null}
</View>
}
ListEmptyComponent={

View File

@@ -19,13 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { RefreshControl, View, useWindowDimensions } from "react-native";
import {
Platform,
RefreshControl,
View,
useWindowDimensions
} from "react-native";
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Config from "react-native-config";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv";
import { useNotebook } from "../../../hooks/use-notebook";
import NotebookScreen from "../../../screens/notebook";
@@ -43,9 +49,8 @@ import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import Sort from "../sort";
import { MoveNotebookSheet } from "../move-notebook";
import { db } from "../../../common/database";
import Sort from "../sort";
const useItemSelectionStore = createItemSelectionStore(true, false);
@@ -63,7 +68,8 @@ class NotebookSheetConfig {
}
static get(item: ConfigItem) {
return MMKV.getInt(NotebookSheetConfig.makeId(item)) || 0;
const value = MMKV.getInt(NotebookSheetConfig.makeId(item));
return typeof value === "number" ? value : 0;
}
static set(item: ConfigItem, index = 0) {
@@ -126,11 +132,6 @@ export const NotebookSheet = () => {
if (!focusedRouteId) return;
const nextRoot = await findRootNotebookId(focusedRouteId);
if (nextRoot !== currentItem.current) {
console.log(
"NotebookSheet.useEffect.canShow",
"Root changed to",
nextRoot
);
useItemSelectionStore.setState({
enabled: false,
selection: {}
@@ -145,11 +146,8 @@ export const NotebookSheet = () => {
if (ref.current?.isOpen()) {
ref.current?.snapToIndex(snapPoint);
} else {
setTimeout(() => {
ref.current?.show(snapPoint);
}, 150);
ref.current?.show(snapPoint);
}
console.log("NotebookSheet.useEffect.didShow", focusedRouteId);
setRoot(nextRoot);
onRequestUpdate();
});
@@ -162,7 +160,7 @@ export const NotebookSheet = () => {
ref.current?.hide();
}
}
}, [canShow, onRequestUpdate, focusedRouteId]);
}, [canShow, focusedRouteId]);
return (
<ActionSheet
@@ -198,7 +196,11 @@ export const NotebookSheet = () => {
backgroundColor: colors.secondary.background
}}
keyboardHandlerEnabled={false}
snapPoints={Config.isTesting === "true" ? [100] : [20, 100]}
snapPoints={
Config.isTesting === "true"
? [100]
: [Platform.OS === "android" ? 15 : 10, 100]
}
initialSnapIndex={1}
backgroundInteractionEnabled
gestureEnabled
@@ -237,7 +239,7 @@ export const NotebookSheet = () => {
>
<Icon
name="notebook-plus"
color={colors.primary.accent}
color={colors.primary.icon}
size={SIZE.xxl}
/>
</View>

View File

@@ -18,7 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useRef, useState } from "react";
import { Platform, TextInput, View } from "react-native";
import {
Platform,
TextInput,
View,
ScrollView as RNScrollView
} from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
@@ -364,7 +369,7 @@ export default function ReminderSheet({
))}
</View>
<ScrollView showsHorizontalScrollIndicator={false} horizontal>
<RNScrollView showsHorizontalScrollIndicator={false} horizontal>
{recurringMode === RecurringModes.Daily ||
recurringMode === RecurringModes.Year
? null
@@ -425,7 +430,7 @@ export default function ReminderSheet({
}}
/>
))}
</ScrollView>
</RNScrollView>
</View>
) : null}

View File

@@ -285,9 +285,9 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
throw new Error("Backup file is invalid");
}
// if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
// throw new Error("Backup file is invalid");
// }
await db.transaction(async () => {
let password;

View File

@@ -50,7 +50,7 @@ const Sort = ({ type, screen }) => {
const setOrderBy = async () => {
let _groupOptions = {
...groupOptions,
sortDirection: groupOptions.sortDirection === "asc" ? "desc" : "asc"
sortDirection: groupOptions?.sortDirection === "asc" ? "desc" : "asc"
};
if (type === "topics") {
_groupOptions.groupBy = "none";
@@ -85,22 +85,22 @@ const Sort = ({ type, screen }) => {
<Button
title={
groupOptions.sortDirection === "asc"
? groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
groupOptions?.sortDirection === "asc"
? groupOptions?.groupBy === "abc" ||
groupOptions?.sortBy === "title"
? "A - Z"
: groupOptions.sortBy === "dueDate"
: groupOptions?.sortBy === "dueDate"
? "Earliest first"
: "Old - New"
: groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
: groupOptions?.groupBy === "abc" ||
groupOptions?.sortBy === "title"
? "Z - A"
: groupOptions.sortBy === "dueDate"
: groupOptions?.sortBy === "dueDate"
? "Latest first"
: "New - Old"
}
icon={
groupOptions.sortDirection === "asc"
groupOptions?.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
@@ -134,7 +134,7 @@ const Sort = ({ type, screen }) => {
alignItems: "center"
}}
>
{groupOptions.groupBy === "abc" ? (
{groupOptions?.groupBy === "abc" ? (
<Button
type="secondary"
title="Title"
@@ -154,18 +154,18 @@ const Sort = ({ type, screen }) => {
item === "dateEdited") ? null : (
<Button
key={item}
type={groupOptions.sortBy === item ? "selected" : "plain"}
type={groupOptions?.sortBy === item ? "selected" : "plain"}
title={SORT[item]}
height={40}
iconPosition="left"
icon={groupOptions.sortBy === item ? "check" : null}
icon={groupOptions?.sortBy === item ? "check" : null}
style={{
marginRight: 10,
paddingHorizontal: 8
}}
buttonType={{
text:
groupOptions.sortBy === item
groupOptions?.sortBy === item
? colors.primary.accent
: colors.secondary.paragraph
}}
@@ -213,11 +213,11 @@ const Sort = ({ type, screen }) => {
key={item}
testID={"btn-" + item}
type={
groupOptions.groupBy === GROUP[item] ? "selected" : "plain"
groupOptions?.groupBy === GROUP[item] ? "selected" : "plain"
}
buttonType={{
text:
groupOptions.groupBy === GROUP[item]
groupOptions?.groupBy === GROUP[item]
? colors.primary.accent
: colors.secondary.paragraph
}}
@@ -229,13 +229,13 @@ const Sort = ({ type, screen }) => {
if (item === "abc") {
_groupOptions.sortBy = "title";
_groupOptions.sortDirection = "asc";
_groupOptions.sortDirection = "desc";
}
updateGroupOptions(_groupOptions);
}}
height={40}
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
icon={groupOptions?.groupBy === GROUP[item] ? "check" : null}
title={
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
}

View File

@@ -20,29 +20,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo";
import React from "react";
import {
ActivityIndicator,
Image,
Platform,
TouchableOpacity,
View
} from "react-native";
import { ActivityIndicator, Image, Platform, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useSyncProgress from "../../hooks/use-sync-progress";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
import { useThemeStore } from "../../stores/use-theme-store";
import { SyncStatus, useUserStore } from "../../stores/use-user-store";
import { eOpenLoginDialog } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import { TimeSince } from "../ui/time-since";
import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
export const UserStatus = () => {
const { colors } = useThemeColors();
const { colors, isDark } = useThemeColors();
const user = useUserStore((state) => state.user);
const syncing = useUserStore((state) => state.syncing);
const lastSyncStatus = useUserStore((state) => state.lastSyncStatus);
@@ -97,7 +93,16 @@ export const UserStatus = () => {
borderRadius: 100
}}
/>
) : null}
) : (
<Icon
name="cog-outline"
size={SIZE.lg - 2}
color={colors.secondary.icon}
style={{
paddingLeft: 8
}}
/>
)}
<View
style={{
@@ -110,14 +115,8 @@ export const UserStatus = () => {
size={SIZE.sm}
color={colors.primary.heading}
>
{!user
? "Login to sync your notes."
: lastSyncStatus === SyncStatus.Failed
? "Sync failed, tap to retry"
: syncing
? `Syncing ${progress ? `(${progress.current})` : ""}`
: !userProfile?.fullName
? "Tap to sync"
{!user || !userProfile?.fullName
? "Settings"
: userProfile.fullName}
</Paragraph>
@@ -132,16 +131,20 @@ export const UserStatus = () => {
"Not logged in"
) : lastSynced && lastSynced !== "Never" ? (
<>
{lastSyncStatus === SyncStatus.Failed
{syncing
? `Syncing ${progress ? `(${progress.current})` : ""}`
: lastSyncStatus === SyncStatus.Failed
? "Sync failed"
: "Synced"}{" "}
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.secondary.paragraph
}}
time={lastSynced}
/>
{!syncing ? (
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.secondary.paragraph
}}
time={lastSynced}
/>
) : null}
{isOffline ? " (offline)" : ""}
</>
) : (
@@ -149,7 +152,7 @@ export const UserStatus = () => {
)}{" "}
<Icon
name="checkbox-blank-circle"
size={11}
size={9}
allowFontScaling
color={
!user || lastSyncStatus === SyncStatus.Failed
@@ -162,56 +165,80 @@ export const UserStatus = () => {
</Paragraph>
</View>
<Pressable
<View
style={{
borderRadius: 100,
width: 40,
height: 40
}}
hitSlop={{
top: 10,
bottom: 10,
left: 20
}}
onPress={() => {
if (user) {
Sync.run();
} else {
tabBarRef.current?.closeDrawer();
eSendEvent(eOpenLoginDialog);
}
flexDirection: "row",
gap: 0
}}
>
{user ? (
syncing ? (
<ActivityIndicator
color={colors.primary.accent}
size={SIZE.xl}
/>
) : lastSyncStatus === SyncStatus.Failed ? (
<Icon
color={colors.error.icon}
name="sync-alert"
size={SIZE.lg}
allowFontScaling
/>
<IconButton
hitSlop={{
top: 10,
bottom: 10
}}
onPress={() => {
useThemeStore.getState().setColorScheme();
}}
name="theme-light-dark"
color={isDark ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg}
style={{
borderRadius: 100,
width: 40,
height: 40
}}
/>
<Pressable
style={{
borderRadius: 100,
width: 40,
height: 40
}}
hitSlop={{
top: 10,
bottom: 10
}}
onPress={() => {
if (user) {
Sync.run();
} else {
tabBarRef.current?.closeDrawer();
eSendEvent(eOpenLoginDialog);
}
}}
>
{user ? (
syncing ? (
<ActivityIndicator
color={colors.primary.accent}
size={SIZE.xl}
/>
) : lastSyncStatus === SyncStatus.Failed ? (
<Icon
color={colors.error.icon}
name="sync-alert"
size={SIZE.lg}
allowFontScaling
/>
) : (
<Icon
allowFontScaling
color={colors.primary.icon}
name="sync"
size={SIZE.lg}
/>
)
) : (
<Icon
allowFontScaling
color={colors.primary.accent}
name="sync"
size={SIZE.lg}
name="login"
/>
)
) : (
<Icon
allowFontScaling
color={colors.primary.accent}
size={SIZE.lg}
name="login"
/>
)}
</Pressable>
)}
</Pressable>
</View>
</Pressable>
</View>
</View>

View File

@@ -19,35 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [
{
title: "Bi-directional note linking",
body: "Now you can link notes to each other in both directions."
},
{
title: "Tabs",
body: "Tabs allow you to have multiple notes open at the same time. You can switch between them easily."
},
{
title: "Nested notebooks",
body: "You can now create notebooks inside other notebooks."
},
{
title: "At-rest encryption",
body: "Your notes are now encrypted when stored on the device."
},
{
title: "Material You themed icon",
body: "The app now has a Material You themed icon that changes color based on the wallpaper.",
platform: "android"
},
{
title: "New note quick settings tile",
body: "You can now add a new note tile to quickly create notes from quick settings panel.",
platform: "android"
},
{
title: "And so much more...",
body: "V3 packs so much that it's hard to put everything here, check out our blog at Notesnook V3 is here, and it's packed with new features and improvements such as bi-directional note linking, better syncing, at-rest encryption, nested notebooks, editor tabs, better app lock and so much more. https://blog.notesnook.com/introducing-notesnook-v3."
}
];
export const features: FeatureType[] = [];

View File

@@ -85,8 +85,8 @@ import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
import {
eClearEditor,
eCloseSheet,
eEditorReset,
eLoginSessionExpired,
eOnLoadNote,
eOpenAnnouncementDialog,
@@ -201,11 +201,13 @@ const onRequestPartialSync = async (
`onRequestPartialSync full:${full}, force:${force}, lastSyncTime:${lastSyncTime}`
);
if (full || force) {
await Sync.run("global", force, full, undefined, lastSyncTime);
} else {
await Sync.run("global", false, false, undefined, lastSyncTime);
}
await Sync.run(
"global",
force,
full ? "full" : "send",
undefined,
lastSyncTime
);
};
const onLogout = async (reason: string) => {
@@ -234,7 +236,7 @@ async function checkForShareExtensionLaunchedInBackground() {
if (notesAddedFromIntent || shareExtensionOpened) {
const id = useTabStore.getState().getCurrentNoteId();
const note = id && (await db.notes.note(id));
eSendEvent("webview_reset");
eSendEvent(eEditorReset);
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
@@ -531,13 +533,15 @@ export const useAppEvents = () => {
}
//@ts-ignore
globalThis["IS_SHARE_EXTENSION"] = false;
} else {
const id = useTabStore.getState().getCurrentNoteId();
const note = id ? await db.notes.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note));
if (locked && SettingsService.canLockAppInBackground()) {
eSendEvent(eClearEditor);
if (
SettingsService.getBackgroundEnterTime() + 60 * 1000 * 10 <
Date.now()
) {
// Reset the editor if the app has been in background for more than 10 minutes.
eSendEvent(eEditorReset);
}
} else {
await saveEditorState();
if (
SettingsService.canLockAppInBackground() &&

View File

@@ -71,12 +71,10 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
if (typeof itemId === "string" && itemId !== itemIdRef.current) return;
if (!isValidIdOrIndex(idOrIndex)) return;
console.log("useDBItem.onUpdateItem", idOrIndex, type);
if (items && typeof idOrIndex === "number") {
items.item(idOrIndex).then((item) => {
setItem(item.item);
itemIdRef.current = item.item.id;
itemIdRef.current = item.item?.id;
});
} else {
if (!(db as any)[type + "s"][type]) {

View File

@@ -36,7 +36,6 @@ export const useNotebook = (
const onRequestUpdate = React.useCallback(() => {
if (!item?.id) return;
console.log("useNotebook.onRequestUpdate", item?.id, Date.now());
const selector = db.relations.from(
{
@@ -59,7 +58,6 @@ export const useNotebook = (
useEffect(() => {
if (nestedNotebooks) {
console.log("useNotebook.useEffect.onRequestUpdate");
onRequestUpdate();
}
}, [item?.id, onRequestUpdate, nestedNotebooks]);

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect } from "react";
import BiometicService from "../services/biometrics";
import BiometricService from "../services/biometrics";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { db } from "../common/database";
@@ -39,8 +39,8 @@ export const useVaultStatus = () => {
const checkVaultStatus = useCallback(() => {
db.vault?.exists().then(async (exists) => {
const available = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
const available = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (
VaultStatusCache.exists === exists &&
VaultStatusCache.biometryEnrolled === fingerprint &&

View File

@@ -32,13 +32,14 @@ import WebView from "react-native-webview";
import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import {
ToastManager,
eSendEvent,
eSubscribeEvent
} from "../../services/event-manager";
import {
eEditorReset,
eOnLoadNote,
eUnlockNote,
eUnlockWithBiometrics,
@@ -54,8 +55,10 @@ import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
import {
editorController,
editorState,
openInternalLink
openInternalLink,
randId
} from "./tiptap/utils";
import { tabBarRef } from "../../utils/global-refs";
const style: ViewStyle = {
height: "100%",
@@ -102,24 +105,20 @@ const Editor = React.memo(
noToolbar,
noHeader
});
const renderKey = useRef(`editor-0` + editorId);
const renderKey = useRef(randId("editor-id") + editorId);
useImperativeHandle(ref, () => ({
get: () => editor
}));
useLockedNoteHandler();
const onError = useCallback(() => {
renderKey.current =
renderKey.current === `editor-0`
? `editor-1` + editorId
: `editor-0` + editorId;
renderKey.current = randId("editor-id") + editorId;
editor.state.current.ready = false;
editor.setLoading(true);
}, [editor, editorId]);
useEffect(() => {
const sub = [eSubscribeEvent("webview_reset", onError)];
const sub = [eSubscribeEvent(eEditorReset, onError)];
return () => {
sub.forEach((s) => s?.unsubscribe());
};
@@ -146,6 +145,7 @@ const Editor = React.memo(
nestedScrollEnabled
onError={onError}
injectedJavaScriptBeforeContentLoaded={`
globalThis.__DEV__ = ${__DEV__}
globalThis.readonly=${readonly};
globalThis.noToolbar=${noToolbar};
globalThis.noHeader=${noHeader};
@@ -208,8 +208,8 @@ const useLockedNoteHandler = () => {
useEffect(() => {
(async () => {
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
@@ -223,9 +223,9 @@ const useLockedNoteHandler = () => {
try {
if (!tabRef.current?.noteLocked || !tabRef.current) return;
console.log("Trying to unlock with biometrics...");
const credentials = await BiometicService.getCredentials(
const credentials = await BiometricService.getCredentials(
"Unlock note",
"Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault."
"Unlock note to open it in editor."
);
if (credentials && credentials?.password && tabRef.current.noteId) {
@@ -233,6 +233,7 @@ const useLockedNoteHandler = () => {
tabRef.current.noteId,
credentials?.password
);
eSendEvent(eOnLoadNote, {
item: note
});
@@ -269,7 +270,7 @@ const useLockedNoteHandler = () => {
try {
const unlocked = await db.vault.unlock(password);
if (!unlocked) throw new Error("Incorrect vault password");
await BiometicService.storeCredentials(password);
await BiometricService.storeCredentials(password);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Biometric unlocking enabled!",
@@ -278,8 +279,8 @@ const useLockedNoteHandler = () => {
context: "global"
});
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
@@ -338,7 +339,7 @@ const useLockedNoteHandler = () => {
}),
eSubscribeEvent(eUnlockWithPassword, onSubmit)
];
if (tabRef.current?.locked) {
if (tabRef.current?.locked && tabBarRef.current?.page() === 2) {
unlock();
}
return () => {

View File

@@ -48,5 +48,6 @@ export const EventTypes = {
unlockWithBiometrics: "editor-events:unlock-biometrics",
disableReadonlyMode: "editor-events:disable-readonly-mode",
readonlyEditorLoaded: "readonlyEditorLoaded",
error: "editorError"
error: "editorError",
dbLogger: "editor-events:dbLogger"
};

View File

@@ -21,12 +21,11 @@ import Sodium from "@ammarahmed/react-native-sodium";
import { isImage } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import DocumentPicker from "react-native-document-picker";
import {
ImagePickerResponse,
launchCamera,
launchImageLibrary
} from "react-native-image-picker";
import DocumentPicker, {
DocumentPickerOptions,
DocumentPickerResponse
} from "react-native-document-picker";
import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress";
@@ -42,8 +41,9 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { basename } from "pathe";
const showEncryptionSheet = (file) => {
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
title: "Encrypting attachment",
paragraph: `Please wait while we encrypt ${file.name} file for upload`,
@@ -51,24 +51,25 @@ const showEncryptionSheet = (file) => {
});
};
const santizeUri = (uri) => {
const santizeUri = (uri: string) => {
uri = decodeURI(uri);
uri = Platform.OS === "ios" ? uri.replace("file:///", "/") : uri;
return uri;
};
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} fileOptions
*/
const file = async (fileOptions) => {
type PickerOptions = {
noteId?: string;
tabId?: number;
type: "image" | "camera" | "file";
reupload: boolean;
hash?: string;
context?: string;
outputType?: "base64" | "url" | "cache";
};
const file = async (fileOptions: PickerOptions) => {
try {
const options = {
const options: DocumentPickerOptions<"ios"> = {
mode: "import",
allowMultiSelection: false
};
@@ -87,11 +88,11 @@ const file = async (fileOptions) => {
file = file[0];
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
let uri = Platform.OS === "ios" ? file.fileCopyUri || file.uri : file.uri;
if (file.size > FILE_SIZE_LIMIT) {
if ((file.size || 0) > FILE_SIZE_LIMIT) {
ToastManager.show({
title: "File too large",
heading: "File too large",
message: "The maximum allowed size per file is 500 MB",
type: "error"
});
@@ -108,29 +109,40 @@ const file = async (fileOptions) => {
return;
}
console.log("file uri: ", uri);
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
showEncryptionSheet(file);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"
});
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
if (
!(await attachFile(
uri,
hash,
file.type || "application/octet-stream",
file.name,
fileOptions
))
)
return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
if (!fileOptions.tabId) return;
if (
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
fileOptions.tabId &&
fileOptions.noteId &&
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
fileOptions.noteId
) {
if (isImage(file.type)) {
if (isImage(file.type || "application/octet-stream")) {
editorController.current?.commands.insertImage(
{
hash: hash,
filename: file.name,
mime: file.type,
size: file.size,
dataurl: await db.attachments.read(hash, "base64"),
title: file.name
mime: file.type || "application/octet-stream",
size: file.size || 0,
dataurl: (await db.attachments.read(hash, "base64")) as string,
type: "image"
},
fileOptions.tabId
);
@@ -139,8 +151,9 @@ const file = async (fileOptions) => {
{
hash: hash,
filename: file.name,
mime: file.type,
size: file.size
mime: file.type || "application/octet-stream",
size: file.size || 0,
type: "file"
},
fileOptions.tabId
);
@@ -151,39 +164,72 @@ const file = async (fileOptions) => {
eSendEvent(eCloseSheet);
}, 1000);
} catch (e) {
eSendEvent(eCloseSheet);
ToastManager.show({
heading: e.message,
heading: (e as Error).message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
});
console.log("attachment error: ", e);
DatabaseLogger.error(e);
}
};
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} options
*/
const camera = async (options) => {
const camera = async (options: PickerOptions) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
launchCamera(
{
includeBase64: true,
mediaType: "photo"
},
(response) => handleImageResponse(response, options)
);
openCamera({
mediaType: "photo",
includeBase64: true,
cropping: false,
multiple: true,
maxFiles: 10,
writeTempFile: true
})
.then((response) => {
handleImageResponse(
Array.isArray(response) ? response : [response],
options
);
})
.catch((e) => {
console.log("camera error: ", e);
});
} catch (e) {
ToastManager.show({
heading: e.message,
heading: (e as Error).message,
type: "error",
context: "global"
});
console.log("attachment error:", e);
}
};
const gallery = async (options: PickerOptions) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
openPicker({
includeBase64: true,
mediaType: "photo",
maxFiles: 10,
cropping: false,
multiple: true
})
.then((response) =>
handleImageResponse(
Array.isArray(response) ? response : [response],
options
)
)
.catch((e) => {
console.log("gallery error: ", e);
});
} catch (e) {
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
ToastManager.show({
heading: (e as Error).message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
@@ -192,53 +238,9 @@ const camera = async (options) => {
}
};
const gallery = async (options) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
launchImageLibrary(
{
includeBase64: true,
mediaType: "photo",
selectionLimit: 10
},
(response) => handleImageResponse(response, options)
);
} catch (e) {
ToastManager.show({
heading: e.message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
});
console.log("attachment error:", e);
}
};
/**
*
* @typedef {{
* noteId?: string,
* tabId?: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* context?: string
* }} ImagePickerOptions
*
* @param {{
* noteId?: string,
* tabId?: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* context?: string
* }} options
* @returns
*/
const pick = async (options) => {
const pick = async (options: PickerOptions) => {
if (!PremiumService.get()) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (editorState().isFocused) {
editorState().isFocused = true;
}
@@ -259,58 +261,55 @@ const pick = async (options) => {
file(options);
}
};
/**
*
* @param {ImagePickerResponse} response
* @param {ImagePickerOptions} options
* @returns
*/
const handleImageResponse = async (response, options) => {
if (
response.didCancel ||
response.errorMessage ||
!response.assets ||
response.assets?.length === 0
) {
return;
}
const result = await AttachImage.present(response);
const handleImageResponse = async (
response: Image[],
options: PickerOptions
) => {
console.log(response, "result-file-picker");
const result = await AttachImage.present(response, options.context);
if (!result) return;
const compress = result.compress;
for (let image of response.assets) {
const isPng = /(png)/g.test(image.type);
const isJpeg = /(jpeg|jpg)/g.test(image.type);
for (const image of response) {
const isPng = /(png)/g.test(image.mime);
const isJpeg = /(jpeg|jpg)/g.test(image.mime);
if (compress && (isPng || isJpeg)) {
image.uri = await compressToFile(
Platform.OS === "ios" ? "file://" + image.uri : image.uri,
image.path = await compressToFile(
Platform.OS === "ios" ? "file://" + image.path : image.path,
isPng ? "PNG" : "JPEG"
);
const stat = await RNFetchBlob.fs.stat(image.uri.replace("file://", ""));
image.fileSize = stat.size;
const stat = await RNFetchBlob.fs.stat(image.path.replace("file://", ""));
image.size = stat.size;
image.path =
Platform.OS === "ios" ? image.path.replace("file://", "") : image.path;
}
if (image.fileSize > IMAGE_SIZE_LIMIT) {
if (image.size > IMAGE_SIZE_LIMIT) {
ToastManager.show({
title: "File too large",
heading: "File too large",
message: "The maximum allowed size per image is 50 MB",
type: "error"
});
return;
}
let b64 = `data:${image.type};base64, ` + image.base64;
const uri = decodeURI(image.uri);
const b64 = `data:${image.mime};base64, ` + image.data;
const uri = decodeURI(image.path);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"
});
let fileName = image.originalFileName || image.fileName;
const fileName = image.sourceURL
? basename(image.sourceURL)
: image.filename || "image";
console.log("attaching image...", fileName);
console.log("attaching file...");
if (!(await attachFile(uri, hash, image.type, fileName, options))) return;
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
console.log("attaching image to note...");
@@ -322,11 +321,13 @@ const handleImageResponse = async (response, options) => {
editorController.current?.commands.insertImage(
{
hash: hash,
mime: image.type,
title: fileName,
mime: image.mime,
type: "image",
dataurl: b64,
size: image.fileSize,
filename: fileName
size: image.size,
filename: fileName as string,
width: image.width,
height: image.height
},
options.tabId
);
@@ -343,10 +344,16 @@ const handleImageResponse = async (response, options) => {
* @param {ImagePickerOptions} options
* @returns
*/
export async function attachFile(uri, hash, type, filename, options) {
export async function attachFile(
uri: string,
hash: string,
type: string,
filename: string,
options: PickerOptions
) {
try {
let exists = await db.attachments.exists(hash);
let encryptionInfo;
const exists = await db.attachments.exists(hash);
let encryptionInfo: any;
if (options?.hash && options.hash !== hash) {
ToastManager.show({
heading: "Please select the same file for reuploading",
@@ -358,26 +365,36 @@ export async function attachFile(uri, hash, type, filename, options) {
}
if (!options.reupload && exists) {
options.reupload = (await filesystem.getUploadedFileSize(hash)) <= 0;
options.reupload = (await filesystem.getUploadedFileSize(hash)) === 0;
}
if (options.reupload) {
DatabaseLogger.log(`Deleting file before reupload. ${hash}`);
const deleted = await db.fs().deleteFile(hash, false);
if (!deleted)
throw new Error(`Failed to delete file before reupload. ${hash}`);
}
if (!exists || options?.reupload) {
let key = await db.attachments.generateKey();
const key = await db.attachments.generateKey();
encryptionInfo = await Sodium.encryptFile(key, {
uri: uri,
type: options.type || "url",
type: options.outputType || "url",
hash: hash
});
} as any);
encryptionInfo.mimeType = type;
encryptionInfo.filename = filename;
encryptionInfo.alg = "xcha-stream";
encryptionInfo.size = encryptionInfo.length;
encryptionInfo.key = key;
if (options?.reupload && exists) await db.attachments.reset(hash);
if (options?.reupload && exists) {
const attachment = await db.attachments.attachment(hash);
if (attachment) await db.attachments.reset(attachment?.id);
}
} else {
encryptionInfo = { hash: hash };
}
await db.attachments.add(encryptionInfo, options.noteId);
await db.attachments.add(encryptionInfo);
return true;
} catch (e) {
DatabaseLogger.error(e);

View File

@@ -37,6 +37,7 @@ export type EditorState = {
isAwaitingResult: boolean;
scrollPosition: number;
overlay?: boolean;
initialLoadCalled?: boolean;
};
export type Settings = {
@@ -74,6 +75,8 @@ export type EditorMessage<T> = {
type: string;
noteId: string;
tabId: number;
resolverId?: string;
hasTimeout?: boolean;
};
export type SavePayload = {
@@ -84,6 +87,7 @@ export type SavePayload = {
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: number;
pendingChanges?: boolean;
};
export type AppState = {

View File

@@ -77,7 +77,6 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState, openInternalLink } from "./utils";
const publishNote = async () => {
const user = useUserStore.getState().user;
if (!user) {
@@ -354,8 +353,15 @@ export const useEditorEvents = (
const data = event.nativeEvent.data;
const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.hasTimeout && editorMessage.resolverId) {
editor.postMessage(EditorEvents.resolve, {
data: true,
resolverId: editorMessage.resolverId
});
}
if (editorMessage.type === EventTypes.load) {
console.log("Editor loaded");
DatabaseLogger.log("Editor is ready");
editor.onLoad();
return;
}
@@ -383,21 +389,36 @@ export const useEditorEvents = (
content: editorMessage.value.html as string,
noteId: noteId,
tabId: editorMessage.tabId,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EventTypes.title:
DatabaseLogger.log("EventTypes.title");
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value as string,
title: editorMessage.value?.title as string,
noteId: noteId,
tabId: editorMessage.tabId,
ignoreEdit: false
ignoreEdit: false,
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EventTypes.logger:
logger.info("[WEBVIEW LOG]", editorMessage.value);
logger.info("[EDITOR LOG]", editorMessage.value);
break;
case EventTypes.dbLogger:
if (editorMessage.value.error) {
DatabaseLogger.error(
editorMessage.value.error,
editorMessage.value.error,
{
message: "[EDITOR_ERROR]" + editorMessage.value.message
}
);
} else {
DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message);
}
break;
case EventTypes.contentchange:
editor.onContentChanged(editorMessage.noteId);
@@ -450,7 +471,7 @@ export const useEditorEvents = (
break;
case EventTypes.filepicker:
editorState().isAwaitingResult = true;
const { pick } = require("./picker.js").default;
const { pick } = require("./picker").default;
pick({
type: editorMessage.value,
noteId: noteId,
@@ -469,12 +490,10 @@ export const useEditorEvents = (
case EventTypes.getAttachmentData: {
const attachment = (editorMessage.value as any)
.attachment as Attachment;
?.attachment as Attachment;
console.log(
"Getting attachment data:",
attachment.hash,
attachment.type
DatabaseLogger.log(
`Getting attachment data: ${attachment?.hash} ${attachment?.type}`
);
downloadAttachment(attachment.hash, true, {
base64: attachment.type === "image",
@@ -487,17 +506,17 @@ export const useEditorEvents = (
console.log(
"Got attachment data:",
!!data,
(editorMessage.value as any).resolverId
editorMessage.resolverId
);
editor.postMessage(EditorEvents.attachmentData, {
resolverId: (editorMessage.value as any).resolverId,
editor.postMessage(EditorEvents.resolve, {
resolverId: editorMessage.resolverId,
data
});
})
.catch(() => {
console.log("Error downloading attachment data");
editor.postMessage(EditorEvents.attachmentData, {
resolverId: (editorMessage.value as any).resolverId,
.catch((e) => {
DatabaseLogger.error(e);
editor.postMessage(EditorEvents.resolve, {
resolverId: editorMessage.resolverId,
data: undefined
});
});
@@ -624,7 +643,7 @@ export const useEditorEvents = (
case EventTypes.createInternalLink: {
LinkNote.present(
editorMessage.value.attributes,
editorMessage.value.resolverId
editorMessage.resolverId as string
);
break;
}

View File

@@ -39,6 +39,7 @@ import { DatabaseLogger, db } from "../../../common/database";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
@@ -46,16 +47,20 @@ import {
import Navigation from "../../../services/navigation";
import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useTagStore } from "../../../stores/use-tag-store";
import {
eEditorReset,
eEditorTabFocused,
eOnLoadNote,
eShowMergeDialog,
eUpdateNoteInEditor
} from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
import { EventTypes } from "./editor-events";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { syncTabs, useTabStore } from "./use-tab-store";
@@ -68,6 +73,7 @@ import {
isEditorLoaded,
post
} from "./utils";
import { sleep } from "../../../utils/time";
type NoteWithContent = Note & {
content?: NoteContent<false>;
@@ -145,8 +151,9 @@ export const useEditor = (
useEffect(() => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId) => {
console.log("Editot tab focus changed", lastTabFocused.current, tabId);
if (lastTabFocused.current !== tabId) lock.current = false;
lastTabFocused.current = tabId as number;
console.log(tabId);
});
return () => {
event?.unsubscribe();
@@ -165,10 +172,11 @@ export const useEditor = (
useEffect(() => {
if (loading) {
overlay(true);
state.current.ready = false;
setLoading(false);
}
}, [loading]);
}, [loading, overlay]);
const withTimer = useCallback(
(id: string, fn: () => void, duration: number) => {
@@ -216,7 +224,8 @@ export const useEditor = (
type,
ignoreEdit,
sessionHistoryId: currentSessionHistoryId,
tabId
tabId,
pendingChanges
}: SavePayload) => {
if (currentNotes.current[id as string]?.readonly || readonly) return;
try {
@@ -277,12 +286,26 @@ export const useEditor = (
);
}, 50);
const saveTimer = setTimeout(() => {
DatabaseLogger.log(`Note save timeout: ${id}...`);
ToastManager.error(
new Error(
"Copy your changes and restart the app to avoid data loss. If the issue persists, please report to us at support@streetwriters.co."
),
"Saving note is taking too long",
"global",
15000
);
}, 30 * 1000);
if (!locked) {
DatabaseLogger.log(`Saving note: ${id}...`);
id = await db.notes?.add({ ...noteData });
saved = true;
DatabaseLogger.log(`Note saved: ${id}...`);
clearTimeout(saveTimer);
if (!note && id) {
editorSessionHistory.newSession(id);
if (id) {
@@ -316,9 +339,22 @@ export const useEditor = (
Notifications.pinNote(id as string);
}
} else {
if (!db.vault.unlocked) {
if (pendingChanges) await sleep(3000);
const unlocked = await unlockVault({
title: "Unlock vault to save note",
paragraph: `This note is locked, unlock to save ${
pendingChanges ? "some pending" : ""
} changes`,
context: "global"
});
if (!unlocked)
throw new Error("Could not save note, vault is locked");
}
noteData.contentId = note?.contentId;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await db.vault?.save(noteData as any);
clearTimeout(saveTimer);
}
if (id && useTabStore.getState().getTabForNote(id) === tabId) {
@@ -341,6 +377,16 @@ export const useEditor = (
}
}
if (
id &&
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(EditorEvents.title, title || note?.title, tabId);
postMessage(EditorEvents.html, data, tabId);
currentNotes.current[id] = note;
}
saveCount.current++;
return id;
} catch (e) {
@@ -415,7 +461,11 @@ export const useEditor = (
commands.focus(tabId);
});
} else {
if (!event.item) return;
if (!event.item) {
overlay(false);
return;
}
console.log("LOADING NOTE", event.item.id);
const item = event.item;
const currentTab = useTabStore
@@ -443,7 +493,6 @@ export const useEditor = (
useTabStore.getState().focusTab(tabId);
setTimeout(() => {
if (blockIdRef.current) {
console.log("scrolling to block", blockIdRef.current);
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
@@ -502,6 +551,7 @@ export const useEditor = (
loadingState.current === currentContents.current[item.id]?.data
) {
// If note is already loading, return.
console.log("Note is already loading...");
return;
}
@@ -535,11 +585,8 @@ export const useEditor = (
10000
);
console.log("blockId", blockIdRef.current);
setTimeout(() => {
if (blockIdRef.current) {
console.log("scrolling to block", blockIdRef.current);
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
@@ -573,124 +620,131 @@ export const useEditor = (
data: Note | ContentItem | TrashItem | DeletedItem,
isLocal?: boolean
) => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
try {
await (async () => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
console.log("Removing tab");
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
return;
}
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
const tab = useTabStore.getState().getTab(tabId);
const note = data.type === "note" ? data : await db.notes?.note(noteId);
lock.current = true;
// Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists(
currentNotes.current[noteId] as ItemReference
);
if (note) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
} else {
useTabStore.getState().updateTab(tabId, {
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
console.log("Removing tab");
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
tabId as number
);
}
console.log("readonly state changed...", note.readonly);
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
}
if (data.type === "tiptap" && note && !isLocal) {
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
lock.current = false;
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data, noteId);
if (!decryptedContent) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
} else {
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current?.data) {
lock.current = false;
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] = data as UnencryptedContentItem;
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
const tab = useTabStore.getState().getTab(tabId);
const note =
data.type === "note" ? data : await db.notes?.note(noteId);
lock.current = true;
// Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists(
currentNotes.current[noteId] as ItemReference
);
if (note) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
} else {
useTabStore.getState().updateTab(tabId, {
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
}
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
tabId as number
);
}
console.log("readonly state changed...", note.readonly);
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
}
}
if (data.type === "tiptap" && note && !isLocal) {
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data);
if (!decryptedContent) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
} else {
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current?.data) {
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] =
data as UnencryptedContentItem;
}
}
}
})();
} catch (e) {
DatabaseLogger.error(e as Error, "Error when applying sync changes");
} finally {
lock.current = false;
}
lock.current = false;
},
[postMessage, commands]
);
@@ -713,7 +767,8 @@ export const useEditor = (
type,
ignoreEdit,
noteId,
tabId
tabId,
pendingChanges
}: {
noteId?: string;
title?: string;
@@ -721,18 +776,29 @@ export const useEditor = (
type: string;
ignoreEdit: boolean;
tabId: number;
pendingChanges?: boolean;
}) => {
DatabaseLogger.log(`Saving content...`);
DatabaseLogger.log(
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
);
if (
lock.current ||
(currentLoadingNoteId.current &&
currentLoadingNoteId.current === noteId)
) {
DatabaseLogger.log(`Skipped saving conent:
DatabaseLogger.log(`Skipped saving content:
lock.current: ${lock.current}
currentLoadingNoteId.current: ${currentLoadingNoteId.current}
`);
if (lock.current) {
setTimeout(() => {
if (lock.current) {
DatabaseLogger.warn("Editor force removed lock after 5 seconds");
lock.current = false;
}
}, 5000);
}
return;
}
@@ -740,7 +806,7 @@ export const useEditor = (
lastContentChangeTime.current[noteId] = Date.now();
}
if (type === EditorEvents.content && noteId) {
if (type === EventTypes.content && noteId) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -755,7 +821,8 @@ export const useEditor = (
id: noteId,
ignoreEdit,
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId
tabId: tabId,
pendingChanges
};
withTimer(
noteId || "newnote",
@@ -767,7 +834,16 @@ export const useEditor = (
onChange(params.data);
return;
}
saveNote(params);
if (useSettingStore.getState().isAppLoading) {
const sub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
saveNote(params);
sub();
}
});
} else {
saveNote(params);
}
},
ignoreEdit ? 0 : 150
);
@@ -810,7 +886,7 @@ export const useEditor = (
useTabStore.getState().currentTab
))
) {
eSendEvent("webview_reset", "onReady");
eSendEvent(eEditorReset, "onReady");
return false;
} else {
syncTabs();
@@ -820,7 +896,6 @@ export const useEditor = (
}, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => {
if (currentNotes.current) overlay(true);
setTimeout(() => {
postMessage(EditorEvents.theme, theme);
});
@@ -835,12 +910,22 @@ export const useEditor = (
const noteId = useTabStore.getState().getCurrentNoteId();
if (!noteId) {
overlay(false);
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else if (state.current?.initialLoadCalled) {
const note = currentNotes.current[noteId];
if (note) {
loadNote({
item: note
});
}
}
if (!state.current?.initialLoadCalled) {
state.current.initialLoadCalled = true;
}
overlay(false);
}, [
postMessage,
theme,

View File

@@ -46,7 +46,7 @@ export function editorState() {
return editorController.current?.state.current || defaultState;
}
export const EditorEvents: { [name: string]: string } = {
export const EditorEvents = {
html: "native:html",
updatehtml: "native:updatehtml",
title: "native:title",
@@ -55,7 +55,8 @@ export const EditorEvents: { [name: string]: string } = {
logger: "native:logger",
status: "native:status",
keyboardShown: "native:keyboardShown",
attachmentData: "native:attachment-data"
attachmentData: "native:attachment-data",
resolve: "native:resolve"
};
export function randId(prefix: string) {

View File

@@ -32,6 +32,7 @@ import { openEditor } from "../notes/common";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const [notes, loading] = useNotes();
const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => {
Navigation.routeNeedsUpdate(

View File

@@ -16,40 +16,37 @@ 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 { resolveItems } from "@notesnook/common";
import { VirtualizedGrouping } from "@notesnook/core";
import { Note, Notebook } from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import { NotebookHeader } from "../../components/list-items/headers/notebook-header";
import SelectionHeader from "../../components/selection-header";
import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { eSendEvent, eSubscribeEvent } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import useNavigationStore, {
NotebookScreenParams
} from "../../stores/use-navigation-store";
import { eUpdateNotebookRoute } from "../../utils/events";
import { findRootNotebookId } from "../../utils/notebooks";
import { openEditor, setOnFirstSave } from "../notes/common";
import SelectionHeader from "../../components/selection-header";
import Paragraph from "../../components/ui/typography/paragraph";
import { View } from "react-native";
import { SIZE } from "../../utils/size";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import { resolveItems } from "@notesnook/common";
import { openEditor, setOnFirstSave } from "../notes/common";
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const params = useRef<NotebookScreenParams>(route?.params);
const [loading, setLoading] = useState(true);
const updateOnFocus = useRef(false);
const [breadcrumbs, setBreadcrumbs] = useState<
{
id: string;
@@ -59,11 +56,17 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
useNavigationFocus(navigation, {
onFocus: () => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
if (updateOnFocus.current) {
onRequestUpdate();
updateOnFocus.current = false;
} else {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
}
syncWithNavigation();
return false;
},
onBlur: () => {
updateOnFocus.current = false;
setOnFirstSave(null);
return false;
}
@@ -79,6 +82,15 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const onRequestUpdate = React.useCallback(
async (data?: NotebookScreenParams) => {
if (
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = true;
return;
}
if (data?.item?.id && params.current.item?.id !== data?.item?.id) {
const nextRootNotebookId = await findRootNotebookId(data?.item?.id);
const currentNotebookRoot = await findRootNotebookId(
@@ -104,7 +116,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
if (notebook) {
const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id);
setBreadcrumbs(breadcrumbs);
setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.length - 1));
params.current.item = notebook;
const notes = await db.relations
.from(notebook, "note")
@@ -122,10 +134,10 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
);
useEffect(() => {
onRequestUpdate();
eSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate);
onRequestUpdate(params.current);
const sub = eSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate);
return () => {
eUnSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate);
sub?.unsubscribe();
};
}, [onRequestUpdate]);
@@ -167,7 +179,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onPressDefaultRightButton={openEditor}
/>
{breadcrumbs ? (
{breadcrumbs && breadcrumbs.length > 0 ? (
<View
style={{
width: "100%",

View File

@@ -81,16 +81,21 @@ const NotesPage = ({
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined;
const updateOnFocus = useRef(false);
const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
if (updateOnFocus.current) {
onRequestUpdate();
updateOnFocus.current = false;
} else {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
}
syncWithNavigation();
if (focusControl) return !prev.current;
return false;
},
onBlur: () => {
updateOnFocus.current = false;
setOnFirstSave(null);
return false;
},
@@ -99,7 +104,6 @@ const NotesPage = ({
const syncWithNavigation = React.useCallback(() => {
const { item } = params.current;
useNavigationStore
.getState()
.setFocusedRouteId(params?.current?.item?.id || route.name);
@@ -113,6 +117,14 @@ const NotesPage = ({
const onRequestUpdate = React.useCallback(
async (data?: NotesScreenParams) => {
if (
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = false;
return;
}
const isNew = data && data?.item?.id !== params.current?.item?.id;
if (data) params.current = data;

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import { View } from "react-native";
import { Platform, View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { IconButton } from "../../components/ui/icon-button";
import Navigation from "../../services/navigation";
@@ -49,8 +49,8 @@ export const SearchBar = ({
return selectionMode && isFocused ? null : (
<View
style={{
height: 50 + insets.top,
paddingTop: insets.top,
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "ios" ? 0 : insets.top,
flexDirection: "row",
alignItems: "center",
flexShrink: 1,

View File

@@ -26,7 +26,7 @@ import { Pressable } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import { ToastManager, presentSheet } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
@@ -119,7 +119,7 @@ const AppLock = () => {
type={appLockMode === item.value ? "secondary" : "transparent"}
onPress={async () => {
if (
!(await BiometicService.isBiometryAvailable()) &&
!(await BiometricService.isBiometryAvailable()) &&
!useUserStore.getState().user &&
item.value !== modes[0].value &&
!SettingsService.getProperty("appLockHasPasswordSecurity")
@@ -139,7 +139,7 @@ const AppLock = () => {
) &&
item.value !== modes[0].value
) {
const verified = await BiometicService.validateUser(
const verified = await BiometricService.validateUser(
"Verify it's you"
);
if (verified) {

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "../../common/database";
import { validateAppLockPassword } from "../../common/database/encryption";
import { presentDialog } from "../../components/dialog/functions";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
@@ -100,9 +100,9 @@ export async function verifyUserWithApplock() {
}
});
} else {
BiometicService.isBiometryAvailable().then((available) => {
BiometricService.isBiometryAvailable().then((available) => {
if (available) {
BiometicService.validateUser("Verify it's you").then((verified) => {
BiometricService.validateUser("Verify it's you").then((verified) => {
resolve(verified);
});
} else if (useUserStore.getState().user) {

View File

@@ -164,12 +164,6 @@ export const LICENSES = [
author: "dooboolab",
link: "https://github.com/dooboolab/react-native-iap"
},
{
name: "react-native-image-picker",
licenseType: "MIT",
author: "Johan du Toit (Johan-dutoit)",
link: "https://github.com/react-native-image-picker/react-native-image-picker"
},
{
name: "react-native-keychain",
licenseType: "MIT",

View File

@@ -38,7 +38,7 @@ import { Update } from "../../components/sheets/update";
import { VaultStatusType, useVaultStatus } from "../../hooks/use-vault-status";
import { BackgroundSync } from "../../services/background-sync";
import BackupService from "../../services/backup";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import {
ToastManager,
eSendEvent,
@@ -349,7 +349,7 @@ export const settingsGroups: SettingSection[] = [
await db.user?.logout();
setLoginMessage();
await PremiumService.setPremiumStatus();
await BiometicService.resetCredentials();
await BiometricService.resetCredentials();
MMKV.clearStore();
clearAllStores();
refreshAllStores();
@@ -394,7 +394,7 @@ export const settingsGroups: SettingSection[] = [
if (verified) {
eSendEvent("settings-loading", true);
await db.user?.deleteUser(value);
await BiometicService.resetCredentials();
await BiometricService.resetCredentials();
SettingsService.set({
introCompleted: true
});
@@ -458,7 +458,7 @@ export const settingsGroups: SettingSection[] = [
id: "background-sync",
name: "Background sync (experimental)",
description:
"Periodically wake up the app in background to sync your notes from other devices.",
"Periodically wake up the app in background to run sync.",
type: "switch",
property: "backgroundSync",
onChange: (value) => {
@@ -470,22 +470,43 @@ export const settingsGroups: SettingSection[] = [
}
},
{
id: "sync-issues-fix",
name: "Having problems with sync",
description: "Try force sync to resolve issues with syncing",
icon: "sync-alert",
modifer: async () => {
id: "pull-sync",
name: "Force pull changes",
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.\n\nThese must only be used for troubleshooting. Using them regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
modifer: () => {
presentDialog({
title: "Force sync",
title: "Force Pull changes",
paragraph:
"If your data on two devices is out of sync even after trying to sync normally. You can run force sync to solve such problems. Usually you should never need to run this otherwise. Force sync means that all your data on this device is reuploaded to the server.",
"This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
negativeText: "Cancel",
positiveText: "Start",
positivePress: async () => {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
Sync.run("global", true, "fetch", () => {
eSendEvent(eCloseSheet);
});
}
});
}
},
{
id: "push-sync",
name: "Force push changes",
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.`,
modifer: () => {
presentDialog({
title: "Force Push changes",
paragraph:
"This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
negativeText: "Cancel",
positiveText: "Start",
positivePress: async () => {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, "send", () => {
eSendEvent(eCloseSheet);
});
}
@@ -789,7 +810,7 @@ export const settingsGroups: SettingSection[] = [
}
},
{
id: "biometic-unlock",
id: "biometric-unlock",
type: "switch",
name: "Biometric unlocking",
icon: "fingerprint",
@@ -859,13 +880,13 @@ export const settingsGroups: SettingSection[] = [
if (!SettingsService.getProperty("appLockEnabled")) {
if (
!SettingsService.getProperty("appLockHasPasswordSecurity") &&
(await BiometicService.isBiometryAvailable())
(await BiometricService.isBiometryAvailable())
) {
SettingsService.setProperty("biometricsAuthEnabled", true);
}
if (
!(await BiometicService.isBiometryAvailable()) &&
!(await BiometricService.isBiometryAvailable()) &&
!SettingsService.getProperty("appLockHasPasswordSecurity")
) {
ToastManager.show({

View File

@@ -41,7 +41,7 @@ import {
TouchableOpacity,
View
} from "react-native";
import { db } from "../../common/database";
import { DatabaseLogger, db } from "../../common/database";
import SheetProvider from "../../components/sheet-provider";
import { Button } from "../../components/ui/button";
import Input from "../../components/ui/input";
@@ -326,11 +326,10 @@ function ThemeSelector() {
return page.themes;
})
.flat()
.filter(
(theme) =>
(!searchQuery || searchQuery === "") &&
darkTheme.id !== theme.id &&
lightTheme.id !== theme.id
.filter((theme) =>
searchQuery && searchQuery !== ""
? true
: darkTheme.id !== theme.id && lightTheme.id !== theme.id
) || []
);
}
@@ -546,7 +545,7 @@ const ThemeSetter = ({
context: "global"
});
} catch (e) {
console.log("Error", e);
DatabaseLogger.error(e);
}
setTimeout(() => {

View File

@@ -31,7 +31,6 @@ import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
import { useUserStore } from "../stores/use-user-store";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -152,13 +151,14 @@ async function updateNextBackupTime() {
/**
* @param {boolean=} progress
* @param {string=} context
* @returns {Promise<{path?: string, error?: Error}}>
* @returns {Promise<{path?: string, error?: Error, report?: boolean}}>
*/
async function run(progress = false, context) {
let androidBackupDirectory = await checkBackupDirExists(false, context);
if (!androidBackupDirectory)
return {
error: new Error("Backup directory not selected")
error: new Error("Backup directory not selected"),
report: false
};
if (progress) {
@@ -259,11 +259,12 @@ async function run(progress = false, context) {
return run(progress, context);
}
DatabaseLogger.error(e, "Backup failed");
DatabaseLogger.error(e);
await sleep(300);
progress && eSendEvent(eCloseSheet);
return {
error: e
error: e,
report: true
};
}
}

View File

@@ -179,7 +179,7 @@ async function validateUser(title: string, description?: string) {
}
}
const BiometicService = {
const BiometricService = {
isBiometryAvailable,
enableFingerprintAuth,
isFingerprintAuthEnabled,
@@ -190,4 +190,4 @@ const BiometicService = {
validateUser
};
export default BiometicService;
export default BiometricService;

View File

@@ -162,7 +162,7 @@ export const ToastManager = {
});
},
hide: () => eSendEvent(eHideToast),
error: (e: Error, title?: string, context?: any) => {
error: (e: Error, title?: string, context?: any, duration = 5000) => {
ToastManager.show({
heading: title,
message: e?.message || "",
@@ -176,7 +176,7 @@ export const ToastManager = {
heading: "Logs copied!",
type: "success",
context: "global",
duration: 5000
duration: duration
});
}
});

View File

@@ -17,40 +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 { decode, EntityLevel } from "entities";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
import * as ScopedStorage from "react-native-scoped-storage";
import { zip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../common/database/index";
import { DatabaseLogger } from "../common/database/index";
import Storage from "../common/database/storage";
import {
exportNote as _exportNote,
ExportableAttachment,
ExportableNote,
exportNotes,
sanitizeFilename
exportNotes
} from "@notesnook/common";
import { Note } from "@notesnook/core";
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { basename, dirname, join } from "pathe";
import downloadAttachment from "../common/filesystem/download-attachment";
import { presentDialog } from "../components/dialog/functions";
import { useSettingStore } from "../stores/use-setting-store";
import BiometicService from "./biometrics";
import { ToastManager } from "./event-manager";
import { cacheDir } from "../common/filesystem/utils";
const MIMETypes = {
txt: "text/plain",
pdf: "application/pdf",
md: "text/markdown",
"md-frontmatter": "text/markdown",
html: "text/html"
};
import { unlockVault } from "../utils/unlock-vault";
const FolderNames: { [name: string]: string } = {
txt: "Text",
@@ -59,16 +45,6 @@ const FolderNames: { [name: string]: string } = {
html: "Html"
};
async function releasePermissions(path: string) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (const uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}
async function getPath(type: string) {
let path =
Platform.OS === "ios" &&
@@ -82,153 +58,11 @@ async function getPath(type: string) {
return path;
}
async function save(
path: string,
data: string,
fileName: string,
extension: "txt" | "pdf" | "md" | "html" | "md-frontmatter"
) {
let uri;
if (Platform.OS === "android") {
uri = await ScopedStorage.writeFile(
path,
data,
`${fileName}.${extension}`,
MIMETypes[extension],
extension === "pdf" ? "base64" : "utf8",
false
);
await releasePermissions(path);
} else {
path = path + fileName + `.${extension}`;
await RNFetchBlob.fs.writeFile(path, data, "utf8");
}
return uri || path;
}
async function makeHtml(note: Note, content?: NoteContent<false>) {
let html = await db.notes.export(note.id, {
format: "html",
contentItem: content
});
if (!html) return "";
html = decode(html, {
level: EntityLevel.HTML
});
return html;
}
async function exportAs(
type: string,
note: Note,
bulk?: boolean,
content?: NoteContent<false>
) {
let data;
switch (type) {
case "html":
{
data = await makeHtml(note, content);
}
break;
case "md":
data = await db.notes.export(note.id, {
format: "md",
contentItem: content
});
break;
case "md-frontmatter":
data = await db.notes.export(note.id, {
format: "md-frontmatter",
contentItem: content
});
break;
case "pdf":
{
const html = await makeHtml(note, content);
const fileName = sanitizeFilename(note.title + Date.now(), {
replacement: "_"
});
const options = {
html: html,
fileName:
Platform.OS === "ios" ? "/exported/PDF/" + fileName : fileName,
width: 595,
height: 852,
bgColor: "#FFFFFF",
padding: 30,
base64: bulk || Platform.OS === "android"
} as { [name: string]: any };
if (Platform.OS === "ios") {
options.directory = "Documents";
}
const res = await RNHTMLtoPDF.convert(options);
data = !bulk && Platform.OS === "ios" ? res.filePath : res.base64;
if (bulk && res.filePath) {
RNFetchBlob.fs.unlink(res.filePath);
}
}
break;
case "txt":
{
data = await db.notes.export(note.id, {
format: "txt",
contentItem: content
});
}
break;
}
return data;
}
async function unlockVault() {
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
if (biometry && fingerprint) {
const credentials = await BiometicService.getCredentials(
"Unlock vault",
"Unlock vault to export locked notes"
);
if (credentials) {
return db.vault.unlock(credentials.password);
}
}
useSettingStore.getState().setSheetKeyboardHandler(false);
return new Promise((resolve) => {
setImmediate(() => {
presentDialog({
context: "export-notes",
input: true,
secureTextEntry: true,
positiveText: "Unlock",
title: "Unlock vault",
paragraph: "Some exported notes are locked, Unlock to export them",
inputPlaceholder: "Enter password",
positivePress: async (value) => {
const unlocked = await db.vault.unlock(value);
if (!unlocked) {
ToastManager.show({
heading: "Invalid password",
message: "Please enter a valid password",
type: "error",
context: "local"
});
return false;
}
resolve(unlocked);
useSettingStore.getState().setSheetKeyboardHandler(true);
return true;
},
onClose: () => {
resolve(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}
});
});
async function unlockVaultForNoteExport() {
return await unlockVault({
title: "Unlock vault",
paragraph: "Some exported notes are locked, Unlock to export them",
context: "export-notes"
});
}
@@ -285,6 +119,7 @@ async function createZip(
callback: (progress?: string) => void
) {
const fileName = `nn-export-${totalNotes}-${type}-${Date.now()}.zip`;
const dir = path;
try {
callback("Creating zip");
const zipOutputPath =
@@ -292,6 +127,7 @@ async function createZip(
? join(path, fileName)
: join(RNFetchBlob.fs.dirs.CacheDir, fileName);
await zip(cacheFolder, zipOutputPath);
callback("Saving zip file");
if (Platform.OS === "android") {
const file = await ScopedStorage.createFile(
@@ -303,6 +139,8 @@ async function createZip(
await copyFileAsync("file://" + zipOutputPath, path);
await RNFetchBlob.fs.unlink(zipOutputPath);
callback();
} else {
path = zipOutputPath;
}
RNFetchBlob.fs.unlink(cacheFolder);
} catch (e) {
@@ -311,6 +149,7 @@ async function createZip(
return {
filePath: path,
fileDir: dir,
type: "application/zip",
name: "zip",
fileName: fileName,
@@ -388,7 +227,7 @@ async function bulkExport(
let currentAttachmentProgress = 0;
for await (const item of exportNotes(notes, {
format: type,
unlockVault: unlockVault as () => Promise<boolean>
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) {
if (item instanceof Error) {
DatabaseLogger.error(item);
@@ -432,7 +271,7 @@ async function exportNote(
let currentAttachmentProgress = 0;
for await (const item of _exportNote(note, {
format: type,
unlockVault: unlockVault as () => Promise<boolean>
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) {
if (item instanceof Error) {
DatabaseLogger.error(item);

View File

@@ -49,6 +49,8 @@ import { DDS } from "./device-detection";
import { eSendEvent } from "./event-manager";
import Navigation from "./navigation";
import SettingsService from "./settings";
import { useUserStore } from "../stores/use-user-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
let pinned: DisplayedNotification[] = [];
@@ -139,6 +141,7 @@ const onEvent = async ({ type, detail }: Event) => {
}
editorState().movedAway = false;
const noteId = notification?.id;
console.log("NOTE ID", noteId);
loadNote(noteId as string, true);
}
@@ -407,18 +410,31 @@ async function loadNote(id: string, jump: boolean) {
if (!DDS.isTab && jump) {
tabBarRef.current?.goToPage(1);
}
eSendEvent("loadingNote", note);
setTimeout(
() => {
eSendEvent(eOnLoadNote, {
item: note
});
if (!jump && !DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
},
tabBarRef?.current ? 0 : 2000
MMKV.setString(
"appState",
JSON.stringify({
editing: true,
movedAway: false,
timestamp: Date.now()
})
);
const isLocked = await db.vaults.itemExists({
type: "note",
id: id
});
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
useTabStore.getState().focusTab(tab);
} else {
useTabStore.getState().focusPreviewTab(id, {
noteId: id,
readonly: note.readonly,
noteLocked: isLocked
});
}
}
async function getChannelId(id: "silent" | "vibrate" | "urgent" | "default") {
@@ -476,7 +492,15 @@ async function displayNotification({
reply_button_text?: string;
id?: string;
}) {
if (!(await checkAndRequestPermissions())) return;
useUserStore.setState({
disableAppLockRequests: true
});
const permission = await checkAndRequestPermissions();
useUserStore.setState({
disableAppLockRequests: false
});
if (!permission) return;
try {
await notifee.displayNotification({
@@ -846,7 +870,16 @@ async function remove(id: string) {
}
async function pinQuickNote(launch: boolean) {
if (!(await checkAndRequestPermissions())) return;
useUserStore.setState({
disableAppLockRequests: true
});
const permission = await checkAndRequestPermissions();
useUserStore.setState({
disableAppLockRequests: false
});
if (!permission) {
return;
}
get().then((items) => {
const notification = items.filter((n) => n.id === "notesnook_note_input");
if (notification && launch) {

View File

@@ -191,11 +191,11 @@ function canLockAppInBackground() {
}
let backgroundEnterTime = 0;
function appEnteredBackground() {
if (canLockAppInBackground()) {
backgroundEnterTime = Date.now();
}
backgroundEnterTime = Date.now();
}
const getBackgroundEnterTime = () => backgroundEnterTime;
function shouldLockAppOnEnterForeground() {
if (
useUserStore.getState().disableAppLockRequests ||
@@ -227,7 +227,8 @@ export const SettingsService = {
shouldLockAppOnEnterForeground,
canLockAppInBackground,
appEnteredBackground,
setPrivacyScreen
setPrivacyScreen,
getBackgroundEnterTime
};
init();

View File

@@ -32,19 +32,24 @@ export const ignoredMessages = [
"WebSocket failed to connect",
"Failed to start the HttpConnection before"
];
let pendingSync = undefined;
let syncTimer = 0;
let pendingSync: any = undefined;
let syncTimer: NodeJS.Timeout;
const run = async (
context = "global",
forced = false,
full = true,
onCompleted,
lastSyncTime
type: "full" | "send" | "fetch" = "full",
onCompleted?: (status?: number) => void,
lastSyncTime?: number
) => {
if (useUserStore.getState().syncing) {
DatabaseLogger.info("Sync in progress");
pendingSync = {
full: full
forced,
type: type,
context: context,
onCompleted,
lastSyncTime
};
return;
}
@@ -70,7 +75,7 @@ const run = async (
) {
initAfterSync();
pendingSync = undefined;
return onCompleted?.(false);
return onCompleted?.(SyncStatus.Failed);
}
userstore.setSyncing(true);
@@ -80,9 +85,8 @@ const run = async (
await BackgroundSync.doInBackground(async () => {
try {
await db.sync({
type: full ? "full" : "send",
force: forced,
lastSyncTime
type: type,
force: forced
});
} catch (e) {
error = e;
@@ -95,14 +99,16 @@ const run = async (
} catch (e) {
error = e;
if (
!ignoredMessages.find((message) => e.message?.includes(message)) &&
!ignoredMessages.find((message) =>
(e as Error).message?.includes(message)
) &&
userstore.user &&
status.isConnected &&
status.isInternetReachable
) {
userstore.setSyncing(false, SyncStatus.Failed);
if (status.isConnected && status.isInternetReachable) {
ToastManager.error(e, "Sync failed", context);
ToastManager.error(e as Error, "Sync failed", context);
}
}
@@ -115,7 +121,14 @@ const run = async (
);
onCompleted?.(error ? SyncStatus.Failed : SyncStatus.Passed);
setImmediate(() => {
if (pendingSync) Sync.run("global", false, pendingSync.full);
if (pendingSync)
Sync.run(
pendingSync.context,
pendingSync.forced,
pendingSync.type,
pendingSync.onCompleted,
pendingSync.lastSyncTime
);
});
}
}, 300);

View File

@@ -174,3 +174,4 @@ export const eUnlockWithPassword = "619";
export const eUpdateNoteInEditor = "620";
export const eOnEnterEditor = "621";
export const eOnExitEditor = "622";
export const eEditorReset = "623";

View File

@@ -0,0 +1,91 @@
/*
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 { db } from "../common/database";
import { presentDialog } from "../components/dialog/functions";
import BiometricService from "../services/biometrics";
import { ToastManager } from "../services/event-manager";
import { useSettingStore } from "../stores/use-setting-store";
let unlockPromise: Promise<any> | undefined = undefined;
export async function unlockVault({
context,
title,
paragraph
}: {
context?: string;
title: string;
paragraph: string;
}) {
if (unlockPromise) {
console.log("Unlocking.... waiting for unlock promise");
return unlockPromise;
}
unlockPromise = new Promise(async (resolve) => {
const result = await (async () => {
if (db.vault.unlocked) return true;
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (biometry && fingerprint) {
const credentials = await BiometricService.getCredentials(
title,
paragraph
);
if (credentials) {
return db.vault.unlock(credentials.password);
}
}
useSettingStore.getState().setSheetKeyboardHandler(false);
return new Promise((resolve) => {
setImmediate(() => {
presentDialog({
context: context,
input: true,
secureTextEntry: true,
positiveText: "Unlock",
title: title,
paragraph: paragraph,
inputPlaceholder: "Enter password",
positivePress: async (value) => {
const unlocked = await db.vault.unlock(value);
if (!unlocked) {
ToastManager.show({
heading: "Invalid password",
message: "Please enter a valid password",
type: "error",
context: "local"
});
return false;
}
resolve(unlocked);
useSettingStore.getState().setSheetKeyboardHandler(true);
return true;
},
onClose: () => {
resolve(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}
});
});
});
})();
unlockPromise = undefined;
resolve(result);
});
return unlockPromise;
}

View File

@@ -111,7 +111,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3008
versionCode 3011
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -15,6 +15,8 @@
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<queries>

View File

@@ -1,3 +1,4 @@
Notesnook V3 is here, and it's packed with new features and improvements such as bi-directional note linking, better syncing, at-rest encryption, nested notebooks, editor tabs, better app lock and so much more.
- Added push/pull changes to troubleshoot sync issues in settings
- Bug fixes and performance improvements
Check out our blogpost to learn about what's new at https://blog.notesnook.com/introducing-notesnook-v3.
Thank you for using Notesnook!

View File

@@ -21,9 +21,9 @@
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */; };
656835812BB29A9800144BAB /* OpenSans-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 656835802BB29A8300144BAB /* OpenSans-Italic.ttf */; };
656DD2AB2B1891DF00A362EA /* (null) in Resources */ = {isa = PBXBuildFile; };
656DD2AC2B1891DF00A362EA /* (null) in Resources */ = {isa = PBXBuildFile; };
656DD2AD2B1891DF00A362EA /* (null) in Resources */ = {isa = PBXBuildFile; };
656DD2AB2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
656DD2AC2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
656DD2AD2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
6593E4A3281C345400492C50 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6593E4A2281C345400492C50 /* AppDelegate.mm */; };
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */ = {isa = PBXBuildFile; fileRef = 659BE46625E11A5100E05671 /* notesnook-text.png */; };
65AA857925E6DDEC00772A01 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857825E6DDEC00772A01 /* WidgetKit.framework */; };
@@ -586,9 +586,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
656DD2AB2B1891DF00A362EA /* (null) in Resources */,
656DD2AC2B1891DF00A362EA /* (null) in Resources */,
656DD2AD2B1891DF00A362EA /* (null) in Resources */,
656DD2AB2B1891DF00A362EA /* BuildFile in Resources */,
656DD2AC2B1891DF00A362EA /* BuildFile in Resources */,
656DD2AD2B1891DF00A362EA /* BuildFile in Resources */,
65C400DF2A80B6B600AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
65C149872A61151B005C40F1 /* extension.bundle in Resources */,
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */,
@@ -1015,7 +1015,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2100;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1089,7 +1089,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.3;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1120,7 +1120,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2100;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1194,7 +1194,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.3;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1353,7 +1353,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2100;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1365,7 +1365,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.3;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1396,7 +1396,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2100;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1409,7 +1409,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.3;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1439,7 +1439,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2100;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1513,7 +1513,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.3;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1544,7 +1544,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2100;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1619,7 +1619,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.3;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -319,8 +319,6 @@ PODS:
- React-Core
- react-native-html-to-pdf-lite (0.9.1):
- React
- react-native-image-picker (4.1.2):
- React-Core
- react-native-image-resizer (3.0.5):
- React-Core
- react-native-in-app-review (4.3.3):
@@ -345,14 +343,15 @@ PODS:
- React-Core
- react-native-safe-area-context (4.9.0):
- React-Core
- react-native-share-extension (2.5.6):
- react-native-share-extension (3.0.0):
- React
- react-native-sodium (1.5.4):
- React
- react-native-theme-switch-animation (0.6.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- react-native-webview (11.26.1):
- react-native-webview (13.10.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- React-NativeModulesApple (0.72.0):
- React-callinvoker
@@ -597,7 +596,6 @@ DEPENDENCIES:
- react-native-get-random-values (from `../../node_modules/react-native-get-random-values`)
- react-native-gzip (from `../../node_modules/react-native-gzip`)
- react-native-html-to-pdf-lite (from `../../node_modules/react-native-html-to-pdf-lite`)
- react-native-image-picker (from `../../node_modules/react-native-image-picker`)
- "react-native-image-resizer (from `../../node_modules/@bam.tech/react-native-image-resizer`)"
- react-native-in-app-review (from `../../node_modules/react-native-in-app-review`)
- "react-native-keep-awake (from `../../node_modules/@sayem314/react-native-keep-awake`)"
@@ -736,8 +734,6 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-gzip"
react-native-html-to-pdf-lite:
:path: "../../node_modules/react-native-html-to-pdf-lite"
react-native-image-picker:
:path: "../../node_modules/react-native-image-picker"
react-native-image-resizer:
:path: "../../node_modules/@bam.tech/react-native-image-resizer"
react-native-in-app-review:
@@ -899,7 +895,6 @@ SPEC CHECKSUMS:
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868
react-native-html-to-pdf-lite: 21bfb169bf4cbcd7bec9f736975ee1b3f5292d4a
react-native-image-picker: 9c8a2687b69300ad9e95cec5d38f35ab9d32467d
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
react-native-keep-awake: caee3ff89eaa21dfe29010f0d143566874a04441
@@ -910,10 +905,10 @@ SPEC CHECKSUMS:
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
react-native-quick-sqlite: e0e23b749382a85e4b57146f753de737a6c3a9e1
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
react-native-share-extension: df66a2ee48a62277d79898375e2142bde0782063
react-native-share-extension: faed334b1ddf165f1e576fcabd3dc1c9e748bfa9
react-native-sodium: 955bb0dc3ea05f8ea06d5e96cb89d1be7b5d7681
react-native-theme-switch-animation: 220f883f7be290e79f2ab022093ed1a7a5929e6d
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
react-native-webview: 90153193e679163ec257011fa457d02765120210
React-NativeModulesApple: 1d81d927ef1a67a3545a01e14c2e98500bf9b199
React-perflogger: 684a11499a0589cc42135d6d5cc04d0e4e0e261a
React-RCTActionSheet: 00b0a4c382a13b834124fa3f541a7d8d1d56efb9

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@ammarahmed/notifee-react-native": "7.4.7",
"@ammarahmed/react-native-share-extension": "^2.5.9",
"@ammarahmed/react-native-share-extension": "^3.0.0",
"@ammarahmed/react-native-sodium": "1.5.4",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^3.2.0",
@@ -41,7 +41,6 @@
"react-native-gzip": "1.1.0",
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "12.11.0",
"react-native-image-picker": "4.1.2",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",
"react-native-mmkv-storage": "^0.10.0-alpha.12",
@@ -61,7 +60,7 @@
"react-native-swiper-flatlist": "3.2.2",
"react-native-tooltips": "^1.0.3",
"react-native-vector-icons": "9.2.0",
"react-native-webview": "^11.14.1",
"react-native-webview": "^13.10.0",
"react-native-zip-archive": "6.0.9",
"react-native-quick-sqlite": "^8.0.6",
"react-native-theme-switch-animation": "^0.6.0",

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.0.0",
"version": "3.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.0.0",
"version": "3.0.2",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -3234,6 +3234,7 @@
"@szhsin/react-menu": "^4.1.0",
"buffer": "^6.0.3",
"framer-motion": "^10.16.8",
"localforage": "^1.10.0",
"mdi-react": "9.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -7162,7 +7163,7 @@
},
"../../packages/editor-mobile/node_modules/@types/prop-types": {
"version": "15.7.11",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/q": {
@@ -7182,7 +7183,7 @@
},
"../../packages/editor-mobile/node_modules/@types/react": {
"version": "18.2.39",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -7213,7 +7214,7 @@
},
"../../packages/editor-mobile/node_modules/@types/scheduler": {
"version": "0.16.8",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/semver": {
@@ -12071,7 +12072,7 @@
},
"../../packages/editor-mobile/node_modules/immer": {
"version": "9.0.21",
"dev": true,
"devOptional": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -22460,7 +22461,6 @@
},
"../../packages/editor/node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"../../packages/editor/node_modules/json-parse-even-better-errors": {
@@ -22516,7 +22516,6 @@
},
"../../packages/editor/node_modules/loose-envify": {
"version": "1.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -23038,7 +23037,6 @@
},
"../../packages/editor/node_modules/react": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -23057,7 +23055,6 @@
},
"../../packages/editor/node_modules/react-dom": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
@@ -23177,7 +23174,6 @@
},
"../../packages/editor/node_modules/scheduler": {
"version": "0.23.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -28443,7 +28439,7 @@
"@ammarahmed/notifee-react-native": "7.4.7",
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-share-extension": "^2.5.9",
"@ammarahmed/react-native-share-extension": "^3.0.0",
"@ammarahmed/react-native-sodium": "1.5.4",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^3.2.0",
@@ -28478,7 +28474,6 @@
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "12.11.0",
"react-native-image-crop-picker": "^0.40.2",
"react-native-image-picker": "4.1.2",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",
"react-native-mmkv-storage": "^0.10.0-alpha.12",
@@ -28501,7 +28496,7 @@
"react-native-tooltips": "^1.0.3",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "9.2.0",
"react-native-webview": "^11.14.1",
"react-native-webview": "^13.10.0",
"react-native-zip-archive": "6.0.9"
},
"devDependencies": {
@@ -28596,9 +28591,9 @@
}
},
"node_modules/@ammarahmed/react-native-share-extension": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.5.9.tgz",
"integrity": "sha512-IEtlh/YCWggcse88yqSX5M+rTyEhu5xHTcje30QbVKtJwtWofbqjzL7UO4EIXKpyFh5u4/CDaY5Yy9kQS1yTjA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-3.0.0.tgz",
"integrity": "sha512-pXf/8Zt46yPfJZ4FMlhI+Bgh/yHllNPiOeb9rqTwGJLuWYA7E6QFNuZ4n4smK7rJZrmhOYV12gFUzFtodgC/8w==",
"dependencies": {
"react-native": "^0.63.1"
}
@@ -30896,7 +30891,6 @@
},
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30910,7 +30904,6 @@
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31057,7 +31050,6 @@
},
"node_modules/@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -31068,7 +31060,6 @@
},
"node_modules/@babel/plugin-proposal-unicode-property-regex": {
"version": "7.18.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -31114,7 +31105,6 @@
},
"node_modules/@babel/plugin-syntax-class-static-block": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31151,7 +31141,6 @@
},
"node_modules/@babel/plugin-syntax-export-namespace-from": {
"version": "7.8.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.3"
@@ -31175,7 +31164,6 @@
},
"node_modules/@babel/plugin-syntax-import-assertions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31189,7 +31177,6 @@
},
"node_modules/@babel/plugin-syntax-import-attributes": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31203,7 +31190,6 @@
},
"node_modules/@babel/plugin-syntax-import-meta": {
"version": "7.10.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -31214,7 +31200,6 @@
},
"node_modules/@babel/plugin-syntax-json-strings": {
"version": "7.8.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -31238,7 +31223,6 @@
},
"node_modules/@babel/plugin-syntax-logical-assignment-operators": {
"version": "7.10.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -31299,7 +31283,6 @@
},
"node_modules/@babel/plugin-syntax-private-property-in-object": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31313,7 +31296,6 @@
},
"node_modules/@babel/plugin-syntax-top-level-await": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31340,7 +31322,6 @@
},
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
"version": "7.18.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -31368,7 +31349,6 @@
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.5",
@@ -31426,7 +31406,6 @@
},
"node_modules/@babel/plugin-transform-class-properties": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31441,7 +31420,6 @@
},
"node_modules/@babel/plugin-transform-class-static-block": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31505,7 +31483,6 @@
},
"node_modules/@babel/plugin-transform-dotall-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -31520,7 +31497,6 @@
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31534,7 +31510,6 @@
},
"node_modules/@babel/plugin-transform-dynamic-import": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31563,7 +31538,6 @@
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31620,7 +31594,6 @@
},
"node_modules/@babel/plugin-transform-json-strings": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31648,7 +31621,6 @@
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31676,7 +31648,6 @@
},
"node_modules/@babel/plugin-transform-modules-amd": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
@@ -31706,7 +31677,6 @@
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
@@ -31723,7 +31693,6 @@
},
"node_modules/@babel/plugin-transform-modules-umd": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
@@ -31752,7 +31721,6 @@
},
"node_modules/@babel/plugin-transform-new-target": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31766,7 +31734,6 @@
},
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31781,7 +31748,6 @@
},
"node_modules/@babel/plugin-transform-numeric-separator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31809,7 +31775,6 @@
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.5",
@@ -31841,7 +31806,6 @@
},
"node_modules/@babel/plugin-transform-optional-catch-binding": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31856,7 +31820,6 @@
},
"node_modules/@babel/plugin-transform-optional-chaining": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31885,7 +31848,6 @@
},
"node_modules/@babel/plugin-transform-private-methods": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31900,7 +31862,6 @@
},
"node_modules/@babel/plugin-transform-private-property-in-object": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
@@ -32000,7 +31961,6 @@
},
"node_modules/@babel/plugin-transform-reserved-words": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32092,7 +32052,6 @@
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32122,7 +32081,6 @@
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32136,7 +32094,6 @@
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -32165,7 +32122,6 @@
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -32180,7 +32136,6 @@
},
"node_modules/@babel/preset-env": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.5",
@@ -32273,7 +32228,6 @@
},
"node_modules/@babel/preset-env/node_modules/semver": {
"version": "6.3.0",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -32296,7 +32250,6 @@
},
"node_modules/@babel/preset-modules": {
"version": "0.1.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@@ -34946,7 +34899,6 @@
},
"node_modules/@types/eslint": {
"version": "8.40.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
@@ -34955,7 +34907,6 @@
},
"node_modules/@types/eslint-scope": {
"version": "3.7.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
@@ -34964,7 +34915,6 @@
},
"node_modules/@types/estree": {
"version": "1.0.1",
"dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
@@ -35413,7 +35363,6 @@
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
@@ -35422,22 +35371,18 @@
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
@@ -35447,12 +35392,10 @@
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35463,7 +35406,6 @@
},
"node_modules/@webassemblyjs/ieee754": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
@@ -35471,7 +35413,6 @@
},
"node_modules/@webassemblyjs/leb128": {
"version": "1.11.6",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@xtuc/long": "4.2.2"
@@ -35479,12 +35420,10 @@
},
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35499,7 +35438,6 @@
},
"node_modules/@webassemblyjs/wasm-gen": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35511,7 +35449,6 @@
},
"node_modules/@webassemblyjs/wasm-opt": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35522,7 +35459,6 @@
},
"node_modules/@webassemblyjs/wasm-parser": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35535,7 +35471,6 @@
},
"node_modules/@webassemblyjs/wast-printer": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35593,12 +35528,10 @@
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@yarnpkg/lockfile": {
@@ -35654,7 +35587,6 @@
},
"node_modules/acorn-import-assertions": {
"version": "1.9.0",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8"
@@ -36759,7 +36691,6 @@
},
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
@@ -37860,7 +37791,6 @@
},
"node_modules/enhanced-resolve": {
"version": "5.15.0",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -37973,7 +37903,6 @@
},
"node_modules/es-module-lexer": {
"version": "1.3.0",
"dev": true,
"license": "MIT"
},
"node_modules/es-set-tostringtag": {
@@ -38304,7 +38233,6 @@
},
"node_modules/eslint-scope": {
"version": "5.1.1",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@@ -38316,7 +38244,6 @@
},
"node_modules/eslint-scope/node_modules/estraverse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -38502,7 +38429,6 @@
},
"node_modules/esrecurse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@@ -38513,7 +38439,6 @@
},
"node_modules/estraverse": {
"version": "5.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -38521,7 +38446,6 @@
},
"node_modules/esutils": {
"version": "2.0.3",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@@ -39578,7 +39502,6 @@
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/global": {
@@ -41822,7 +41745,6 @@
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@@ -42090,7 +42012,6 @@
},
"node_modules/loader-runner": {
"version": "4.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
@@ -44742,7 +44663,6 @@
},
"node_modules/randombytes": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
@@ -45141,14 +45061,6 @@
"react-native": "*"
}
},
"node_modules/react-native-image-picker": {
"version": "4.1.2",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-image-zoom-viewer": {
"version": "3.0.1",
"license": "MIT",
@@ -45461,8 +45373,9 @@
}
},
"node_modules/react-native-webview": {
"version": "11.26.1",
"license": "MIT",
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.10.0.tgz",
"integrity": "sha512-bntBbc3JHBve17NL5fqBWPiOYYDz1VmYUPA0UbsUPHgZj6t7TXUQd4yn2yL//XFKXPDlvMO+MPB9E1uAYpEp/g==",
"dependencies": {
"escape-string-regexp": "2.0.0",
"invariant": "2.2.4"
@@ -45521,7 +45434,6 @@
},
"node_modules/react-test-renderer": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"react-is": "^18.2.0",
@@ -45534,12 +45446,10 @@
},
"node_modules/react-test-renderer/node_modules/react-is": {
"version": "18.2.0",
"dev": true,
"license": "MIT"
},
"node_modules/react-test-renderer/node_modules/scheduler": {
"version": "0.23.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -46375,7 +46285,6 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
@@ -47431,7 +47340,6 @@
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.9",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
@@ -47464,7 +47372,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/jest-worker": {
"version": "27.5.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -47477,7 +47384,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/supports-color": {
"version": "8.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -48222,7 +48128,6 @@
},
"node_modules/watchpack": {
"version": "2.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -48245,7 +48150,6 @@
},
"node_modules/webpack": {
"version": "5.88.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -48360,7 +48264,6 @@
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.0",
"version": "3.0.3",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -0,0 +1,12 @@
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
index 5de0845..1b158d8 100644
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
@@ -692,6 +692,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
image.putString("mime", options.outMimeType);
image.putInt("size", (int) new File(compressedImagePath).length());
image.putString("modificationDate", String.valueOf(modificationDate));
+ image.putString("sourceURL", path);
if (includeBase64) {
image.putString("data", getBase64StringFromFile(compressedImagePath));

View File

@@ -1,169 +0,0 @@
diff --git a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
index 8e59033..c87febe 100644
--- a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
+++ b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
@@ -1,6 +1,7 @@
package com.imagepicker;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
import android.content.ContentResolver;
@@ -21,6 +22,7 @@ import android.provider.OpenableColumns;
import android.util.Base64;
import android.webkit.MimeTypeMap;
+import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import androidx.exifinterface.media.ExifInterface;
@@ -36,6 +38,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -266,7 +269,9 @@ public class Utils {
MediaMetadataRetriever m = new MediaMetadataRetriever();
m.setDataSource(context, uri);
int duration = Math.round(Float.parseFloat(m.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))) / 1000;
- m.release();
+ try {
+ m.release();
+ } catch(Exception e) {}
return duration;
}
@@ -383,7 +388,7 @@ public class Utils {
return fileUris;
}
- static ReadableMap getImageResponseMap(Uri uri, Options options, Context context) {
+ static WritableMap getImageResponseMap(Uri uri, Options options, Context context) {
String fileName = uri.getLastPathSegment();
int[] dimensions = getImageDimensions(uri, context);
@@ -418,13 +423,16 @@ public class Utils {
for(int i = 0; i < fileUris.size(); ++i) {
Uri uri = fileUris.get(i);
+ String fileName = getNameFromURI(context,uri);
if (isImageType(uri, context)) {
if (uri.getScheme().contains("content")) {
uri = getAppSpecificStorageUri(uri, context);
}
uri = resizeImage(uri, context, options);
- assets.pushMap(getImageResponseMap(uri, options, context));
+ WritableMap resMap = getImageResponseMap(uri, options, context);
+ resMap.putString("originalFileName",fileName);
+ assets.pushMap(resMap);
} else if (isVideoType(uri, context)) {
assets.pushMap(getVideoResponseMap(uri, context));
} else {
@@ -438,6 +446,32 @@ public class Utils {
return response;
}
+ /**
+ * Return file name from Uri given.
+ * @param context the context, cannot be null.
+ * @param uri uri request for file name, cannot be null
+ * @return the corresponding display name for file defined in uri or null if error occurs.
+ */
+ @SuppressLint("Range")
+ static String getNameFromURI(@NonNull Context context, @NonNull Uri uri) {
+ String result = null;
+ Cursor c = null;
+ try {
+ c = context.getContentResolver().query(uri, null, null, null, null);
+ c.moveToFirst();
+ result = c.getString(c.getColumnIndex(OpenableColumns.DISPLAY_NAME));
+ }
+ catch (Exception e){
+ // error occurs
+ }
+ finally {
+ if(c != null){
+ c.close();
+ }
+ }
+ return result;
+ }
+
static ReadableMap getErrorMap(String errCode, String errMsg) {
WritableMap map = Arguments.createMap();
map.putString("errorCode", errCode);
diff --git a/node_modules/react-native-image-picker/ios/ImagePickerManager.m b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
index b634ffb..18b8950 100644
--- a/node_modules/react-native-image-picker/ios/ImagePickerManager.m
+++ b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
@@ -96,6 +96,39 @@ - (void) showPickerViewController:(UIViewController *)picker
});
}
+-(NSString *)cachesDirectoryName
+{
+ static NSString *cachePath = nil;
+ if(!cachePath) {
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
+ cachePath = [paths objectAtIndex:0];
+ }
+
+ return cachePath;
+}
+
+///
+/// Builds paths for identifiers in the cache directory
+///
+-(NSString *)pathForName:(NSString *)name
+{
+ NSString *cachePath = [self cachesDirectoryName];
+ NSString *path = [cachePath stringByAppendingPathComponent:name];
+ return path;
+}
+
+#pragma mark - NSData Cache methods
+
+///
+/// Saves the given data to the cache directory
+///
+-(NSString *)saveToCacheDirectory:(NSData *)data withName:(NSString *)name
+{
+ NSString *path = [self pathForName:name];
+ [data writeToFile:path atomically:YES];
+ return path;
+}
+
#pragma mark - Helpers
-(NSMutableDictionary *)mapImageToAsset:(UIImage *)image data:(NSData *)data {
@@ -121,22 +154,14 @@ -(NSMutableDictionary *)mapImageToAsset:(UIImage *)image data:(NSData *)data {
asset[@"type"] = [@"image/" stringByAppendingString:fileType];
NSString *fileName = [self getImageFileName:fileType];
- NSString *path = [[NSTemporaryDirectory() stringByStandardizingPath] stringByAppendingPathComponent:fileName];
- [data writeToFile:path atomically:YES];
+ NSString *path = [self saveToCacheDirectory:data withName:fileName];
if ([self.options[@"includeBase64"] boolValue]) {
asset[@"base64"] = [data base64EncodedStringWithOptions:0];
}
- NSURL *fileURL = [NSURL fileURLWithPath:path];
- asset[@"uri"] = [fileURL absoluteString];
-
- NSNumber *fileSizeValue = nil;
- NSError *fileSizeError = nil;
- [fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError];
- if (fileSizeValue){
- asset[@"fileSize"] = fileSizeValue;
- }
+ asset[@"uri"] = path;
+ asset[@"fileSize"] = [NSNumber numberWithInteger:[data length]];
asset[@"fileName"] = fileName;
asset[@"width"] = @(image.size.width);

View File

@@ -33,6 +33,7 @@ import {
SafeAreaView,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
useWindowDimensions
@@ -115,12 +116,14 @@ const ShareView = () => {
const appendNoteId = useShareStore((state) => state.appendNote);
const [note, setNote] = useState({ ...defaultNote });
const noteContent = useRef("");
const noteTitle = useRef("");
const [loading, setLoading] = useState(false);
const [loadingExtension, setLoadingExtension] = useState(true);
const [rawData, setRawData] = useState({
type: null,
value: null
});
const inputRef = useRef(null);
const [mode, setMode] = useState(1);
const keyboardHeight = useRef(0);
const { width, height } = useWindowDimensions();
@@ -192,6 +195,7 @@ const ShareView = () => {
}
return;
}
let note = { ...defaultNote };
for (let item of data) {
if (item.type === "text") {
@@ -207,7 +211,14 @@ const ShareView = () => {
if (!key) continue;
if (key.includes("TITLE") || key.includes("SUBJECT")) {
note.title = item[key];
console.log("Note title will be", note.title);
noteTitle.current = note.title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
if (key.includes("TEXT") && !note.content.data) {
note.content.data = item[key];
noteContent.current = item[key];
}
}
} else {
@@ -216,8 +227,9 @@ const ShareView = () => {
if (
(isImage(item.type) && item.size > IMAGE_SIZE_LIMIT) ||
(!isImage(item.type) && item.size > FILE_SIZE_LIMIT)
)
) {
continue;
}
setRawFiles((files) => {
const index = files.findIndex(
@@ -244,6 +256,7 @@ const ShareView = () => {
);
const onLoad = useCallback(() => {
console.log(noteContent.current, "current...");
eSendEvent(eOnLoadNote + "shareEditor", {
id: null,
content: {
@@ -275,7 +288,7 @@ const ShareView = () => {
const onPress = async () => {
setLoading(true);
if (!noteContent.current && rawFiles.length === 0) {
if (!noteContent.current && rawFiles.length === 0 && !noteTitle.current) {
setLoading(false);
return;
}
@@ -298,12 +311,14 @@ const ShareView = () => {
type: "tiptap"
},
id: note.id,
sessionId: Date.now()
sessionId: Date.now(),
title: noteTitle.current
};
} else {
noteData = { ...note };
noteData.content.data = noteContent.current;
noteData.sessionId = Date.now();
noteData.title = noteTitle.current;
}
try {
@@ -468,10 +483,29 @@ const ShareView = () => {
borderBottomWidth: 1,
paddingBottom: 12,
borderBottomColor: colors.secondary.background,
paddingHorizontal: 12
paddingHorizontal: 12,
gap: 10
}}
>
<Heading size={SIZE.lg}>Save to Notesnook</Heading>
<TextInput
placeholder="Enter note title"
ref={inputRef}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "OpenSans-SemiBold",
fontSize: SIZE.lg,
paddingBottom: 0,
paddingTop: 0
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current.focus();
}}
/>
<Button
title="Done"
type="accent"
@@ -626,13 +660,15 @@ const ShareView = () => {
}}
>
{!loadingExtension && !loadingPage ? (
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
<>
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
</>
) : (
<>
{loadingPage ? (
@@ -646,7 +682,19 @@ const ShareView = () => {
</SafeAreaProvider>
</View>
{appendNoteId ? <AppendNote id={appendNoteId} /> : null}
{appendNoteId ? (
<AppendNote
id={appendNoteId}
onLoad={(title) => {
if (!noteTitle.current) {
noteTitle.current = title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
}}
/>
) : null}
<View
style={{
@@ -766,10 +814,16 @@ const ShareView = () => {
);
};
const AppendNote = ({ id }) => {
const AppendNote = ({ id, onLoad }) => {
const { colors } = useThemeColors();
const [item] = useDBItem(id, "note");
useEffect(() => {
if (item?.title) {
onLoad?.(item.title);
}
}, [item?.title, onLoad]);
return !item ? null : (
<Paragraph
size={SIZE.xs}

View File

@@ -1 +1 @@
Test note 1 (9) Test note 1 (8) Test note 1 (7) Test note 1 (6) Test note 1 (5) Test note 1 (4) Test note 1 (3) Test note 1 (2) Test note 1 (1) Test note 1 (0) 53ad8e4e40ebebd0f400498d
53ad8e4e40ebebd0f400498dTest note 1 (0) Test note 1 (1) Test note 1 (2) Test note 1 (3) Test note 1 (4) Test note 1 (5) Test note 1 (6) Test note 1 (7) Test note 1 (8) Test note 1 (9)

View File

@@ -1 +1 @@
Test note 2 (9)Test note 2 (8)Test note 2 (7)Test note 2 (6)Test note 2 (5)Test note 2 (4)Test note 2 (3)Test note 2 (2)Test note 2 (1)Test note 2 (0)f054d19e9a2f46eff7b9bb25
f054d19e9a2f46eff7b9bb25Test note 2 (0)Test note 2 (1)Test note 2 (2)Test note 2 (3)Test note 2 (4)Test note 2 (5)Test note 2 (6)Test note 2 (7)Test note 2 (8)Test note 2 (9)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1 +1 @@
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made

View File

@@ -262,7 +262,7 @@ test("using Shift+Click when no notes are selected should not crash the app", as
info.setTimeout(60 * 1000);
const { notes } = await populateList(page, 5);
await page.reload();
await notes.focus();
const note = await notes.findNote({ title: "Test note 3" });

View File

@@ -123,7 +123,7 @@ export class AppModel {
);
}
async waitForSync(state: "completed" | "synced" = "completed") {
async waitForSync(state: "completed" | "synced" | "syncing" = "completed") {
await this.page
.locator(getTestId(`sync-status-${state}`))
.waitFor({ state: "visible" });

View File

@@ -284,11 +284,9 @@ test("change title of a locked note", async ({ page }) => {
await page.waitForTimeout(150);
await page.reload();
await page.waitForTimeout(500);
const editedNote = await notes.findNote({ title, content: NOTE.content });
await notes.waitForList();
const editedNote = await notes.findNote({ title });
await editedNote?.openLockedNote(PASSWORD);
await notes.editor.waitForLoading();
expect(await note?.getTitle()).toContain(title);
expect(await notes.editor.getTitle()).toContain(title);
});

View File

@@ -17,12 +17,25 @@
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
<style>
img {
max-width: 100% !important;
height: auto !important;
border-radius: 5px;
.image-container {
display: block;
}
.image-container.align-right {
display: flex;
justify-content: end;
}
.image-container.align-center {
display: flex;
justify-content: center;
}
.image-container.float {
float: left;
}
.image-container.float.align-right {
float: right;
}
body {
background-color: transparent !important;
color: #202124;
@@ -166,6 +179,6 @@
</head>
<body>
<h1>Test 1</h1>
<p data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
<p data-block-id="xxx" data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
</body>
</html>

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { test, Browser, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { USER } from "./utils";
import exp from "constants";
async function createDevice(browser: Browser) {
// Create two isolated browser contexts
@@ -40,7 +39,9 @@ async function actAndSync<T>(
) {
const results = await Promise.all([
...actions.filter((a) => !!a),
...devices.map((d) => d.waitForSync("synced"))
...devices.map((d) =>
d.waitForSync("syncing").then(() => d.waitForSync("synced"))
)
]);
await Promise.all(devices.map((d) => d.page.waitForTimeout(2000)));
@@ -61,6 +62,7 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
createDevice(browser),
createDevice(browser)
]);
const [notesA, notesB] = await Promise.all(
[deviceA, deviceB].map((d) => d.goToNotes())
);
@@ -72,14 +74,17 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
if ((await notesB.editor.getContent("text")) !== "")
await actAndSync([deviceA, deviceB], notesB.editor.clear());
await expect(notesA.editor.content).toHaveText("");
await expect(notesB.editor.content).toHaveText("");
await expect(notesA.editor.content).toBeEmpty();
await expect(notesB.editor.content).toBeEmpty();
await actAndSync([deviceA, deviceB], notesB.editor.setContent(newContent));
expect(noteA).toBeDefined();
expect(noteB).toBeDefined();
await expect(notesA.editor.content).toHaveText(newContent);
await expect(notesB.editor.content).toHaveText(newContent);
await (await deviceA.goToSettings())?.logout();
await (await deviceB.goToSettings())?.logout();
});
function makeid(length: number) {

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.0.0",
"version": "3.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.0.0",
"version": "3.0.3",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.0.0",
"version": "3.0.3",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -18,12 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { lazify } from "../utils/lazify";
import { logger } from "../utils/logger";
import { showToast } from "../utils/toast";
import { db } from "./db";
async function download(hash: string, groupId?: string) {
const attachment = await db.attachments.attachment(hash);
if (!attachment) return;
if (!attachment) {
logger.debug("could not find attachment for download", { hash, groupId });
return;
}
const downloadResult = await db
.fs()
.downloadFile(
@@ -40,19 +44,27 @@ async function download(hash: string, groupId?: string) {
}
export async function saveAttachment(hash: string) {
const response = await download(hash);
if (!response) return;
try {
const response = await download(hash);
if (!response) return;
const { attachment, key } = response;
await lazify(import("../interfaces/fs"), ({ saveFile }) =>
saveFile(attachment.hash, {
key,
iv: attachment.iv,
name: attachment.filename,
type: attachment.mimeType,
isUploaded: !!attachment.dateUploaded
})
);
const { attachment, key } = response;
await lazify(import("../interfaces/fs"), ({ saveFile }) =>
saveFile(attachment.hash, {
key,
iv: attachment.iv,
name: attachment.filename,
type: attachment.mimeType,
isUploaded: !!attachment.dateUploaded
})
);
} catch (e) {
console.error(e);
showToast(
"error",
`Failed to download attachment: ${hash} (error: ${(e as Error).message})`
);
}
}
type OutputTypeToReturnType = {
@@ -68,6 +80,7 @@ export async function downloadAttachment<
type: TType,
groupId?: string
): Promise<TOutputType | undefined> {
logger.debug("downloading attachment", { hash, type, groupId });
try {
const response = await download(hash, groupId);
if (!response) return;
@@ -85,6 +98,7 @@ export async function downloadAttachment<
isUploaded: !!attachment.dateUploaded
})
);
logger.debug("Attachment decrypted", { hash });
if (!blob) return;
return blob as TOutputType;
@@ -106,9 +120,12 @@ export async function checkAttachment(hash: string) {
import("../interfaces/fs"),
({ getUploadedFileSize }) => getUploadedFileSize(hash)
);
if (size <= 0) return { failed: "File length is 0." };
if (size === 0) throw new Error("File length is 0.");
else if (size === -1) throw new Error("File verification check failed.");
} catch (e) {
return { failed: e instanceof Error ? e.message : "Unknown error." };
const reason = e instanceof Error ? e.message : "Unknown error.";
await db.attachments.markAsFailed(attachment.id, reason);
return { failed: reason };
}
return { success: true };
}

View File

@@ -31,6 +31,8 @@ import {
} from "@notesnook/common";
import Vault from "./vault";
import { ExportStream } from "../utils/streams/export-stream";
import { showToast } from "../utils/toast";
import { confirm } from "./dialog-controller";
export async function exportToPDF(
title: string,
@@ -79,20 +81,36 @@ export async function exportNotes(
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
notes: FilteredSelector<Note>
): Promise<boolean> {
return await TaskManager.startTask({
const result = await TaskManager.startTask({
type: "modal",
title: "Exporting notes",
subtitle: "Please wait while your notes are exported.",
action: async (report) => {
const errors: Error[] = [];
const exportStream = new ExportStream(report, (e) => errors.push(e));
await fromAsyncIterator(
_exportNotes(notes, { format, unlockVault: Vault.unlockVault })
)
.pipeThrough(new ExportStream(report))
.pipeThrough(exportStream)
.pipeThrough(createZipStream())
.pipeTo(await createWriteStream("notes.zip"));
return true;
return {
errors,
count: exportStream.progress
};
}
});
confirm({
title: `Exported ${result.count} notes`,
message:
result.errors.length > 0
? `Export completed with ${result.errors.length} errors:
${result.errors.map((e, i) => `${i + 1}. ${e.message}`).join("\n")}`
: "Export completed with 0 errors.",
positiveButtonText: "Okay"
});
return true;
}
const FORMAT_TO_EXT = {
@@ -130,7 +148,9 @@ export async function exportNote(
unlockVault: Vault.unlockVault
})
)
.pipeThrough(new ExportStream(report))
.pipeThrough(
new ExportStream(report, (e) => showToast("error", e.message))
)
.pipeThrough(createZipStream())
.pipeTo(
await createWriteStream(

View File

@@ -195,7 +195,10 @@ export async function restoreBackupFile(backupFile: File) {
}
entries.push(entry);
}
if (!isValid) throw new Error("Invalid backup.");
if (!isValid)
console.warn(
"The backup file does not contain the verification .nnbackup file."
);
await db.transaction(async () => {
for (const entry of entries) {

View File

@@ -75,6 +75,10 @@ export class SharedService<T extends object> extends EventTarget {
{ signal: this.#onClose.signal }
);
window.addEventListener("beforeunload", () => {
this.close();
});
this.proxy = this.#createProxy();
}

View File

@@ -303,7 +303,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
break;
}
case "force-sync": {
await appStore.sync(true, true);
await appStore.sync({ type: "full", force: true });
break;
}
case "backup": {

View File

@@ -121,7 +121,7 @@ function Header(props: HeaderProps) {
}}
defaultItems={() =>
db.tags.all.limit(10).items(undefined, {
sortBy: "dateCreated",
sortBy: "title",
sortDirection: "desc"
})
}

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