Compare commits

..

59 Commits

Author SHA1 Message Date
Ammar Ahmed
8f8a73a9e1 mobile: fix build on newer xcode 2026-07-30 23:25:45 +05:00
Abdullah Atta
f7c755f2d6 editor: sort code block languages by recently used (#10144) 2026-07-30 08:57:38 +05:00
Kashaf Ansari
10a08d5719 mobile: fix add notes to notebook selection behavior (#10104)
* mobile: fix add notes to notebook selection behavior

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: improve move notes keyboard interactions

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: fix bottom inset when keyboard is open

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:56:18 +05:00
Kashaf Ansari
6e10ce8d8e mobile: add sync status button to side menu (#10112)
* mobile: add sync status button to side menu

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: minor fixes

* mobile: hide sync status during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: skipped sync during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:54:05 +05:00
Abdullah Atta
b5140d9947 Merge pull request #10159 from streetwriters/release/3.4.5
web: bump version to 3.4.5
2026-07-28 21:09:08 +05:00
Abdullah Atta
2288b946ad desktop: launch app with --hidden flag for tests 2026-07-28 19:53:37 +05:00
Abdullah Atta
5fd43ae08e desktop: fix auto start with minimized window on macos 2026-07-28 19:53:37 +05:00
Abdullah Atta
729c410602 global: update intl & lockfiles 2026-07-28 19:53:37 +05:00
Abdullah Atta
bb30336f5c desktop: add tests for #9942 2026-07-28 19:52:48 +05:00
01zulfi
0dc89ba945 web: bump version to 3.4.5
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:52:47 +05:00
Abdullah Atta
bb51244fa3 editor: update prosemirror packages (#10158)
* editor: update prosemirror packages

* editor: fix `__serializeForClipboard` usage
2026-07-28 19:46:51 +05:00
Abdullah Atta
b5ac41c29b web: improve "skip & go directly to app" wording (#10167) 2026-07-28 19:46:23 +05:00
01zulfi
aafb6049b2 core: delete all vaults when user triggers their vault deletion (#9939)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:45:30 +05:00
Abdullah Atta
32070327f9 core: add logs to diagnose why decryption fails after changing password (#10139) 2026-07-27 10:20:25 +05:00
Abdullah Atta
771a8bb4e3 web: fix editor undo history reset on image delete (#10143) 2026-07-25 08:55:58 +05:00
Ammar Ahmed
4f816b47ba ci: fix ios build entitlements (#10146) 2026-07-24 18:55:11 +05:00
01zulfi
1071249baa desktop: fix app not appearing in alt-tab switcher (#9764)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-24 13:05:59 +05:00
Ammar Ahmed
d4775ad340 Merge pull request #10133 from streetwriters/chore/update-workflows
ci: publish ios preview app on linux runner
2026-07-24 12:10:35 +05:00
Chloe Oletto
0d8c855cd0 desktop: If --hidden passed via cli and any system tray option enabled, don't show the window. streetwriters/notesnook#9902 flathub/com.Notesnook.notesnook#99 (#9942)
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-07-24 09:16:17 +05:00
Ammar Ahmed
d63ce5a213 Merge pull request #10126 from streetwriters/mobile-release/3.4.7
mobile: release v3.4.7
2026-07-22 11:12:45 +05:00
Ammar Ahmed
a1d6bb91e5 ci: fix ci workflow 2026-07-22 10:53:18 +05:00
Ammar Ahmed
a20f6960d7 ci: fix workflow 2026-07-22 10:45:49 +05:00
Ammar Ahmed
6b2b224f41 ci: fix ios workflow 2026-07-22 10:37:31 +05:00
Ammar Ahmed
054a023142 ci: fix intermittent ios preview build failure 2026-07-22 10:13:16 +05:00
Ammar Ahmed
441a6396be ci: publish ios preview app on linux runner 2026-07-22 10:02:18 +05:00
Chloe Oletto
987671e59c web: fix web-clipper crash on clipped file. (#10127)
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-07-21 12:45:13 +05:00
Ammar Ahmed
0fc705ce1e Merge pull request #10130 from streetwriters/chore/update-workflows
ci: update mobile workflows
2026-07-21 12:41:02 +05:00
Ammar Ahmed
b3805534d6 ci: remove authorize step in android/ios preview builds 2026-07-21 12:35:08 +05:00
Ammar Ahmed
77c009e83d mobile: fix toast message shows under the sheet in global context 2026-07-21 12:14:08 +05:00
Ammar Ahmed
c9f1cba702 ci: update mobile workflows 2026-07-21 11:36:23 +05:00
Ammar Ahmed
637cf914ee mobile: fix app gets stuck after deleting history item on iOS 2026-07-21 10:25:12 +05:00
Ammar Ahmed
64877c519b Merge pull request #10128 from streetwriters/chore/update-workflows
ci: update workflows to use actions/checkout@v5
2026-07-21 10:04:20 +05:00
Ammar Ahmed
229b620473 ci: update workflows to use actions/checkout@v5 2026-07-21 09:27:49 +05:00
Ammar Ahmed
658e915589 mobile: release v3.4.7 2026-07-20 13:12:20 +05:00
Ammar Ahmed
d4658aa032 Merge pull request #10095 from kashaf-ansari-dev/fix-reminder-notification-permission
Improve the Android reminder notification permission flow by properly handling notification and exact alarm permissions.
2026-07-17 09:10:57 +05:00
Ammar Ahmed
489c793806 Merge pull request #10041 from kashaf-ansari-dev/feat/9723-clear-note-history
mobile: add option to clear note version history
2026-07-15 09:52:04 +05:00
kashaf-ansari-dev
eeae993736 mobile: add version history deletion strings
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
c8cf4ce52a mobile: fix individual note history version deletion
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
f6c31d19d8 mobile: add missing context prop in dialog
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
a94e8d6852 mobile: render clear-history dialog with local context
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
2edbc3bf2e mobile: remove clear version history action for multiple selected notes
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
e4a7d63e07 mobile: add option to clear note version history
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:30 +05:00
Ammar Ahmed
76fd61af4f Merge pull request #10085 from streetwriters/fix/651
mobile: fix unable to change plan on buy-plan screen
2026-07-14 08:43:53 +05:00
Ammar Ahmed
98386a6d71 Merge pull request #10094 from streetwriters/mobile-release/3.4.6
mobile: release v3.4.6
2026-07-13 22:44:13 +05:00
kashaf-ansari-dev
539a630f77 mobile: improve reminder permission settings flow on Android
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 18:07:40 +05:00
kashaf-ansari-dev
6b65f8031d mobile: fix reminder creation after granting notification permission
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 14:58:31 +05:00
01zulfi
3c85cddfe7 Merge pull request #10093 from streetwriters/release/3.4.4
web: bump version to 3.4.4
2026-07-13 14:14:25 +05:00
Ammar Ahmed
e426426468 mobile: release v3.4.6 2026-07-13 12:56:49 +05:00
01zulfi
109e46bd06 web: bump version to 3.4.4
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-13 12:28:07 +05:00
Ammar Ahmed
281a973c37 mobile: fix unable to change plan on buy-plan screen 2026-07-11 12:00:15 +05:00
Ammar Ahmed
2738207c20 Merge pull request #9998 from kashaf-ansari-dev/fix-9967-linking-note-blocks-loading-state
mobile: prevent upgrade prompt flash in note block linking list
2026-07-10 18:41:04 +05:00
01zulfi
1aa73c1f23 web: update editor's font size dynamically when default font size setting changes (#10047)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-10 09:39:21 +05:00
Masatoshi Ogiwara
9ec22724e9 fix: typo in some documents (#10071) 2026-07-09 10:34:09 +05:00
Kashaf Ansari
4a7aa27bd9 mobile: add keep-screen-on preference in settings-behaviour (#10043)
* mobile: add keep-screen-on preference in settings-behaviour

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: default Keep Screen On to true and clarify description

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-09 09:49:38 +05:00
kashaf-ansari-dev
6051614d96 mobile: add bottom spacing for create link button
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-08 18:23:23 +05:00
kashaf-ansari-dev
295a461dda mobile: wire upgrade plan button to paywall on link-note
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-08 17:55:00 +05:00
Chloe Oletto
a4cbe83211 vericrypt: Update to support DEK. (#10005)
* vericrypt: Update to support DEK.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* vericrypt: Clear account data on restart.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update apps/vericrypt/src/components/decrypted-result.tsx

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Update data encryption documentation to be accurate with the current client versions.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Explain what happens better.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Fix typo in encryption key generation section

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update docs/help/contents/how-is-my-data-encrypted.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: update wording

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-07-08 10:24:17 +05:00
Masatoshi Ogiwara
afd63db043 docs(help): typo in documents (#10064) 2026-07-08 08:41:17 +05:00
kashaf-ansari-dev
e0b9932aba mobile: prevent upgrade prompt flash in note block linking list
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-19 22:31:56 +05:00
96 changed files with 2054 additions and 2323 deletions

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Enable KVM
run: |

View File

@@ -0,0 +1,104 @@
name: Notesnook Android Preview Build
# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and
# compiled with a read-only GITHUB_TOKEN and NO repository secrets. The release
# APK is built without any signing secret (the same as before). Firebase
# distribution and the PR comment happen in android.preview.publish.yml, which
# runs in the trusted `workflow_run` context and never executes fork code.
# Because no secrets are exposed here, the build runs automatically for every
# PR (including forks) with no authorization gate.
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/android.preview.build.yml"
- ".github/workflows/android.preview.publish.yml"
concurrency:
group: android-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-22.04
env:
STAGING_BUILD: true
steps:
- name: Checkout PR code
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/android && chmod +x ./gradlew
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build arm64-v8a apk
run: |
cd apps/mobile/android
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
- name: Stage build artifact
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/artifact"
cp apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk \
"$RUNNER_TEMP/artifact/app-preview.apk"
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}"
} > "$RUNNER_TEMP/artifact/pr-meta.env"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: android-preview-build
path: ${{ runner.temp }}/artifact
if-no-files-found: error
retention-days: 1
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/generated/sourcemaps/**/*.map

View File

@@ -1,135 +0,0 @@
name: Notesnook Android Preview
on:
pull_request_target:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/android.preview.firebase.yml"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
runs-on: ubuntu-22.04
env:
STAGING_BUILD: true
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/android && chmod +x ./gradlew
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build arm64-v8a apk
run: |
cd apps/mobile/android
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
- name: Get app version
id: package-version
uses: saionaro/extract-package-version@master
with:
path: apps/mobile
- name: Publish to firebase CLI
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- android-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/generated/sourcemaps/**/*.map

View File

@@ -0,0 +1,72 @@
name: Notesnook Android Preview Publish
# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so
# it has the base repo's secrets and a write-scoped token. It downloads the APK
# the build produced and distributes it + posts the PR comment. It never checks
# out or executes fork code.
on:
workflow_run:
workflows: ["Notesnook Android Preview Build"]
types: [completed]
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: android-preview-build
path: ${{ runner.temp }}/artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Load PR metadata
run: cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_ENV"
- name: Publish to Firebase
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: ${{ runner.temp }}/artifact/app-preview.apk
releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}
- name: Post or update PR comment
uses: actions/github-script@v7
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- android-preview-comment -->';
const prNumber = Number(process.env.PR_NUMBER);
if (!prNumber) return;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.HEAD_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -35,7 +35,7 @@ jobs:
needs: authorize
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -56,7 +56,7 @@ jobs:
macos-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -127,7 +127,7 @@ jobs:
linux-x64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -171,7 +171,7 @@ jobs:
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -215,7 +215,7 @@ jobs:
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -142,7 +142,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -246,7 +246,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -314,7 +314,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -378,7 +378,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -58,7 +58,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -92,7 +92,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -126,7 +126,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

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

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

152
.github/workflows/ios.preview.build.yml vendored Normal file
View File

@@ -0,0 +1,152 @@
name: Notesnook iOS Preview Build
# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and
# compiled with a read-only GITHUB_TOKEN and NO repository secrets. It only
# produces an *unsigned* archive. Signing, Firebase distribution and PR
# comments happen in ios.preview.publish.yml, which runs in the trusted
# `workflow_run` context and never executes fork code. Because no secrets are
# exposed here, the build runs automatically for every PR (including forks)
# with no authorization gate.
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/ios.preview.build.yml"
- ".github/workflows/ios.preview.publish.yml"
# A fork that spams pushes shouldn't queue up macOS builds.
concurrency:
group: ios-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build:
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout PR code
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
- name: Setup iOS Platform
run: |
# GitHub runs this as `/bin/bash -e`, so `set +e` is required -- without
# it the first failure aborts the step before any retry can happen.
# Both commands hit "Unable to connect to simulator" (exit 70) when
# CoreSimulatorService is wedged, so retry each and bounce it between.
set +e
DL_DIR="$RUNNER_TEMP/ios-platform"
retry() {
for i in 1 2 3; do
"$@" && return 0
echo "Attempt $i failed ($*); resetting CoreSimulator..."
sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService simdiskimaged 2>/dev/null
sleep 20
xcrun simctl list runtimes >/dev/null 2>&1
done
echo "::error::Failed after 3 attempts: $*"
exit 1
}
xcrun simctl list runtimes >/dev/null 2>&1
retry xcodebuild -downloadPlatform iOS -exportPath "$DL_DIR"
# Find the dmg; its name embeds a build number that changes.
DMG="$(find "$DL_DIR" -name '*.dmg' | head -n1)"
[ -n "$DMG" ] || { echo "::error::No dmg in $DL_DIR"; exit 1; }
retry xcodebuild -importPlatform "$DMG"
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npm run tx @notesnook/mobile:build
- name: Cache Pods
uses: actions/cache@v4
id: pods-cache
with:
path: apps/mobile/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/ios
pod install
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Get marketing version
id: marketing-version
run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Make staging xcconfig active
run: |
cd apps/mobile/ios/build-configs
./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}}
# Archive unsigned: this job has no certs/profiles (secrets), and the iOS
# device SDK forbids ad-hoc signing, so real signing happens in the trusted
# publish workflow (ios.preview.publish.yml). Do NOT pass
# CODE_SIGN_ENTITLEMENTS="" -- each target must keep its own entitlements so
# the archive records them; blanking them dropped the App Group and crashed
# MMKV on launch.
- name: Archive (unsigned)
run: |
set -euo pipefail
xcodebuild \
-workspace apps/mobile/ios/Notesnook.xcworkspace \
-scheme Notesnook \
-configuration Release \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/Notesnook.xcarchive" \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
archive
- name: Stage build artifact
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/artifact"
tar -czf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" \
-C "$RUNNER_TEMP" Notesnook.xcarchive
# Carry the PR context forward; workflow_run cannot see it reliably for forks.
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}"
} > "$RUNNER_TEMP/artifact/pr-meta.env"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ios-preview-build
path: ${{ runner.temp }}/artifact
if-no-files-found: error
retention-days: 1

View File

@@ -1,173 +0,0 @@
name: Notesnook iOS Preview
on:
pull_request_target:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/ios.preview.firebase.yml"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
- name: Setup iOS Platform
run: |
xcodebuild -downloadPlatform iOS -exportPath ~/Downloads
xcodebuild -importPlatform ~/Downloads/iphonesimulator_26.1_23B86.dmg
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npm run tx @notesnook/mobile:build
- name: Cache Pods
uses: actions/cache@v3
id: pods-cache
with:
path: apps/mobile/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/ios
pod install
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Get marketing version
id: marketing-version
run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Make staging xcconfig active
run: |
cd apps/mobile/ios/build-configs
./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}}
- name: Build iOS App
uses: ammarahm-ed/ios-build-action@master
with:
bundle-identifier: org.streetwriters.notesnook
scheme: Notesnook
configuration: "Release"
export-options: apps/mobile/ios/ExportOptionsStaging.plist
export-method: "ad-hoc"
project-path: apps/mobile/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
update-targets: |
Notesnook
Make Note
NotesWidgetExtension
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
code-signing-identity: Apple Distribution
team-id: ${{ secrets.APPLE_TEAM_ID }}
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
output-path: Notesnook.ipa
mobileprovision-base64: |
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-preview-ipa
path: Notesnook.ipa
if-no-files-found: error
retention-days: 1
publish:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 20
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: ios-preview-ipa
path: .
- name: Publish to firebase CLI
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_IOS_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: Notesnook.ipa
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- ios-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -0,0 +1,222 @@
name: Notesnook iOS Preview Publish
# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so
# it has the base repo's secrets and a write-scoped token. It checks out the
# BASE repository (never fork code), downloads the unsigned archive the build
# produced, then signs + distributes it and posts the PR comment. Signing is
# done directly with `codesign` on the prebuilt archive -- no app build phases
# run here, so fork code is never executed in this trusted context.
#
# Split into two jobs on purpose: signing needs macOS, but the Firebase
# distribution action is a Docker container action and only runs on Linux.
on:
workflow_run:
workflows: ["Notesnook iOS Preview Build"]
types: [completed]
jobs:
export:
# Only publish previews for builds that actually succeeded.
if: github.event.workflow_run.conclusion == 'success'
runs-on: macos-26
timeout-minutes: 30
outputs:
pr_number: ${{ steps.meta.outputs.PR_NUMBER }}
head_sha: ${{ steps.meta.outputs.HEAD_SHA }}
steps:
- name: Checkout base repo (trusted)
uses: actions/checkout@v5
# No `ref` -> checks out the default branch, i.e. trusted base code.
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: ios-preview-build
path: ${{ runner.temp }}/artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Load PR metadata & extract archive
id: meta
run: |
set -euo pipefail
cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_OUTPUT"
tar -xzf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" -C "$RUNNER_TEMP"
- name: Import signing certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
- name: Install provisioning profiles
env:
PROFILE_APP: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
PROFILE_SHARE: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
PROFILE_WIDGET: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
run: |
set -euo pipefail
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILE_DIR"
i=0
for p in "$PROFILE_APP" "$PROFILE_SHARE" "$PROFILE_WIDGET"; do
echo "$p" | base64 --decode > "$PROFILE_DIR/preview-$i.mobileprovision"
i=$((i + 1))
done
# Sign the unsigned archive by hand. `xcodebuild -exportArchive` re-signs
# from the archive's `archived-expanded-entitlements.xcent`, which an
# unsigned archive does not contain, so it dropped the App Group and the
# app crashed on launch. Instead we sign each bundle directly, taking the
# entitlements from each provisioning profile (exactly what Apple
# provisioned -- App Group, keychain groups with the right team prefix).
# Runs on macOS but executes NO fork build phases, so it stays trusted.
- name: Sign IPA
run: |
set -euo pipefail
PROFILES="$HOME/Library/MobileDevice/Provisioning Profiles"
APP="$(find "$RUNNER_TEMP/Notesnook.xcarchive/Products/Applications" -maxdepth 1 -name '*.app' | head -n1)"
[ -n "$APP" ] || { echo "::error::No .app in archive"; exit 1; }
# Single Apple Distribution identity from the imported keychain.
IDENTITY="$(security find-identity -v -p codesigning | awk '/Apple Distribution/{print $2; exit}')"
[ -n "$IDENTITY" ] || { echo "::error::No Apple Distribution identity found"; exit 1; }
echo "Signing identity: $IDENTITY"
TMP="$RUNNER_TEMP/sign"; mkdir -p "$TMP"
# Decode a profile's bundle id (application-identifier minus team prefix).
profile_bundle_id() {
security cms -D -i "$1" -o "$TMP/_p.plist" 2>/dev/null
local appid; appid="$(plutil -extract Entitlements.application-identifier raw -o - "$TMP/_p.plist")"
echo "${appid#*.}"
}
# Find the profile matching a bundle id; echoes its path.
profile_for() {
local want="$1" p
for p in "$PROFILES"/*.mobileprovision; do
[ -e "$p" ] || continue
[ "$(profile_bundle_id "$p")" = "$want" ] && { echo "$p"; return 0; }
done
return 1
}
# Embed the matching profile and sign a bundle with the profile's entitlements.
sign_bundle() {
local bundle="$1" bid prof ent
bid="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$bundle/Info.plist")"
prof="$(profile_for "$bid")" || { echo "::error::No provisioning profile for $bid"; exit 1; }
ent="$TMP/ent-$bid.plist"
security cms -D -i "$prof" -o "$TMP/_p.plist"
plutil -extract Entitlements xml1 -o "$ent" "$TMP/_p.plist"
cp "$prof" "$bundle/embedded.mobileprovision"
echo "Signing $(basename "$bundle") ($bid)"
codesign --force --sign "$IDENTITY" --entitlements "$ent" "$bundle"
}
# Sign inside-out: nested code first, then extensions, then the app.
if [ -d "$APP/Frameworks" ]; then
find "$APP/Frameworks" -maxdepth 1 \( -name '*.framework' -o -name '*.dylib' \) -print0 \
| while IFS= read -r -d '' f; do codesign --force --sign "$IDENTITY" "$f"; done
fi
if [ -d "$APP/PlugIns" ]; then
for appex in "$APP/PlugIns"/*.appex; do
[ -e "$appex" ] || continue
if [ -d "$appex/Frameworks" ]; then
find "$appex/Frameworks" -maxdepth 1 \( -name '*.framework' -o -name '*.dylib' \) -print0 \
| while IFS= read -r -d '' f; do codesign --force --sign "$IDENTITY" "$f"; done
fi
sign_bundle "$appex"
done
fi
sign_bundle "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
# Package into an IPA.
rm -rf "$TMP/Payload"; mkdir -p "$TMP/Payload"
cp -R "$APP" "$TMP/Payload/"
( cd "$TMP" && zip -qry "$RUNNER_TEMP/Notesnook.ipa" Payload )
# Fail fast if the App Group didn't make it into the signed app -- its
# absence is the launch crash (MMKV nil group path). Never distribute
# a build without it.
echo "::group::Signed app entitlements"
codesign -d --entitlements :- "$APP" 2>/dev/null || true
echo "::endgroup::"
codesign -d --entitlements :- "$APP" 2>/dev/null | grep -q "group.org.streetwriters.notesnook" \
|| { echo "::error::Signed app is missing the App Group entitlement; it would crash on launch."; exit 1; }
echo "App Group entitlement present."
- name: Upload signed IPA
uses: actions/upload-artifact@v4
with:
name: ios-preview-ipa
path: ${{ runner.temp }}/Notesnook.ipa
if-no-files-found: error
retention-days: 1
# Must be Linux: wzieba/Firebase-Distribution-Github-Action is a Docker
# container action, which GitHub only supports on Linux runners.
distribute:
needs: export
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Download signed IPA
uses: actions/download-artifact@v4
with:
name: ios-preview-ipa
path: ${{ runner.temp }}
- name: Publish to Firebase
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_IOS_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: ${{ runner.temp }}/Notesnook.ipa
releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ needs.export.outputs.pr_number }}
- name: Post or update PR comment
uses: actions/github-script@v7
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
PR_NUMBER: ${{ needs.export.outputs.pr_number }}
HEAD_SHA: ${{ needs.export.outputs.head_sha }}
with:
script: |
const marker = '<!-- ios-preview-comment -->';
const prNumber = Number(process.env.PR_NUMBER);
if (!prNumber) return;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.HEAD_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -9,7 +9,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

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

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Collect package metadata
id: package_metadata

View File

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

View File

@@ -19,7 +19,7 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

View File

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

View File

@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
@@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}

View File

@@ -42,6 +42,7 @@ export interface AppContext {
export interface TestOptions {
version: string;
args?: string[];
config?: Record<string, unknown>;
}
@@ -72,7 +73,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
const ctx: AppContext = {
app,
@@ -82,7 +84,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
ctx.app = app;
ctx.userDataDir = userDataDir;
@@ -94,11 +97,12 @@ export async function buildAndLaunchApp(
async function launchApp(
executablePath: string,
userDataDir: string,
version?: string
version?: string,
args: string[] = []
) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
args: IS_DEBUG ? [...args] : ["--hidden", ...args],
baseURL: "https://app.notesnook.com",
acceptDownloads: true,
env: {
@@ -154,6 +158,7 @@ export async function buildApp(version?: string) {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
CSC_IDENTITY_AUTO_DISCOVERY: "false",
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,

View File

@@ -17,7 +17,65 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test } from "@nn/test";
import { test, expect } from "@nn/test";
import type { ElectronApplication } from "@playwright/test";
async function getMainWindowState(app: ElectronApplication) {
return await app.evaluate((window) => {
const { BrowserWindow } = window;
const mainWindow = BrowserWindow.getAllWindows()[0];
if (!mainWindow) throw new Error("Main window not found");
return {
isMinimized: mainWindow.isMinimized(),
isVisible: mainWindow.isVisible()
};
});
}
test("make sure app loads", async ({ page }) => {
await page.waitForSelector(".ProseMirror");
});
test("hidden launch minimizes when tray is disabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: false
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(true);
});
test("hidden launch does not minimize when close-to-tray is enabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: true
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(false);
});

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -92,6 +92,17 @@ async function createWindow() {
const cliOptions = await parseArguments(process.argv);
setTheme(getTheme());
// this workaround is necessary because macos doesn't support
// the --hidden flag when launching the app on startup
if (
process.platform === "darwin" &&
app.getLoginItemSettings().wasOpenedAtLogin &&
config.desktopSettings.autoStart &&
config.desktopSettings.startMinimized
) {
cliOptions.hidden = true;
}
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
@@ -141,7 +152,13 @@ async function createWindow() {
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
if (
cliOptions.hidden &&
!(
config.desktopSettings.minimizeToSystemTray ||
config.desktopSettings.closeToSystemTray
)
)
mainWindow.minimize();
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
@@ -164,6 +181,14 @@ async function createWindow() {
);
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
mainWindow.on("show", () =>
/**
* We may set `skipTaskbar` to true at startup.
* This also removes the window from the Alt-Tab switcher.
* To fix that, whenever the app is shown, we set `skipTaskbar` to false.
*/
mainWindow.setSkipTaskbar(false)
);
mainWindow.once("closed", () => {
globalThis.window = null;
});

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
with:
persist-credentials: false

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
with:
persist-credentials: false

View File

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3111
versionCode 3113
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -1,3 +1,4 @@
- Add option to clear note version history
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -36,6 +36,7 @@ import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
interface FloatingButtonProps {
onPress: () => void;
@@ -47,6 +48,7 @@ interface FloatingButtonProps {
position?: "left" | "right";
size?: "small" | "large";
style?: ViewStyle;
hideOnKeyboard?: boolean;
}
const FloatingButton = ({
@@ -57,26 +59,28 @@ const FloatingButton = ({
testID,
position = "right",
size = "large",
style
style,
hideOnKeyboard = true
}: FloatingButtonProps) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const translate = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const route = useRoute();
const insets = useGlobalSafeAreaInsets();
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translate.value
},
{
translateY: translate.value
}
]
};
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: hideOnKeyboard ? translate.value : 0
},
{
translateY: hideOnKeyboard ? translate.value : 0
}
],
bottom: hideOnKeyboard ? 15 : keyboardHeight.value + 15
}));
const animate = useCallback(
(toValue: number) => {
@@ -97,12 +101,22 @@ const FloatingButton = ({
editorState().keyboardState = false;
if (deviceMode !== "mobile") return;
animate(0);
keyboardHeight.value = withTiming(0);
};
const onKeyboardShow = () => {
const onKeyboardShow = (e: any) => {
editorState().keyboardState = true;
if (deviceMode !== "mobile") return;
animate(150);
if (hideOnKeyboard) {
animate(150);
} else {
keyboardHeight.value = withTiming(
e.endCoordinates.height - insets.bottom,
{
duration: 250
}
);
}
};
const sub = [
@@ -110,9 +124,9 @@ const FloatingButton = ({
Keyboard.addListener("keyboardDidHide", onKeyboardHide)
];
return () => {
sub.forEach((sub) => sub?.remove?.());
sub.forEach((sub) => sub.remove());
};
}, [deviceMode, animate]);
}, [deviceMode, animate, hideOnKeyboard, keyboardHeight, insets.bottom]);
return deviceMode !== "mobile" && !alwaysVisible ? null : (
<Animated.View
@@ -161,10 +175,10 @@ const FloatingButton = ({
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}

View File

@@ -72,6 +72,7 @@ const MergeConflicts = () => {
const insets = useGlobalSafeAreaInsets();
const content = useRef<UnencryptedContentItem>(null);
const { height } = useSettingStore((state) => state.dimensions);
const keepScreenOn = useSettingStore((state) => state.settings.keepScreenOn);
const applyChanges = async () => {
const contentToSave = selectedContent;
@@ -155,9 +156,9 @@ const MergeConflicts = () => {
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
@@ -182,9 +183,9 @@ const MergeConflicts = () => {
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
@@ -385,7 +386,7 @@ const MergeConflicts = () => {
paddingTop: insets.top
}}
>
<KeepAwake />
{keepScreenOn && <KeepAwake />}
{dialogVisible && (
<BaseDialog visible={true}>
<DialogContainer>

View File

@@ -29,7 +29,7 @@ import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useDBItem } from "../../hooks/use-db-item";
import { presentSheet } from "../../services/event-manager";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
@@ -39,15 +39,19 @@ import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
import { presentDialog } from "../dialog/functions";
import { Dialog } from "../dialog";
const HistoryItem = ({
index,
items,
note
note,
refresh
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
note: Note;
refresh: () => void;
}) => {
const [item] = useDBItem(index, "noteHistory", items);
const { colors } = useThemeColors();
@@ -75,12 +79,13 @@ const HistoryItem = ({
}}
content={content}
note={note}
update={refresh}
/>
),
context: "note_history"
});
},
[note]
[note, refresh]
);
return (
@@ -112,7 +117,8 @@ const HistoryItem = ({
};
export default function NoteHistory({
note
note,
fwdRef
}: {
note: Note;
fwdRef: RefObject<ActionSheetRef>;
@@ -121,7 +127,7 @@ export default function NoteHistory({
const [_loading, setLoading] = useState(true);
const { colors } = useThemeColors();
useEffect(() => {
const refresh = useCallback(() => {
db.noteHistory
.get(note.id)
.sorted({
@@ -137,17 +143,56 @@ export default function NoteHistory({
});
}, [note.id]);
useEffect(() => {
refresh();
}, [refresh]);
const renderItem = useCallback(
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} note={note} />
<HistoryItem
index={index}
items={history}
note={note}
refresh={refresh}
/>
),
[history, note]
[history, note, refresh]
);
return (
<View>
<SheetProvider context="note_history" />
<DialogHeader title={strings.noteHistory()} padding={12} />
<Dialog context="local" />
<DialogHeader
title={strings.noteHistory()}
padding={12}
button={
history?.placeholders.length
? {
title: strings.clearHistory(),
type: "secondary",
onPress: () => {
presentDialog({
title: strings.clearHistory(),
paragraph: strings.clearHistoryConfirmation(),
positiveText: strings.clear(),
negativeText: strings.cancel(),
positiveType: "errorShade",
context: "local",
positivePress: async () => {
await db.noteHistory.clearSessions(note.id);
fwdRef.current?.hide();
ToastManager.show({
heading: strings.historyCleared(),
type: "success"
});
}
});
}
}
: undefined
}
/>
<Seperator />

View File

@@ -53,7 +53,8 @@ import {
export default function NotePreview({
session,
content,
note
note,
update
}: {
session?: HistorySession & { session: string };
content:
@@ -64,6 +65,7 @@ export default function NotePreview({
>
| undefined;
note: TrashOrItem<Note>;
update?: () => void;
}) {
const { colors } = useThemeColors();
const [locked, setLocked] = useState(false);
@@ -107,24 +109,35 @@ export default function NotePreview({
}, [note]);
const deleteNote = async () => {
const isSession = !!session;
presentDialog({
title: strings.deleteNote(),
paragraph: strings.deleteNoteConfirmation(),
title: isSession ? strings.deleteVersion() : strings.deleteNote(),
paragraph: isSession
? strings.deleteVersionConfirmation()
: strings.deleteNoteConfirmation(),
positiveText: strings.delete(),
negativeText: strings.cancel(),
context: "local",
context: "preview",
positivePress: async () => {
if (note) {
if (isSession) {
await db.noteHistory.remove(session.id);
update?.();
eSendEvent(eCloseSheet, "note_history");
Navigation.queueRoutesForUpdate();
} else if (note.type === "trash") {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode();
ToastManager.show({
heading: strings.noteDeleted(),
type: "success",
context: "local"
});
eSendEvent(eCloseSheet);
}
ToastManager.show({
heading: isSession ? strings.versionDeleted() : strings.noteDeleted(),
type: "success",
context: isSession ? "local" : "global"
});
return true;
},
positiveType: "error"
});
@@ -137,7 +150,7 @@ export default function NotePreview({
width: "100%"
}}
>
<Dialog context="local" />
<Dialog context="preview" />
<DialogHeader
padding={12}
title={content?.title || note.title || session?.session}

View File

@@ -112,7 +112,8 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
}
setStep(Steps.buy);
}
}, [pricingPlans, routeParams.state]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [routeParams.state]);
useEffect(() => {
let listener: NativeEventSubscription;

View File

@@ -170,147 +170,147 @@ export const SelectionHeader = React.memo(
{!selectedItemsList.length
? null
: (focusedRouteId === "Monographs"
? [
{
title: strings.unpublish(),
onPress: () => {
presentDialog({
title: strings.doActions.unpublish.note(
selectedItemsList.length
),
positiveText: strings.unpublish(),
negativeText: strings.cancel(),
positivePress: async () => {
for (const id of selectedItemsList) {
await db.monographs.unpublish(id);
}
Navigation.queueRoutesForUpdate();
clearSelection();
},
positiveType: "errorShade"
});
},
visible: true,
icon: "delete"
}
]
: [
{
title: strings.manageTags(),
onPress: async () => {
ManageTags.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: strings.export(),
onPress: async () => {
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: strings.addToNotebook(),
onPress: async () => {
Navigation.navigate("LinkNotebooks", {
noteIds: selectedItemsList
});
},
visible: type === "note",
icon: "plus"
},
{
title: strings.unlinkNotebook(),
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(
id,
...selectedItemsList
);
updateNotebook(id);
? [
{
title: strings.unpublish(),
onPress: () => {
presentDialog({
title: strings.doActions.unpublish.note(
selectedItemsList.length
),
positiveText: strings.unpublish(),
negativeText: strings.cancel(),
positivePress: async () => {
for (const id of selectedItemsList) {
await db.monographs.unpublish(id);
}
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: strings.unfavorite(),
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" &&
type !== "note" &&
type !== "notebook",
icon: "delete"
},
{
title: strings.restore(),
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: strings.delete(),
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
]
).map((item) =>
!item.visible ? null : (
<IconButton
size={AppFontSize.lg}
type="plain"
tooltipText={item.title}
tooltipPosition={NativeTooltip.POSITIONS.TOP}
testID={`select-${item.icon}`}
name={item.icon}
key={item.title}
color={colors.primary.icon}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
positiveType: "errorShade"
});
},
visible: true,
icon: "delete"
}
]
: [
{
title: strings.manageTags(),
onPress: async () => {
ManageTags.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: strings.export(),
onPress: async () => {
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: strings.addToNotebook(),
onPress: async () => {
Navigation.navigate("LinkNotebooks", {
noteIds: selectedItemsList
});
},
visible: type === "note",
icon: "plus"
},
{
title: strings.unlinkNotebook(),
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(
id,
...selectedItemsList
);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: strings.unfavorite(),
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" &&
type !== "note" &&
type !== "notebook",
icon: "delete"
},
{
title: strings.restore(),
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: strings.delete(),
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
]
).map((item) =>
!item.visible ? null : (
<IconButton
size={AppFontSize.lg}
type="plain"
tooltipText={item.title}
tooltipPosition={NativeTooltip.POSITIONS.TOP}
testID={`select-${item.icon}`}
name={item.icon}
key={item.title}
color={colors.primary.icon}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
</View>
<IconButton

View File

@@ -28,7 +28,7 @@ import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { ActivityIndicator, TextInput, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
@@ -40,6 +40,8 @@ import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import Navigation from "../../../services/navigation";
import { useUserStore } from "../../../stores/use-user-store";
const ListNoteItem = ({
id,
@@ -118,8 +120,8 @@ const ListBlockItem = ({
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: !item.content || item.content.trim() === ""
? strings.linkNoteEmptyBlock()
: item.content}
? strings.linkNoteEmptyBlock()
: item.content}
</Paragraph>
<View
@@ -158,6 +160,7 @@ export default function LinkNote(props: {
const [selectedNote, setSelectedNote] = useState<Note>();
const [selectedNodeId, setSelectedNodeId] = useState<string>();
const [blocksLoading, setBlocksLoading] = useState(false);
useEffect(() => {
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
@@ -206,10 +209,12 @@ export default function LinkNote(props: {
const onSelectNote = async (note: Note) => {
setSelectedNote(note);
setBlocksLoading(true);
inputRef.current?.clear();
setTimeout(async () => {
nodesRef.current = await db.notes.contentBlocks(note.id);
setNodes(nodesRef.current);
setBlocksLoading(false);
});
// Fetch and set note's nodes.
};
@@ -224,6 +229,7 @@ export default function LinkNote(props: {
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingBottom: DefaultAppStyles.GAP,
minHeight: "100%",
maxHeight: "100%"
}}
@@ -330,28 +336,42 @@ export default function LinkNote(props: {
windowSize={3}
keyExtractor={(item) => item.id}
ListEmptyComponent={
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: colors.secondary.background,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius,
borderWidth: 0.5,
borderColor: colors.secondary.border,
alignItems: "center"
}}
>
<Paragraph color={colors.secondary.paragraph}>
{blockLinking?.error}
</Paragraph>
<Button
title={strings.upgradePlan()}
!blockLinking || blocksLoading ? (
<ActivityIndicator size={25} color={colors.primary.accent} />
) : !blockLinking.isAllowed ? (
<View
style={{
width: "100%"
gap: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: colors.secondary.background,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius,
borderWidth: 0.5,
borderColor: colors.secondary.border,
alignItems: "center"
}}
type="accent"
/>
</View>
>
<Paragraph color={colors.secondary.paragraph}>
{blockLinking?.error}
</Paragraph>
<Button
title={strings.upgradePlan()}
style={{
width: "100%"
}}
type="accent"
onPress={() => {
Navigation.navigate("PayWall", {
context: useUserStore.getState().user
? "logged-in"
: "logged-out",
canGoBack: true
});
props.close?.();
}}
/>
</View>
) : null
}
data={blockLinking?.isAllowed ? nodes : []}
/>

View File

@@ -30,6 +30,7 @@ import { Pressable } from "../ui/pressable";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import { useSideBarDraggingStore } from "./dragging-store";
import SyncStatusButton from "./sync-status-button";
const SettingsIcon = () => {
const { colors } = useThemeColors();
@@ -123,7 +124,7 @@ export const SideMenuHeader = (props: { rightButtons?: IconButtonProps[] }) => {
size={AppFontSize.lg}
/>
))}
<SyncStatusButton />
<SettingsIcon />
</View>
</View>

View File

@@ -0,0 +1,161 @@
/*
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 { useTimeAgo } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo";
import React from "react";
import { View } from "react-native";
import Animated, {
Easing,
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming
} from "react-native-reanimated";
import Sync from "../../services/sync";
import { SyncStatus, useUserStore } from "../../stores/use-user-store";
import { AppFontSize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import AppIcon from "../ui/AppIcon";
import { IconButton } from "../ui/icon-button";
const SyncStatusButton = () => {
const { colors } = useThemeColors();
const [user, syncing, lastSyncStatus, lastSynced, isLoggingOut] =
useUserStore((state) => [
state.user,
state.syncing,
state.lastSyncStatus,
state.lastSynced,
state.isLoggingOut
]);
const { isInternetReachable } = useNetInfo();
const isOffline = !isInternetReachable;
const hasSyncedBefore = lastSynced && lastSynced !== "Never";
const isFailed = lastSyncStatus === SyncStatus.Failed;
const isSynced = lastSyncStatus === SyncStatus.Passed;
const lastSyncedTimeAgo = useTimeAgo(lastSynced, {
interval: 5000,
live: true
});
const getIconColor = (): string => {
if (syncing) return colors.primary.accent;
if (isSynced) return colors.primary.accent;
return colors.secondary.icon;
};
const rotation = useSharedValue(0);
React.useEffect(() => {
if (syncing && !isOffline) {
rotation.value = 0;
rotation.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
-1,
false
);
} else {
cancelAnimation(rotation);
rotation.value = withTiming(360, {
duration: 1000 * (1 - (rotation.value % 360) / 360),
easing: Easing.linear
});
}
}, [syncing, rotation, isOffline]);
const rotationStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }]
}));
const tooltipText = React.useMemo(() => {
const offlineSuffix = isOffline ? ` (${strings.offline()})` : "";
if (syncing) return strings.syncing();
if (!hasSyncedBefore) return `${strings.never()}${offlineSuffix}`;
if (isFailed) return `${strings.syncFailed()}${offlineSuffix}`;
return `${strings.synced()}${lastSyncedTimeAgo}${offlineSuffix ? `${offlineSuffix}` : ""}`;
}, [isOffline, syncing, hasSyncedBefore, isFailed, lastSyncedTimeAgo]);
const onPress = () => {
if (syncing) return;
Sync.run();
};
if (!user || isLoggingOut) return null;
return (
<View
style={{
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
{syncing ? (
<View
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border,
justifyContent: "center",
alignItems: "center"
}}
>
<Animated.View style={rotationStyle}>
<AppIcon name="sync" size={AppFontSize.lg} color={getIconColor()} />
</Animated.View>
</View>
) : (
<IconButton
name="sync"
onPress={onPress}
tooltipText={tooltipText}
tooltipPosition={NativeTooltip.POSITIONS.BOTTOM}
size={AppFontSize.lg}
color={getIconColor()}
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border
}}
/>
)}
{!syncing && (isFailed || isOffline) ? (
<AppIcon
name="information"
color={isOffline ? colors.static.yellow : colors.error.icon}
size={AppFontSize.xxs}
style={{ position: "absolute", bottom: -3, right: -3 }}
/>
) : null}
</View>
);
};
export default SyncStatusButton;

View File

@@ -345,6 +345,7 @@ const onLogout = async (reason: string) => {
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
useUserStore.getState().setIsLoggingOut(false);
eSendEvent(eAfterSync);
};

View File

@@ -447,7 +447,9 @@ const onChangeTab = async (event: { i: number; from: number }) => {
if (event.i === 2) {
editorState().movedAway = false;
editorState().isFocused = true;
activateKeepAwake();
if (useSettingStore.getState().settings.keepScreenOn) {
activateKeepAwake();
}
eSendEvent(eOnEnterEditor);
if (

View File

@@ -35,7 +35,10 @@ import Paragraph from "../../components/ui/typography/paragraph";
import { useDBItem } from "../../hooks/use-db-item";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import { createItemSelectionStore } from "../../stores/item-selection-store";
import {
createItemSelectionStore,
SelectionState
} from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
@@ -47,14 +50,12 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
const currentNotebook = props.route.params.notebook;
const inputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const selectionCount = useItemSelectionStore(
(state) =>
Object.keys(state.selection).filter(
(k) =>
state.selection?.[k] === "selected" ||
state.selection[k] === "deselected"
)?.length > 0
);
const hasPendingChanges = useItemSelectionStore((state) => {
return Object.keys(state.selection).some((id) => {
return state.selection[id] !== state.initialState[id];
});
});
useNavigationFocus(props.navigation, { focusOnInit: true });
@@ -76,20 +77,6 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
.catch(() => {
setLoading(false);
});
db.relations
.from(currentNotebook, "note")
.get()
.then((existingNotes) => {
const selection: { [name: string]: any } = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection: selection,
initialState: selection
});
});
},
[currentNotebook]
);
@@ -97,6 +84,25 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
useEffect(() => {
loadNotes();
const loadSelection = async () => {
const existingNotes = await db.relations
.from(currentNotebook, "note")
.get();
const selection: Record<string, SelectionState> = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection,
initialState: selection
});
};
loadSelection();
return () => {
useItemSelectionStore.getState().reset();
};
@@ -159,6 +165,8 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
/>
<FlatList
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View
style={{
@@ -176,17 +184,19 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
)}
</View>
}
style={{
flexGrow: 1
style={{ flex: 1 }}
contentContainerStyle={{
paddingBottom: hasPendingChanges ? 80 : 0
}}
data={loading ? [] : notes?.placeholders}
renderItem={renderItem}
/>
{selectionCount ? (
{hasPendingChanges ? (
<FloatingButton
icon="check"
alwaysVisible
hideOnKeyboard={false}
onPress={async () => {
await db.notes?.addToNotebook(
currentNotebook.id,

View File

@@ -28,6 +28,7 @@ import {
} from "../../components/dialogs/progress";
import Navigation from "../../services/navigation";
import BackupService from "../../services/backup";
import { useUserStore } from "../../stores/use-user-store";
export async function logoutUser() {
const hasUnsyncedChanges = await db.hasUnsyncedChanges();
@@ -47,6 +48,7 @@ export async function logoutUser() {
: undefined,
positivePress: async (_, takeBackup) => {
eSendEvent(eCloseSimpleDialog);
useUserStore.getState().setIsLoggingOut(true);
setTimeout(async () => {
try {
startProgress({
@@ -104,6 +106,7 @@ export async function logoutUser() {
DatabaseLogger.error(e);
ToastManager.error(e as Error, strings.logoutError());
endProgress();
useUserStore.getState().setIsLoggingOut(false);
}
}, 300);
}

View File

@@ -210,7 +210,7 @@ export const settingsGroups: SettingSection[] = [
: status === SubscriptionStatus.ACTIVE
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
@@ -965,6 +965,14 @@ export const settingsGroups: SettingSection[] = [
property: "checkForUpdates",
icon: "update"
},
{
id: "keep-screen-on",
type: "switch",
name: strings.keepScreenOn(),
description: strings.keepScreenOnDesc(),
property: "keepScreenOn",
icon: "cellphone-screenshot",
},
{
id: "image-compression",
type: "component",

View File

@@ -35,7 +35,7 @@ import notifee, {
import NetInfo from "@react-native-community/netinfo";
import dayjs, { Dayjs } from "dayjs";
import { encodeNonAsciiHTML } from "entities";
import { Platform } from "react-native";
import { AppState, Platform } from "react-native";
import { db, setupDatabase } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import { presentDialog } from "../components/dialog/functions";
@@ -45,7 +45,7 @@ import { useRelationStore } from "../stores/use-relation-store";
import { useReminderStore } from "../stores/use-reminder-store";
import { useSettingStore } from "../stores/use-setting-store";
import { useUserStore } from "../stores/use-user-store";
import { eOnLoadNote } from "../utils/events";
import { eCloseSimpleDialog, eOnLoadNote } from "../utils/events";
import { fluidTabsRef } from "../utils/global-refs";
import { convertNoteToText } from "../utils/note-to-text";
import { NotesnookModule } from "../utils/notesnook-module";
@@ -577,7 +577,7 @@ async function displayNotification({
}
function openSettingsDialog(context: string) {
return new Promise((resolve) => {
return new Promise<boolean>((resolve) => {
presentDialog({
title: strings.notificationsDisabled(),
paragraph: strings.notificationsDisabledDesc(),
@@ -597,49 +597,94 @@ function openSettingsDialog(context: string) {
});
}
function waitForAppFocusAfterSettings(timeoutMs = 120000): Promise<void> {
return new Promise((resolve) => {
let didLeaveApp = AppState.currentState !== "active";
const timeout = setTimeout(() => {
subscription.remove();
resolve();
}, timeoutMs);
const subscription = AppState.addEventListener("change", (state) => {
if (state !== "active") {
didLeaveApp = true;
return;
}
if (didLeaveApp && state === "active") {
clearTimeout(timeout);
subscription.remove();
resolve();
}
});
});
}
async function checkAndRequestPermissions(
promptUser?: boolean
): Promise<boolean> {
let permissionStatus = await notifee.getNotificationSettings();
if (Platform.OS === "android") {
if (
permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED &&
permissionStatus.android.alarm === 1
)
const hasNotificationPermission =
permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED;
const hasAlarmPermission = permissionStatus.android.alarm === 1;
if (hasNotificationPermission && hasAlarmPermission) {
return true;
}
if (permissionStatus.authorizationStatus === AuthorizationStatus.DENIED) {
permissionStatus = await notifee.requestPermission();
}
// Wait for user to return from exact alarm settings
if (permissionStatus.android.alarm !== 1) {
const waitForAppFocus = waitForAppFocusAfterSettings();
await notifee.openAlarmPermissionSettings();
await waitForAppFocus;
}
permissionStatus = await notifee.getNotificationSettings();
if (
const authorized =
permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED &&
permissionStatus.android.alarm === 1
)
return true;
permissionStatus.android.alarm === 1;
if (promptUser) {
if (await openSettingsDialog("local")) {
await notifee.openNotificationSettings();
return false;
}
if (authorized) {
return true;
}
return false;
} else {
permissionStatus = await notifee.requestPermission();
if (permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED)
return true;
if (promptUser) {
await openSettingsDialog("local");
const openSettings = await openSettingsDialog("local");
if (openSettings) {
const waitForAppFocus = waitForAppFocusAfterSettings();
await notifee.openNotificationSettings();
await waitForAppFocus;
permissionStatus = await notifee.getNotificationSettings();
const finalAuthorized =
permissionStatus.authorizationStatus ===
AuthorizationStatus.AUTHORIZED &&
permissionStatus.android.alarm === 1;
if (finalAuthorized) {
eSendEvent(eCloseSimpleDialog);
return true;
}
}
return false;
}
return false;
}
}
// iOS
permissionStatus = await notifee.requestPermission();
if (permissionStatus.authorizationStatus === AuthorizationStatus.AUTHORIZED) {
return true;
}
if (promptUser) {
await openSettingsDialog("local");
}
return false;
}
async function getTriggers(
reminder: Reminder
): Promise<(Trigger & { id: string })[] | undefined> {

View File

@@ -63,6 +63,11 @@ const run = async (
clearTimeout(syncTimer);
syncTimer = setTimeout(async () => {
if (useUserStore.getState().isLoggingOut) {
DatabaseLogger.info("Sync skipped — user is logging out");
return;
}
const userstore = useUserStore.getState();
userstore.setSyncing(true);
const user = await db.user.getUser();

View File

@@ -56,17 +56,14 @@ export function createItemSelectionStore(
canEnableMultiSelectMode: multiSelectMode,
initialState: {},
markAs: (item, state) => {
set({
selection: {
...get().selection,
[item.id]:
state === "deselected"
? get().initialState === undefined
? undefined
: "deselected"
: state
}
});
const selection = { ...get().selection };
const initial = get().initialState[item.id];
if (state === "deselected" && initial === undefined) {
delete selection[item.id];
} else {
selection[item.id] = state;
}
set({ selection });
},
multiSelect: false,
toggleMultiSelect: () => {

View File

@@ -104,6 +104,7 @@ export type Settings = {
checkForUpdates?: boolean;
defaultLineHeight: number;
imageCompression: "ask-every-time" | "enabled" | "disabled";
keepScreenOn?: boolean;
};
type DimensionsType = {
@@ -213,7 +214,8 @@ export const defaultSettings: SettingStore["settings"] = {
lastFullBackupDate: 0,
checkForUpdates: true,
defaultLineHeight: EDITOR_LINE_HEIGHT.DEFAULT,
imageCompression: "ask-every-time"
imageCompression: "ask-every-time",
keepScreenOn: true
};
export const useSettingStore = create<SettingStore>((set, get) => ({

View File

@@ -48,6 +48,8 @@ export interface UserStore {
disableAppLockRequests: boolean;
setDisableAppLockRequests: (disableAppLockRequests: boolean) => void;
profile?: Partial<Profile>;
isLoggingOut: boolean;
setIsLoggingOut: (value: boolean) => void;
}
export const useUserStore = create<UserStore>((set) => ({
@@ -95,5 +97,7 @@ export const useUserStore = create<UserStore>((set) => ({
set({ disableAppLockRequests: false });
}, 1000);
},
profile: undefined
profile: undefined,
isLoggingOut: false,
setIsLoggingOut: (value) => set({ isLoggingOut: value })
}));

View File

@@ -5,6 +5,8 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip
require_relative 'scripts/patch_fmt_consteval'
platform :ios, min_ios_version_supported
prepare_react_native_project!
@@ -66,6 +68,10 @@ post_install do |installer|
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)
# Keep fmt buildable on Xcode >= 26.2. See ios/scripts/patch_fmt_consteval.rb.
PatchFmtConsteval.apply!(installer.sandbox.root)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2189
IOS_MARKETING_VERSION = 3.4.5
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2189
IOS_MARKETING_VERSION = 3.4.5
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Staging iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2189
IOS_MARKETING_VERSION = 3.4.5
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -0,0 +1,77 @@
# Xcode >= 26.2 rejects fmt's compile-time format-string check with
#
# call to consteval function 'fmt::fstring<...>::fstring<char[N]>' is not a
# constant expression
#
# React Native 0.81/0.82 vendor fmt 11.0.2, which hits this. It is an upstream
# incompatibility (reproducible in a stock RN app), but left alone it makes those
# RN versions permanently un-buildable on a modern Xcode.
#
# fmt's own escape hatch is FMT_USE_CONSTEVAL: 0 downgrades the format-string
# check from compile-time to run-time. fmt 11.0.2 does not guard its detection
# block with #ifndef, so we cannot simply predefine the macro -- and doing it
# through the build settings is worse anyway:
#
# * a command-line GCC_PREPROCESSOR_DEFINITIONS outranks every per-target
# value, silently dropping COCOAPODS=1, RCT_METRO_PORT, ...
# * a second `post_install` block in the Podfile REPLACES React Native's own.
#
# So we patch the header itself, right after fmt has made up its mind and before
# the first use of the macro. Idempotent, and safe to run on every pod install.
module PatchFmtConsteval
MARKER = 'NOTESNOOK_FMT_CONSTEVAL_PATCH'.freeze
# The line that first consumes the macro; our override goes immediately above
# it, i.e. after the whole detection cascade.
ANCHOR = "#if FMT_USE_CONSTEVAL\n".freeze
OVERRIDE = <<~PATCH.freeze
// #{MARKER}: Xcode >= 26.2 rejects fmt's consteval format-string check
// (see ios/scripts/patch_fmt_consteval.rb). Applied automatically by
// `pod install`; downgrades the check to run-time.
#undef FMT_USE_CONSTEVAL
#define FMT_USE_CONSTEVAL 0
PATCH
# pods_root: the Pods directory (installer.sandbox.root).
def self.apply!(pods_root)
header = File.join(pods_root.to_s, 'fmt', 'include', 'fmt', 'base.h')
unless File.exist?(header)
Pod::UI.warn "fmt: #{header} not found, skipping consteval patch."
return
end
contents = File.read(header)
if contents.include?(MARKER)
Pod::UI.puts 'fmt: consteval patch already applied.'
return
end
index = contents.index(ANCHOR)
if index.nil?
Pod::UI.warn 'fmt: could not find `#if FMT_USE_CONSTEVAL` in base.h; ' \
'the consteval patch was NOT applied. If this fmt version ' \
'still uses a consteval format-string check, builds on ' \
'Xcode >= 26.2 will fail -- update ' \
'ios/scripts/patch_fmt_consteval.rb.'
return
end
contents.insert(index, OVERRIDE)
# CocoaPods checks pod sources out read-only (0444), so make the header
# writable for the write and restore the original mode afterwards.
mode = File.stat(header).mode & 0o7777
begin
File.chmod(mode | 0o200, header)
File.write(header, contents)
ensure
File.chmod(mode, header)
end
Pod::UI.puts 'fmt: patched base.h to set FMT_USE_CONSTEVAL=0 (Xcode >= 26.2 compatibility).'
end
end

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.4.0-beta.1",
"version": "3.4.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.4.0-beta.1",
"version": "3.4.5",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -290,7 +290,7 @@
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook-importer/core": "^2.2.5",
"@notesnook-importer/core": "^2.4.5",
"@notesnook/common": "file:../common",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.4.5",
"version": "3.4.7",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {

View File

@@ -128,7 +128,8 @@ const EXTRA_ICON_NAMES = [
"identifier",
"image-area",
"clock-outline",
"delete-sweep-outline"
"delete-sweep-outline",
"sync"
];
const __filename = fileURLToPath(import.meta.url);

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/vericrypt",
"version": "1.3.0",
"version": "1.4.0",
"private": true,
"devDependencies": {
"@types/platform": "^1.3.6",
@@ -43,4 +43,4 @@
"last 1 safari version"
]
}
}
}

View File

@@ -31,7 +31,7 @@ import { PasteEncryptedData, SyncRequestBody } from "./components/step-4";
import { StepSeperator } from "./components/step-seperator";
import { Footer } from "./components/footer";
import { useState } from "react";
import { NNCrypto } from "@notesnook/crypto";
import { NNCrypto, Cipher } from "@notesnook/crypto";
import { Code } from "./components/code";
import { Accordion } from "./components/accordion";
import { DecryptedResult } from "./components/decrypted-result";
@@ -41,7 +41,7 @@ const instructions = [
"Go to Notesnook",
"Open Settings",
<>
Click on <Code text="Backup data recovery key" />
Click on <Code text="Save data recovery key" />
</>,
"Enter your account password for verification",
"Confirm that your generated encryption key matches"
@@ -52,6 +52,7 @@ function App() {
const [salt, setSalt] = useState<string>();
const [key, setKey] = useState<string>();
const [data, setData] = useState<SyncRequestBody | undefined>();
const [accountDataKey, setAccountDataKey] = useState<Cipher<"base64"> | null | undefined>();
const theme = useTheme({ accent: getDefaultAccentColor(), theme: "light" });
return (
@@ -67,7 +68,7 @@ function App() {
>
<LoginToNotesnook />
<StepSeperator />
<GetAccountSalt onSaltSubmitted={setSalt} />
<GetAccountSalt onSaltSubmitted={setSalt} setAccountDataKey={setAccountDataKey} />
{salt && (
<>
@@ -91,7 +92,7 @@ function App() {
return true;
}}
popup={{
title: "Your data encryption key",
title: "Your master encryption key",
body: key ? (
<>
<Text
@@ -146,11 +147,13 @@ function App() {
password={password}
salt={salt}
data={data}
accountKey={accountDataKey}
onRestartProcess={() => {
setSalt(undefined);
setPassword(undefined);
setKey(undefined);
setData(undefined);
setAccountDataKey(undefined);
}}
/>
</>

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Flex, Button, Text, Link } from "@theme-ui/components";
import { StepContainer } from "./step-container";
import { SyncRequestBody } from "./step-4";
import { NNCrypto } from "@notesnook/crypto";
import { NNCrypto, Cipher } from "@notesnook/crypto";
import { useEffect, useState } from "react";
import { FcDataEncryption } from "react-icons/fc";
import { Code } from "./code";
@@ -32,6 +32,7 @@ type DecryptedResultProps = {
password: string;
salt: string;
data: SyncRequestBody;
accountKey: Cipher<"base64"> | null | undefined;
onRestartProcess: () => void;
};
@@ -51,10 +52,19 @@ export function DecryptedResult(props: DecryptedResultProps) {
};
const crypto = new NNCrypto();
const key = await crypto.exportKey(props.password, props.salt);
let encryptionKey = undefined;
if (props.accountKey != null){
// Need to decrypt the account key
props.accountKey.format = "base64"; // account keys don't have the format set, but they're base64.
const dataEncryptionKey = JSON.parse(await crypto.decrypt(key, props.accountKey, "text"))
encryptionKey = dataEncryptionKey;
} else {
encryptionKey = key;
}
for (const arrayKey in data) {
const array = data[arrayKey];
for (const encryptedItem of (props.data as any)[arrayKey]) {
const data = await crypto.decrypt(key, encryptedItem, "text");
const data = await crypto.decrypt(encryptionKey, encryptedItem, "text");
array.push(JSON.parse(data));
}
}
@@ -150,19 +160,9 @@ export function DecryptedResult(props: DecryptedResultProps) {
support@streetwriters.co
</Link>{" "}
or{" "}
<Link href="https://discord.gg/">joining our Discord community</Link>.
<Link href="https://go.notesnook.com/discord">join our Discord community</Link>.
We&apos;ll do our best to alleviate all your worries.
</Text>
<Text as="p" variant="body" sx={{ mx: 2, mt: 2, fontWeight: "bold" }}>
What about open sourcing Notesnook?
</Text>
<Text as="p" variant="body" sx={{ mx: 2, my: 2 }}>
Open sourcing is another part of garnering our users&apos; trust. We
have <Link href="https://notesnook.com/roadmap">plans</Link> to begin
open sourcing in May but open sourcing will not make this tool
obsolete. Verifying the integrity of encrypted data at any point in
time is very important even if the software is open source.
</Text>
</Accordion>
<Flex sx={{ alignSelf: "center", mt: 4 }}>
<Button

View File

@@ -38,11 +38,6 @@ export function LoginToNotesnook() {
>
<Text variant="title">Welcome to Vericrypt</Text>
<Text as="p" variant="body" sx={{ mt: 1 }}>
<del>
Trust is a huge problem in closed source end-to-end encrypted
applications. How can you be sure that the app is actually encrypting
your data?
</del>
</Text>
<Text as="p" variant="body" sx={{ mt: 1 }}>
The only way to earn a user&apos;s trust is by allowing them to see how
@@ -56,9 +51,10 @@ export function LoginToNotesnook() {
</Link>
</Text>
<Text as="p" variant="body" sx={{ mt: 1 }}>
Yes, that&apos;s right. Notesnook is now 100% open source under the
GPLv3 license. That includes the app, the encryption library, the
backend server, and everything else.
Yes, that&apos;s right. Notesnook is 100% open source under the
GPLv3 license. That includes the app, the encryption library, and
everything else. We&apos;ve even open sourced the server, available under the
AGPLv3 license.
</Text>
<Text as="p" variant="body" sx={{ mt: 1 }}>
However, even with an open source app, how can you be sure that the app
@@ -86,7 +82,7 @@ export function LoginToNotesnook() {
sx={{ alignSelf: "center", mt: 2 }}
onClick={() => window.open("https://app.notesnook.com/login", "_blank")}
>
Login to Notesnook
Get Started by Logging in
</Button>
</StepContainer>
);

View File

@@ -16,11 +16,11 @@ 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 { Flex, Text, Image, Input } from "@theme-ui/components";
import { Flex, Text, Image, Textarea } from "@theme-ui/components";
import { StepContainer } from "./step-container";
import DevtoolsCopySaltChrome from "../assets/screenshots/devtools_copy_salt.png";
import DevtoolsCopyUsersChrome from "../assets/screenshots/devtools_copy_users.png";
import DevtoolsCopySaltFirefox from "../assets/screenshots/firefox/firefox_copy_salt.png";
import DevtoolsCopyUsersFirefox from "../assets/screenshots/firefox/firefox_copy_users.png";
import { Accordion } from "./accordion";
import { getCombo } from "../utils/keycombos";
@@ -29,9 +29,11 @@ import { KeyCombo } from "./key-combo";
import { Code } from "./code";
import { useState } from "react";
import { getSourceUrl } from "../utils/links";
import { Cipher } from "@notesnook/crypto";
type GetAccountSaltProps = {
onSaltSubmitted: (salt: string) => void;
setAccountDataKey: (accountDataKey: Cipher<"base64"> | null) => void;
};
const steps = {
@@ -52,10 +54,10 @@ const steps = {
</Text>,
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Follow the steps as shown in the image:</Text>
<Image src={DevtoolsCopySaltChrome} width={"100%"} sx={{ mt: 1 }} />
<Image src={DevtoolsCopyUsersChrome} width={"100%"} sx={{ mt: 1 }} />
</Flex>,
<Flex key="copy-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Copy the salt and paste it below.</Text>
<Text as="p">Copy everything in the response and paste it below.</Text>
</Flex>
],
firefox: [
@@ -75,10 +77,10 @@ const steps = {
</Text>,
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Follow the steps as shown in the image:</Text>
<Image src={DevtoolsCopySaltFirefox} width={"100%"} sx={{ mt: 1 }} />
<Image src={DevtoolsCopyUsersFirefox} width={"100%"} sx={{ mt: 1 }} />
</Flex>,
<Flex key="copy-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Copy the salt and paste it below.</Text>
<Text as="p">Copy everything in the response and paste it below.</Text>
</Flex>
]
};
@@ -114,14 +116,14 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
sx={{ flexDirection: "column" }}
>
<Flex sx={{ justifyContent: "space-between", alignItems: "center" }}>
<Text variant="title">Account salt</Text>
<Text variant="title">Account Key Data</Text>
<Code
text="src/components/step-2.tsx"
href={getSourceUrl("src/components/step-2.tsx")}
/>
</Flex>
<Accordion
title="How to get your account salt?"
title="How to get your key data?"
sx={{
border: "1px solid var(--border)",
mt: 2,
@@ -129,9 +131,9 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
}}
>
<Text variant="body" sx={{ mx: 2 }}>
We&apos;ll be extracting your account&apos;s salt right from
You&apos;ll be extracting your account&apos;s keys right from
Notesnook&apos;s local database that lives in your web browser. So put
on your seat belt and let&apos;s get some salt!
on your seat belt and let&apos;s retrieve those keys.
</Text>
<Text as="ol" variant="body" sx={{ mb: 2 }}>
{instructions?.map((item, index) => (
@@ -141,12 +143,11 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
))}
</Text>
</Accordion>
<Input
<Textarea
variant="forms.clean"
id="salt"
name="salt"
type="text"
placeholder="Enter your account salt"
id="account-data"
name="account-data"
placeholder="Paste the response here"
sx={{
mt: 2,
fontSize: "subheading",
@@ -162,13 +163,21 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
spellCheck={false}
onChange={(e) => {
setIsSaltValid(undefined);
/*
If there is no dataEncryptionKey, try reading the legacyDataEncryptionKey.
If there is a legacyDataEncryptionKey, use this one.
If there is no legacyDataEncryptionKey, it is a legacy account (pre DEK update) that has not
changed passwords. (Use master key instead.)
*/
try {
const value = e.target.value;
const isValid = Buffer.from(value, "base64").length === 16;
const value = JSON.parse(e.target.value);
props.setAccountDataKey(value.dataEncryptionKey ?? value.legacyDataEncryptionKey ?? null);
const salt = value.salt;
const isValid = Buffer.from(salt, "base64").length === 16;
if (!isValid) setIsSaltValid(false);
else {
setIsSaltValid(true);
props.onSaltSubmitted(value);
props.onSaltSubmitted(salt);
}
} catch (e) {
console.error(e);

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -252,16 +252,6 @@ const MemoizedEditorView = React.memo(EditorView, (prev, next) => {
prev.session.activeSearchResultId === next.session.activeSearchResultId &&
prev.session.nonce === next.session.nonce;
if (
"attachmentsLength" in prev.session &&
"attachmentsLength" in next.session
) {
return (
baseConditions &&
prev.session.attachmentsLength === next.session.attachmentsLength
);
}
if ("note" in prev.session && "note" in next.session) {
return (
baseConditions &&

View File

@@ -684,6 +684,12 @@ function TiptapWrapper(
}
}, [props.spellcheck]);
useEffect(() => {
if (editorContainerRef.current) {
editorContainerRef.current.style.fontSize = `${editorConfig.fontSize}px`;
}
}, [editorConfig.fontSize]);
return (
<Flex
ref={containerRef}

View File

@@ -31,6 +31,7 @@ import { getFormattedDate } from "@notesnook/common";
import { useStore as useThemeStore } from "../stores/theme-store";
import { isCipher } from "@notesnook/core";
import { attachFiles } from "../components/editor/picker";
import { BaseAttachment } from "@notesnook/editor";
export class WebExtensionServer implements Server {
async login() {
@@ -88,11 +89,16 @@ export class WebExtensionServer implements Server {
}
);
let attachment;
for await (const message of attachFiles([clippedFile])) {
if (message.type === "done") attachment = message.attachment;
else if (message.type === "error") return;
}
let attachment: BaseAttachment | undefined;
await attachFiles([clippedFile], (message) => {
switch (message.type) {
case ("done"):
attachment = message.attachment;
break;
case ("error"):
return;
}
})
if (!attachment) return;
clipContent += h("iframe", [], {
@@ -116,14 +122,14 @@ export class WebExtensionServer implements Server {
content +=
clip.mode === "bookmark"
? h("div", [
h("p", [`Date bookmarked: ${getFormattedDate(Date.now())}`]),
h("hr")
]).innerHTML
h("p", [`Date bookmarked: ${getFormattedDate(Date.now())}`]),
h("hr")
]).innerHTML
: h("div", [
h("hr"),
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
h("p", [`Date clipped: ${getFormattedDate(Date.now())}`])
]).innerHTML;
h("hr"),
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
h("p", [`Date clipped: ${getFormattedDate(Date.now())}`])
]).innerHTML;
const id = await db.notes.add({
id: note?.id,

View File

@@ -40,7 +40,7 @@ import AuthContainer from "../components/auth-container";
import { useTimer } from "../hooks/use-timer";
import { ErrorText } from "../components/error-text";
import { AuthenticatorType, User } from "@notesnook/core";
import { showLogoutConfirmation } from "../dialogs/confirm";
import { ConfirmDialog, showLogoutConfirmation } from "../dialogs/confirm";
import { TaskManager } from "../common/task-manager";
import { strings } from "@notesnook/intl";
import { ScrollContainer } from "@notesnook/ui";
@@ -907,10 +907,19 @@ export function AuthForm<T extends AuthRoutes>(props: AuthFormProps<T>) {
sx={{
mt: 5,
color: "paragraph",
textDecoration: "none"
textDecoration: "none",
position: "absolute",
top: 0,
right: 5
}}
onClick={() => {
openURL("/notes/", { authenticated: false });
onClick={async () => {
const result = await ConfirmDialog.show({
title: strings.offlineMode(),
message: strings.offlineModeDesc(),
negativeButtonText: strings.cancel(),
positiveButtonText: strings.understand()
});
if (result) openURL("/notes/", { authenticated: false });
}}
>
{strings.skipAndGoToApp()}

View File

@@ -21,7 +21,7 @@ On all three platforms we use the same exact library for all cryptographic funct
> info Fun story
>
> When we first added encryption, we used AES-GCM-256 across platforms but the cross-platform compatbility was abyssmal. That is when I found out about the great libsodium. Written in C, wrappers available for all platforms...what more could I want?
> When we first added encryption, we used AES-GCM-256 across platforms but the cross-platform compatibility was abyssmal. That is when I found out about the great libsodium. Written in C, wrappers available for all platforms...what more could I want?
## Process
@@ -41,13 +41,7 @@ This process is repeated every time you sign in.
### 2. Key generation
After you are signed in, the app requests your user data which includes, among other things, your salt.
> info Salt generation
>
> When you create an account, the server generates a cryptographically secure random salt for you. This salt is used for key generation.
You password & salt is then used to derive a strong irreversible key using Argon2 as the password key derivation function (PKDF).
When you first sign up for an account, your client generates two encryption keys. One is a unique data encryption key that encrypts all your notes and other data. The second is your master encryption key, this is derived by your password and predictable salt. This key protects all your encryption keys, like the aforementioned data encryption key. If you change your password, your client will re-encrypt your existing data encryption key with your new master key.
### 3. Encryption key storage
@@ -55,7 +49,7 @@ You password & salt is then used to derive a strong irreversible key using Argon
Instead of storing the key as plain text (and allowing anyone to copy/move it), we use browser's `IndexedDB` to store the key as a `CryptoKey`.
`CryptoKey` is stored securely by the browser and cannot be exported, viewed, copied except by the app & browser.
`CryptoKey` is stored securely by the browser and cannot be exported, viewed, or copied except by the app & browser.
# [Mobile](#/tab/mobile)
@@ -65,12 +59,12 @@ On iOS and Android, the encryption key is stored in the phone's keychain.
### 4. Data encryption
Encryption only takes place when you sync. Each item in the database is encrypted seperately using XChaCha-Poly1305-IETF.
Encryption only takes place when you sync. Each item in the database is encrypted separately using XChaCha-Poly1305-IETF.
#### How it works
1. The item is read from the database as JSON object and stringified (i.e. converted to a string).
2. The string is encrypted using the encryption key generated earlier.
2. The string is encrypted using the data encryption key generated earlier.
3. The result is a JSON object which contains:
1. A base64 encoded `cipher`
2. A 192-bit nonce (`iv`)
@@ -83,3 +77,9 @@ Encryption only takes place when you sync. Each item in the database is encrypte
> See the whole process in action [here.](https://vericrypt.notesnook.com/)
This object is then sent to the server for storage. The server performs no further operation on this data (because it can't).
## Faqs
### I am an old user of Notesnook, I don't have a data encryption key.
Your data encryption key will be created when you change your password.

View File

@@ -2,7 +2,7 @@
> error This feature is Android only.
A simple, quick and convinient way to take notes on your phone from notifications. This works the same way as a messaging/chat app allows you to reply to messages from notifications.
A simple, quick and convenient way to take notes on your phone from notifications. This works the same way as a messaging/chat app allows you to reply to messages from notifications.
![Notes in notifications](/static/mobile-integration/android-quick-note-notifications.png)

View File

@@ -2,7 +2,7 @@
Basic web clipping support is available on both Android & iOS apps via share extensions.
We are going to use browser app as an example to demonstarte how you can clip information from webpages and other apps on your iPhone.
We are going to use browser app as an example to demonstrate how you can clip information from webpages and other apps on your iPhone.
1. Select some text on a webpage then Tap on "Share" on the context menu.
@@ -40,7 +40,7 @@ We are going to use browser app as an example to demonstarte how you can clip in
> info
>
> Share exenstion will save the note you selected so in future web clips, it will be selected by default until you reset it.
> Share extension will save the note you selected so in future web clips, it will be selected by default until you reset it.
## Clipping full webpage content

View File

@@ -29,7 +29,7 @@ The first step to recovering your account consists of requesting an account reco
5. On the next page, your email should be prefilled. If it isn't, fill it out.
![](/static/account-recovery/step-2.png)
6. Click on `Send recovery email`
7. If everything goes well, you should recieve an email from Notesnook in your inbox:
7. If everything goes well, you should receive an email from Notesnook in your inbox:
![](/static/account-recovery/recovery_email.png)
> info What if I didn't receive an email?
>

View File

@@ -0,0 +1,3 @@
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,4 @@
- Add option to clear note version history
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -231,6 +231,80 @@ test("delete vault and delete all locked notes", () =>
expect(await db.vaults.default()).toBeUndefined();
}));
test("delete all vaults and their locked notes", () =>
noteTest().then(async ({ db, id: note1Id }) => {
/**
* simulating the case where multiple vaults have been created
* and each vault has a locked note
*/
const key = {
format: "base64",
alg: "aes-256-gcm",
cipher: "key",
iv: "iv",
salt: "salt",
length: 16
};
const vault1Id = await db.vaults.add({
title: "Vault 1",
key
});
const vault2Id = await db.vaults.add({
title: "Vault 2",
key
});
await db.relations.add(
{
id: vault1Id,
type: "vault"
},
{
id: note1Id,
type: "note"
}
);
const note2Id = await db.notes.add(TEST_NOTE);
await db.relations.add(
{
id: vault2Id,
type: "vault"
},
{
id: note2Id,
type: "note"
}
);
await db.vault.delete(true);
expect(
await db.relations
.from(
{
id: vault1Id,
type: "vault"
},
"note"
)
.has(note1Id)
).toBe(false);
expect(
await db.relations
.from(
{
id: vault2Id,
type: "vault"
},
"note"
)
.has(note2Id)
).toBe(false);
expect(await db.notes.exists(note1Id)).toBe(false);
expect(await db.notes.exists(note2Id)).toBe(false);
expect(await db.vaults.default()).toBeUndefined();
expect(await db.vaults.all.count()).toBe(0);
}));
test("vault password is cleared after specified time", () =>
databaseTest().then(async (db) => {
await expect(db.vault.create("password")).resolves.toBe(true);

View File

@@ -0,0 +1,50 @@
/*
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 { databaseTest } from "./utils/index.js";
import { test, expect } from "vitest";
test("remove all vaults", () =>
databaseTest().then(async (db) => {
const key = {
format: "base64" as const,
alg: "aes-256-gcm",
cipher: "key",
iv: "iv",
salt: "salt",
length: 16
};
await db.vaults.add({
title: "Vault 1",
key
});
await db.vaults.add({
title: "Vault 2",
key
});
expect(await db.vaults.all.count()).toBe(2);
expect(await db.vaults.default()).toBeDefined();
await db.vaults.removeAll();
expect(await db.vaults.all.count()).toBe(0);
expect(await db.vaults.default()).toBeUndefined();
}));

View File

@@ -387,6 +387,10 @@ export class Sync {
const itemsToDecrypt = itemsByKeyVersion.get(keyInfo.version);
if (!itemsToDecrypt || itemsToDecrypt.length === 0) continue;
this.logger.info("Decrypting using key", {
keyInfo: keyInfo.version,
items: itemsToDecrypt.length
});
decrypted.push(
...(await this.db.storage().decryptMulti(keyInfo.key, itemsToDecrypt))
);

View File

@@ -404,11 +404,13 @@ class UserManager {
{ version: KeyVersion; key: SerializedKey }[] | undefined
> {
const masterKey = await this.getMasterKey();
logger.info("master key exists: ", { masterKey: !!masterKey });
if (!masterKey) return;
const dataEncryptionKey = await this.keyManager.get("dataEncryptionKey", {
refetchUser: false
});
logger.info("DEK exists: ", { dataEncryptionKey: !!dataEncryptionKey });
if (!dataEncryptionKey)
return [
{
@@ -424,6 +426,9 @@ class UserManager {
refetchUser: false
}
);
logger.info("legacy DEK exists: ", {
legacyDataEncryptionKey: !!legacyDataEncryptionKey
});
if (legacyDataEncryptionKey)
keys.push({
key: await this.keyManager.unwrapKey(
@@ -436,6 +441,10 @@ class UserManager {
key: await this.keyManager.unwrapKey(dataEncryptionKey, masterKey),
version: KEY_VERSION.DEK
});
logger.info("Keys:", {
keys: keys.length,
keyVersions: keys.map((k) => k.version)
});
return keys;
}

View File

@@ -161,17 +161,30 @@ export default class Vault {
}
}
/**
*
* There's an unintentional and unrelated bug where multiple vaults
* can be created.
* So when user triggers delete, we should delete all vaults.
*/
async delete(deleteAllLockedNotes = false) {
const vault = await this.db.vaults.default();
if (!vault) return;
const vaults = await this.db.vaults.all.items();
if (!vaults.length) return;
if (deleteAllLockedNotes) {
const relations = await this.db.relations.from(vault, "note").get();
const lockedIds = relations.map((r) => r.toId);
await this.db.notes.remove(...lockedIds);
const lockedIds = new Set<string>();
for (const vault of vaults) {
const relations = await this.db.relations.from(vault, "note").get();
for (const { toId } of relations) {
lockedIds.add(toId);
}
}
if (lockedIds.size) {
await this.db.notes.remove(...lockedIds);
}
}
await this.db.vaults.remove(vault.id);
await this.db.vaults.removeAll();
this.password = undefined;
}

View File

@@ -87,4 +87,11 @@ export class Vaults implements ICollection {
async itemExists(reference: ItemReference) {
return (await this.db.relations.to(reference, "vault").count()) > 0;
}
async removeAll() {
const vaults = await this.all.items();
for (const vault of vaults) {
await this.remove(vault.id);
}
}
}

View File

@@ -63,7 +63,7 @@
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook-importer/core": "^2.2.5",
"@notesnook-importer/core": "^2.4.5",
"@notesnook/common": "file:../common",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",

View File

@@ -53,7 +53,10 @@
"papaparse": "^5.5.3",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-view": "1.34.2",
"prosemirror-model": "1.25.11",
"prosemirror-state": "1.4.4",
"prosemirror-transform": "1.12.0",
"prosemirror-view": "1.42.2",
"re-resizable": "^6.9.18",
"react-colorful": "^5.6.1",
"redent": "^4.0.0",
@@ -4950,9 +4953,10 @@
}
},
"node_modules/prosemirror-model": {
"version": "1.22.3",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz",
"integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==",
"version": "1.25.11",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
"integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
}
@@ -4976,9 +4980,10 @@
}
},
"node_modules/prosemirror-state": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
@@ -5023,19 +5028,21 @@
}
},
"node_modules/prosemirror-transform": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz",
"integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.34.2",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.2.tgz",
"integrity": "sha512-tPX/V2Xd70vrAGQ/V9CppJtPKnQyQMypJGlLylvdI94k6JaG+4P6fVmXPR1zc1eVTW0gq3c6zsfqwJKCRLaG9Q==",
"version": "1.42.2",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.2.tgz",
"integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-model": "^1.25.8",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}

View File

@@ -73,7 +73,10 @@
"papaparse": "^5.5.3",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-view": "1.34.2",
"prosemirror-model": "1.25.11",
"prosemirror-state": "1.4.4",
"prosemirror-transform": "1.12.0",
"prosemirror-view": "1.42.2",
"re-resizable": "^6.9.18",
"react-colorful": "^5.6.1",
"redent": "^4.0.0",

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/prosemirror-model/dist/index.cjs b/node_modules/prosemirror-model/dist/index.cjs
index aa31355..1409b46 100644
index 96ef508..8f56356 100644
--- a/node_modules/prosemirror-model/dist/index.cjs
+++ b/node_modules/prosemirror-model/dist/index.cjs
@@ -95,6 +95,7 @@ var Fragment = function () {
@@ -112,6 +112,7 @@ var Fragment = function () {
var nodeStart = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
var parent = arguments.length > 4 ? arguments[4] : undefined;
for (var i = 0, pos = 0; pos < to; i++) {
@@ -10,7 +10,7 @@ index aa31355..1409b46 100644
var child = this.content[i],
end = pos + child.nodeSize;
if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
@@ -2817,7 +2818,7 @@ function _renderSpec(doc, structure, xmlNS, blockArraysIn) {
@@ -2860,7 +2861,7 @@ function _renderSpec(doc, structure, xmlNS, blockArraysIn) {
var tagName = structure[0],
suspicious;
if (typeof tagName != "string") throw new RangeError("Invalid array passed to renderSpec");
@@ -20,10 +20,10 @@ index aa31355..1409b46 100644
if (space > 0) {
xmlNS = tagName.slice(0, space);
diff --git a/node_modules/prosemirror-model/dist/index.js b/node_modules/prosemirror-model/dist/index.js
index 0097d9f..19d7031 100644
index 6b00598..c7c4a38 100644
--- a/node_modules/prosemirror-model/dist/index.js
+++ b/node_modules/prosemirror-model/dist/index.js
@@ -84,6 +84,7 @@ class Fragment {
@@ -94,6 +94,7 @@ class Fragment {
*/
nodesBetween(from, to, f, nodeStart = 0, parent) {
for (let i = 0, pos = 0; pos < to; i++) {
@@ -31,7 +31,7 @@ index 0097d9f..19d7031 100644
let child = this.content[i], end = pos + child.nodeSize;
if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
let start = pos + 1;
@@ -3397,9 +3398,9 @@ function renderSpec(doc, structure, xmlNS, blockArraysIn) {
@@ -3420,9 +3421,9 @@ function renderSpec(doc, structure, xmlNS, blockArraysIn) {
let tagName = structure[0], suspicious;
if (typeof tagName != "string")
throw new RangeError("Invalid array passed to renderSpec");

View File

@@ -1,46 +1,46 @@
diff --git a/node_modules/prosemirror-view/dist/index.cjs b/node_modules/prosemirror-view/dist/index.cjs
index 8ea57c7..c289489 100644
index a615cb7..c1a6cbb 100644
--- a/node_modules/prosemirror-view/dist/index.cjs
+++ b/node_modules/prosemirror-view/dist/index.cjs
@@ -980,8 +980,8 @@ var ViewDesc = function () {
if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset)) return;
@@ -1005,8 +1005,8 @@ var ViewDesc = function () {
if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) return;
var domSelExtended = false;
if ((domSel.extend || anchor == head) && !brKludge) {
if ((domSel.extend || anchor == head) && !(brKludge && gecko)) {
- domSel.collapse(anchorDOM.node, anchorDOM.offset);
try {
+ domSel.collapse(anchorDOM.node, anchorDOM.offset);
if (anchor != head) domSel.extend(headDOM.node, headDOM.offset);
domSelExtended = true;
} catch (_) {}
@@ -3456,7 +3456,7 @@ editHandlers.drop = function (view, _event) {
@@ -3647,7 +3647,7 @@ function handleDrop(view, event, dragging) {
});
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
}
- view.focus();
+ if (!dragging || !dragging.nodeView) view.focus();
view.dispatch(tr.setMeta("uiEvent", "drop"));
};
}
handlers.focus = function (view) {
diff --git a/node_modules/prosemirror-view/dist/index.js b/node_modules/prosemirror-view/dist/index.js
index 9583dc3..6899e62 100644
index 61118ee..ec4c853 100644
--- a/node_modules/prosemirror-view/dist/index.js
+++ b/node_modules/prosemirror-view/dist/index.js
@@ -1052,8 +1052,8 @@ class ViewDesc {
@@ -1079,8 +1079,8 @@ class ViewDesc {
// browsers support it yet.
let domSelExtended = false;
if ((domSel.extend || anchor == head) && !brKludge) {
if ((domSel.extend || anchor == head) && !(brKludge && gecko)) {
- domSel.collapse(anchorDOM.node, anchorDOM.offset);
try {
+ domSel.collapse(anchorDOM.node, anchorDOM.offset);
if (anchor != head)
domSel.extend(headDOM.node, headDOM.offset);
domSelExtended = true;
@@ -3731,7 +3731,7 @@ editHandlers.drop = (view, _event) => {
@@ -3885,7 +3885,7 @@ function handleDrop(view, event, dragging) {
tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo);
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
}
- view.focus();
+ if (!dragging || !dragging.nodeView) view.focus();
view.dispatch(tr.setMeta("uiEvent", "drop"));
};
}
handlers.focus = view => {

View File

@@ -236,8 +236,27 @@ type LanguageSelectorProps = {
onClose: () => void;
};
function LanguageSelector(props: LanguageSelectorProps) {
const { onLanguageSelected, selectedLanguage, onClose } = props;
const [languages, setLanguages] = useState(Languages);
const { selectedLanguage, onClose } = props;
const recentlyUsed =
config.get<Record<string, number>>("recentlyUsedCodeBlockLanguages", {}) ??
{};
const [languages, setLanguages] = useState(() =>
[...Languages].sort((a, b) => {
const aCount = recentlyUsed[a.filename] || 0;
const bCount = recentlyUsed[b.filename] || 0;
if (aCount !== bCount) return bCount - aCount;
return 0;
})
);
const onLanguageSelected = (language: string) => {
props.onLanguageSelected(language);
config.set("recentlyUsedCodeBlockLanguages", {
...recentlyUsed,
[language]: Date.now()
});
};
return (
<Popup onClose={onClose}>

View File

@@ -18,8 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { FunctionComponent, SyntheticEvent } from "react";
import { NodeView, Decoration, DecorationSource } from "prosemirror-view";
import { Node as PMNode, Slice } from "prosemirror-model";
import {
NodeView,
Decoration,
DecorationSource,
ViewMutationRecord
} from "prosemirror-view";
import { Node as PMNode } from "prosemirror-model";
import { NodeSelection } from "prosemirror-state";
import { PortalProviderAPI } from "./react-portal-provider.js";
import {
@@ -29,19 +34,18 @@ import {
ContentDOM
} from "./types.js";
import { Editor, NodeViewRendererProps } from "@tiptap/core";
import { __serializeForClipboard, EditorView } from "prosemirror-view";
import { EmotionThemeProvider } from "@notesnook/theme";
import { isAndroid, isiOS } from "../../utils/platform.js";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
// This is hacky workaround to manually handle serialization when
// drag/dropping on mobile devices.
declare module "prosemirror-view" {
export function __serializeForClipboard(
view: EditorView,
slice: Slice
): { dom: HTMLElement; text: string };
}
// declare module "prosemirror-view" {
// export function __serializeForClipboard(
// view: EditorView,
// slice: Slice
// ): { dom: HTMLElement; text: string };
// }
const portalProviderAPI = new PortalProviderAPI();
export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
private domRef!: HTMLElement;
@@ -401,7 +405,7 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
}
ignoreMutation(
mutation: MutationRecord | { type: "selection"; target: Element }
mutation: ViewMutationRecord // MutationRecord | { type: "selection"; target: Element }
) {
if (!this.dom || !this.contentDOM) {
return true;
@@ -501,7 +505,7 @@ function forceHandleDrag(event: DragEvent, editor: Editor) {
if (!event.dataTransfer) return;
const { view } = editor;
const slice = view.state.selection.content();
const { dom, text } = __serializeForClipboard(view, slice);
const { dom, text } = view.serializeForClipboard(slice);
event.dataTransfer.clearData();
event.dataTransfer.setData("Text", text);

View File

@@ -138,7 +138,7 @@ msgstr "{count, plural, one {Are you sure you want to download all attachments o
msgid "{count, plural, one {Are you sure you want to move this notebook to {selectedNotebookTitle}?} other {Are you sure you want to move these # notebooks {selectedNotebookTitle}?}}"
msgstr "{count, plural, one {Are you sure you want to move this notebook to {selectedNotebookTitle}?} other {Are you sure you want to move these # notebooks {selectedNotebookTitle}?}}"
#: generated/action-confirmations.ts:76
#: generated/action-confirmations.ts:82
msgid "{count, plural, one {Are you sure you want to permanently delete this item?} other {Are you sure you to permanently delete these items?}}"
msgstr "{count, plural, one {Are you sure you want to permanently delete this item?} other {Are you sure you to permanently delete these items?}}"
@@ -150,6 +150,10 @@ msgstr "{count, plural, one {Are you sure you want to permanently delete this no
msgid "{count, plural, one {Are you sure you want to permanently delete this notebook?} other {Are you sure you to permanently delete these notebooks?}}"
msgstr "{count, plural, one {Are you sure you want to permanently delete this notebook?} other {Are you sure you to permanently delete these notebooks?}}"
#: generated/action-confirmations.ts:69
msgid "{count, plural, one {Are you sure you want to permanently delete this version?} other {Are you sure you to permanently delete these versions?}}"
msgstr "{count, plural, one {Are you sure you want to permanently delete this version?} other {Are you sure you to permanently delete these versions?}}"
#: src/strings.ts:135
msgid "{count, plural, one {Attach image} other {Attach # images}}"
msgstr "{count, plural, one {Attach image} other {Attach # images}}"
@@ -162,7 +166,7 @@ msgstr "{count, plural, one {Attachment deleted} other {# attachments deleted}}"
msgid "{count, plural, one {Attachment downloaded at {path}} other {#/{total} attachments downloaded as a zip file at {path}}}"
msgstr "{count, plural, one {Attachment downloaded at {path}} other {#/{total} attachments downloaded as a zip file at {path}}}"
#: generated/actions.ts:216
#: generated/actions.ts:222
msgid "{count, plural, one {Color renamed} other {# colors renamed}}"
msgstr "{count, plural, one {Color renamed} other {# colors renamed}}"
@@ -175,7 +179,7 @@ msgid "{count, plural, one {Delete color} other {Delete # colors}}"
msgstr "{count, plural, one {Delete color} other {Delete # colors}}"
#: generated/do-actions.ts:12
#: generated/do-actions.ts:53
#: generated/do-actions.ts:59
msgid "{count, plural, one {Delete item} other {Delete # items}}"
msgstr "{count, plural, one {Delete item} other {Delete # items}}"
@@ -195,6 +199,10 @@ msgstr "{count, plural, one {Delete reminder} other {Delete # reminders}}"
msgid "{count, plural, one {Delete tag} other {Delete # tags}}"
msgstr "{count, plural, one {Delete tag} other {Delete # tags}}"
#: generated/do-actions.ts:36
msgid "{count, plural, one {Delete version} other {Delete # versions}}"
msgstr "{count, plural, one {Delete version} other {Delete # versions}}"
#: generated/in-progress-actions.ts:16
msgid "{count, plural, one {Deleting attachment...} other {Deleting # attachments...}}"
msgstr "{count, plural, one {Deleting attachment...} other {Deleting # attachments...}}"
@@ -219,27 +227,27 @@ msgstr "{count, plural, one {Deleting reminder...} other {Deleting # reminders..
msgid "{count, plural, one {Deleting tag...} other {Deleting # tags...}}"
msgstr "{count, plural, one {Deleting tag...} other {Deleting # tags...}}"
#: generated/do-actions.ts:274
#: generated/do-actions.ts:280
msgid "{count, plural, one {Download attachment} other {Download # attachments}}"
msgstr "{count, plural, one {Download attachment} other {Download # attachments}}"
#: generated/do-actions.ts:283
#: generated/do-actions.ts:289
msgid "{count, plural, one {Download item} other {Download # items}}"
msgstr "{count, plural, one {Download item} other {Download # items}}"
#: generated/do-actions.ts:208
#: generated/do-actions.ts:214
msgid "{count, plural, one {Edit item} other {Edit # items}}"
msgstr "{count, plural, one {Edit item} other {Edit # items}}"
#: generated/do-actions.ts:191
#: generated/do-actions.ts:197
msgid "{count, plural, one {Edit notebook} other {Edit # notebooks}}"
msgstr "{count, plural, one {Edit notebook} other {Edit # notebooks}}"
#: generated/do-actions.ts:195
#: generated/do-actions.ts:201
msgid "{count, plural, one {Edit reminder} other {Edit # reminders}}"
msgstr "{count, plural, one {Edit reminder} other {Edit # reminders}}"
#: generated/do-actions.ts:187
#: generated/do-actions.ts:193
msgid "{count, plural, one {Edit tag} other {Edit # tags}}"
msgstr "{count, plural, one {Edit tag} other {Edit # tags}}"
@@ -255,41 +263,41 @@ msgstr "{count, plural, one {Item could not be published} other {# items could n
msgid "{count, plural, one {Item could not be unpublished} other {# items could not be unpublished}}"
msgstr "{count, plural, one {Item could not be unpublished} other {# items could not be unpublished}}"
#: generated/actions.ts:208
#: generated/actions.ts:214
msgid "{count, plural, one {Item created} other {# items created}}"
msgstr "{count, plural, one {Item created} other {# items created}}"
#: generated/actions.ts:41
#: generated/actions.ts:47
msgid "{count, plural, one {Item deleted} other {# items deleted}}"
msgstr "{count, plural, one {Item deleted} other {# items deleted}}"
#: generated/actions.ts:179
#: generated/actions.ts:185
msgid "{count, plural, one {Item edited} other {# items edited}}"
msgstr "{count, plural, one {Item edited} other {# items edited}}"
#: generated/actions.ts:64
#: generated/actions.ts:70
msgid "{count, plural, one {Item moved to trash} other {# items moved to trash}}"
msgstr "{count, plural, one {Item moved to trash} other {# items moved to trash}}"
#: generated/actions.ts:80
#: generated/actions.ts:93
#: generated/actions.ts:86
#: generated/actions.ts:99
msgid "{count, plural, one {Item permanently deleted} other {# items permanently deleted}}"
msgstr "{count, plural, one {Item permanently deleted} other {# items permanently deleted}}"
#: generated/actions.ts:110
#: generated/actions.ts:116
msgid "{count, plural, one {Item published} other {# items published}}"
msgstr "{count, plural, one {Item published} other {# items published}}"
#: generated/actions.ts:225
#: generated/actions.ts:231
msgid "{count, plural, one {Item renamed} other {# items renamed}}"
msgstr "{count, plural, one {Item renamed} other {# items renamed}}"
#: generated/actions.ts:143
#: generated/actions.ts:156
#: generated/actions.ts:149
#: generated/actions.ts:162
msgid "{count, plural, one {Item restored} other {# items restored}}"
msgstr "{count, plural, one {Item restored} other {# items restored}}"
#: generated/actions.ts:127
#: generated/actions.ts:133
msgid "{count, plural, one {Item unpublished} other {# items unpublished}}"
msgstr "{count, plural, one {Item unpublished} other {# items unpublished}}"
@@ -321,23 +329,23 @@ msgstr "{count, plural, one {Note deleted} other {# notes deleted}}"
msgid "{count, plural, one {Note exported} other {# notes exported}}"
msgstr "{count, plural, one {Note exported} other {# notes exported}}"
#: generated/actions.ts:49
#: generated/actions.ts:55
msgid "{count, plural, one {Note moved to trash} other {# notes moved to trash}}"
msgstr "{count, plural, one {Note moved to trash} other {# notes moved to trash}}"
#: generated/actions.ts:72
#: generated/actions.ts:78
msgid "{count, plural, one {Note permanently deleted} other {# notes permanently deleted}}"
msgstr "{count, plural, one {Note permanently deleted} other {# notes permanently deleted}}"
#: generated/actions.ts:101
#: generated/actions.ts:107
msgid "{count, plural, one {Note published} other {# notes published}}"
msgstr "{count, plural, one {Note published} other {# notes published}}"
#: generated/actions.ts:135
#: generated/actions.ts:141
msgid "{count, plural, one {Note restored} other {# notes restored}}"
msgstr "{count, plural, one {Note restored} other {# notes restored}}"
#: generated/actions.ts:118
#: generated/actions.ts:124
msgid "{count, plural, one {Note unpublished} other {# notes unpublished}}"
msgstr "{count, plural, one {Note unpublished} other {# notes unpublished}}"
@@ -345,7 +353,7 @@ msgstr "{count, plural, one {Note unpublished} other {# notes unpublished}}"
msgid "{count, plural, one {Note will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?} other {# notes will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?}}"
msgstr "{count, plural, one {Note will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?} other {# notes will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?}}"
#: generated/actions.ts:187
#: generated/actions.ts:193
msgid "{count, plural, one {Notebook created} other {# notebooks created}}"
msgstr "{count, plural, one {Notebook created} other {# notebooks created}}"
@@ -353,28 +361,28 @@ msgstr "{count, plural, one {Notebook created} other {# notebooks created}}"
msgid "{count, plural, one {Notebook deleted} other {# notebooks deleted}}"
msgstr "{count, plural, one {Notebook deleted} other {# notebooks deleted}}"
#: generated/actions.ts:168
#: generated/actions.ts:174
msgid "{count, plural, one {Notebook edited} other {# notebooks edited}}"
msgstr "{count, plural, one {Notebook edited} other {# notebooks edited}}"
#: generated/actions.ts:53
#: generated/actions.ts:59
msgid "{count, plural, one {Notebook moved to trash} other {# notebooks moved to trash}}"
msgstr "{count, plural, one {Notebook moved to trash} other {# notebooks moved to trash}}"
#: generated/actions.ts:76
#: generated/actions.ts:82
msgid "{count, plural, one {Notebook permanently deleted} other {# notebooks permanently deleted}}"
msgstr "{count, plural, one {Notebook permanently deleted} other {# notebooks permanently deleted}}"
#: generated/actions.ts:139
#: generated/actions.ts:145
msgid "{count, plural, one {Notebook restored} other {# notebooks restored}}"
msgstr "{count, plural, one {Notebook restored} other {# notebooks restored}}"
#: generated/do-actions.ts:141
#: generated/do-actions.ts:147
msgid "{count, plural, one {Permanently delete attachment} other {Permanently delete # attachments}}"
msgstr "{count, plural, one {Permanently delete attachment} other {Permanently delete # attachments}}"
#: generated/do-actions.ts:145
#: generated/do-actions.ts:156
#: generated/do-actions.ts:151
#: generated/do-actions.ts:162
msgid "{count, plural, one {Permanently delete item} other {Permanently delete # items}}"
msgstr "{count, plural, one {Permanently delete item} other {Permanently delete # items}}"
@@ -383,15 +391,15 @@ msgstr "{count, plural, one {Permanently delete item} other {Permanently delete
msgid "{count, plural, one {permanently deleting item...} other {permanently deleting # items...}}"
msgstr "{count, plural, one {permanently deleting item...} other {permanently deleting # items...}}"
#: generated/do-actions.ts:99
#: generated/do-actions.ts:105
msgid "{count, plural, one {Pin item} other {Pin # items}}"
msgstr "{count, plural, one {Pin item} other {Pin # items}}"
#: generated/do-actions.ts:84
#: generated/do-actions.ts:90
msgid "{count, plural, one {Pin note} other {Pin # notes}}"
msgstr "{count, plural, one {Pin note} other {Pin # notes}}"
#: generated/do-actions.ts:88
#: generated/do-actions.ts:94
msgid "{count, plural, one {Pin notebook} other {Pin # notebooks}}"
msgstr "{count, plural, one {Pin notebook} other {Pin # notebooks}}"
@@ -399,11 +407,11 @@ msgstr "{count, plural, one {Pin notebook} other {Pin # notebooks}}"
msgid "{count, plural, one {Prevent note from syncing} other {Prevent # notes from syncing}}"
msgstr "{count, plural, one {Prevent note from syncing} other {Prevent # notes from syncing}}"
#: generated/do-actions.ts:133
#: generated/do-actions.ts:139
msgid "{count, plural, one {Publish item} other {Publish # items}}"
msgstr "{count, plural, one {Publish item} other {Publish # items}}"
#: generated/do-actions.ts:124
#: generated/do-actions.ts:130
msgid "{count, plural, one {Publish note} other {Publish # notes}}"
msgstr "{count, plural, one {Publish note} other {Publish # notes}}"
@@ -411,47 +419,47 @@ msgstr "{count, plural, one {Publish note} other {Publish # notes}}"
msgid "{count, plural, one {Reminder deleted} other {# reminders deleted}}"
msgstr "{count, plural, one {Reminder deleted} other {# reminders deleted}}"
#: generated/do-actions.ts:253
#: generated/do-actions.ts:259
msgid "{count, plural, one {Remove attachment} other {Remove # attachments}}"
msgstr "{count, plural, one {Remove attachment} other {Remove # attachments}}"
#: generated/do-actions.ts:249
#: generated/do-actions.ts:255
msgid "{count, plural, one {Remove color} other {Remove # colors}}"
msgstr "{count, plural, one {Remove color} other {Remove # colors}}"
#: generated/do-actions.ts:266
#: generated/do-actions.ts:272
msgid "{count, plural, one {Remove item} other {Remove # items}}"
msgstr "{count, plural, one {Remove item} other {Remove # items}}"
#: generated/do-actions.ts:245
#: generated/do-actions.ts:251
msgid "{count, plural, one {Remove shortcut} other {Remove # shortcuts}}"
msgstr "{count, plural, one {Remove shortcut} other {Remove # shortcuts}}"
#: generated/do-actions.ts:216
#: generated/do-actions.ts:222
msgid "{count, plural, one {Rename attachment} other {Rename # attachments}}"
msgstr "{count, plural, one {Rename attachment} other {Rename # attachments}}"
#: generated/do-actions.ts:220
#: generated/do-actions.ts:226
msgid "{count, plural, one {Rename color} other {Rename # colors}}"
msgstr "{count, plural, one {Rename color} other {Rename # colors}}"
#: generated/do-actions.ts:237
#: generated/do-actions.ts:243
msgid "{count, plural, one {Rename item} other {Rename # items}}"
msgstr "{count, plural, one {Rename item} other {Rename # items}}"
#: generated/do-actions.ts:224
#: generated/do-actions.ts:230
msgid "{count, plural, one {Rename tag} other {Rename # tags}}"
msgstr "{count, plural, one {Rename tag} other {Rename # tags}}"
#: generated/do-actions.ts:179
#: generated/do-actions.ts:185
msgid "{count, plural, one {Restore item} other {Restore # items}}"
msgstr "{count, plural, one {Restore item} other {Restore # items}}"
#: generated/do-actions.ts:164
#: generated/do-actions.ts:170
msgid "{count, plural, one {Restore note} other {Restore # notes}}"
msgstr "{count, plural, one {Restore note} other {Restore # notes}}"
#: generated/do-actions.ts:168
#: generated/do-actions.ts:174
msgid "{count, plural, one {Restore notebook} other {Restore # notebooks}}"
msgstr "{count, plural, one {Restore notebook} other {Restore # notebooks}}"
@@ -460,11 +468,11 @@ msgstr "{count, plural, one {Restore notebook} other {Restore # notebooks}}"
msgid "{count, plural, one {Restoring item...} other {Restoring # items...}}"
msgstr "{count, plural, one {Restoring item...} other {Restoring # items...}}"
#: generated/actions.ts:195
#: generated/actions.ts:201
msgid "{count, plural, one {Shortcut created} other {# shortcuts created}}"
msgstr "{count, plural, one {Shortcut created} other {# shortcuts created}}"
#: generated/actions.ts:191
#: generated/actions.ts:197
msgid "{count, plural, one {Tag created} other {# tags created}}"
msgstr "{count, plural, one {Tag created} other {# tags created}}"
@@ -472,30 +480,34 @@ msgstr "{count, plural, one {Tag created} other {# tags created}}"
msgid "{count, plural, one {Tag deleted} other {# tags deleted}}"
msgstr "{count, plural, one {Tag deleted} other {# tags deleted}}"
#: generated/actions.ts:164
#: generated/actions.ts:170
msgid "{count, plural, one {Tag edited} other {# tags edited}}"
msgstr "{count, plural, one {Tag edited} other {# tags edited}}"
#: generated/do-actions.ts:76
#: generated/do-actions.ts:82
msgid "{count, plural, one {Unpin item} other {Unpin # items}}"
msgstr "{count, plural, one {Unpin item} other {Unpin # items}}"
#: generated/do-actions.ts:61
#: generated/do-actions.ts:67
msgid "{count, plural, one {Unpin note} other {Unpin # notes}}"
msgstr "{count, plural, one {Unpin note} other {Unpin # notes}}"
#: generated/do-actions.ts:65
#: generated/do-actions.ts:71
msgid "{count, plural, one {Unpin notebook} other {Unpin # notebooks}}"
msgstr "{count, plural, one {Unpin notebook} other {Unpin # notebooks}}"
#: generated/do-actions.ts:116
#: generated/do-actions.ts:122
msgid "{count, plural, one {Unpublish item} other {Unpublish # items}}"
msgstr "{count, plural, one {Unpublish item} other {Unpublish # items}}"
#: generated/do-actions.ts:107
#: generated/do-actions.ts:113
msgid "{count, plural, one {Unpublish note} other {Unpublish # notes}}"
msgstr "{count, plural, one {Unpublish note} other {Unpublish # notes}}"
#: generated/actions.ts:28
msgid "{count, plural, one {Version deleted} other {# versions deleted}}"
msgstr "{count, plural, one {Version deleted} other {# versions deleted}}"
#: src/strings.ts:2510
msgid "{count} characters"
msgstr "{count} characters"
@@ -1647,6 +1659,10 @@ msgstr "Clear data & reset account"
msgid "Clear default notebook"
msgstr "Clear default notebook"
#: src/strings.ts:2803
msgid "Clear history"
msgstr "Clear history"
#: src/strings.ts:1015
msgid "Clear logs"
msgstr "Clear logs"
@@ -2295,6 +2311,10 @@ msgstr "Delete account"
msgid "Delete all"
msgstr "Delete all"
#: src/strings.ts:2805
msgid "Delete all version history for this note?"
msgstr "Delete all version history for this note?"
#: src/strings.ts:2732
msgid "Delete attachment"
msgstr "Delete attachment"
@@ -3821,6 +3841,10 @@ msgstr "Keep"
msgid "Keep open"
msgstr "Keep open"
#: src/strings.ts:2800
msgid "Keep screen on"
msgstr "Keep screen on"
#: src/strings.ts:1361
msgid "Keep your data safe"
msgstr "Keep your data safe"
@@ -4709,6 +4733,10 @@ msgstr "Off"
msgid "Offline"
msgstr "Offline"
#: src/strings.ts:2810
msgid "Offline mode"
msgstr "Offline mode"
#: src/strings.ts:2688
msgid "OK"
msgstr "OK"
@@ -5191,6 +5219,10 @@ msgstr "Pressing \"X\" will hide the app in your system tray."
msgid "Prevent note title from appearing in tab/window title."
msgstr "Prevent note title from appearing in tab/window title."
#: src/strings.ts:2802
msgid "Prevent the screen from turning off while the editor is focused."
msgstr "Prevent the screen from turning off while the editor is focused."
#: src/strings.ts:2325
msgid "Preview attachment"
msgstr "Preview attachment"
@@ -6128,8 +6160,8 @@ msgid "Select folder with backup files"
msgstr "Select folder with backup files"
#: src/strings.ts:112
msgid "Select how you would like to recieve the code"
msgstr "Select how you would like to recieve the code"
msgid "Select how you would like to receive the code"
msgstr "Select how you would like to receive the code"
#: src/strings.ts:2366
msgid "Select language"
@@ -6449,10 +6481,6 @@ msgstr "Size"
msgid "Skip"
msgstr "Skip"
#: src/strings.ts:1831
msgid "Skip & go directly to the app"
msgstr "Skip & go directly to the app"
#: src/strings.ts:673
msgid "Skip introduction"
msgstr "Skip introduction"
@@ -7407,6 +7435,10 @@ msgstr "Use native OS titlebar instead of replacing it with a custom one. Requir
msgid "Use native titlebar"
msgstr "Use native titlebar"
#: src/strings.ts:1831
msgid "Use offline"
msgstr "Use offline"
#: src/strings.ts:1801
msgid "Use recovery key"
msgstr "Use recovery key"
@@ -7457,6 +7489,10 @@ msgstr "User verification failed"
msgid "Using {instance} (v{version})"
msgstr "Using {instance} (v{version})"
#: src/strings.ts:2812
msgid "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly."
msgstr "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly."
#: src/strings.ts:2265
msgid "Using official Notesnook instance"
msgstr "Using official Notesnook instance"
@@ -7525,6 +7561,10 @@ msgstr "Verifying your email"
msgid "Version"
msgstr "Version"
#: src/strings.ts:2804
msgid "Version history cleared"
msgstr "Version history cleared"
#: src/strings.ts:50
msgid "Vibrate"
msgstr "Vibrate"
@@ -7736,8 +7776,8 @@ msgid "Yes, you can cancel your trial anytime. No questions asked."
msgstr "Yes, you can cancel your trial anytime. No questions asked."
#: src/strings.ts:106
msgid "You also agree to recieve marketing emails from us which you can opt-out of from app settings."
msgstr "You also agree to recieve marketing emails from us which you can opt-out of from app settings."
msgid "You also agree to receive marketing emails from us which you can opt-out of from app settings."
msgstr "You also agree to receive marketing emails from us which you can opt-out of from app settings."
#: src/strings.ts:2604
msgid "You are already subscribed to this plan."

View File

@@ -138,7 +138,7 @@ msgstr ""
msgid "{count, plural, one {Are you sure you want to move this notebook to {selectedNotebookTitle}?} other {Are you sure you want to move these # notebooks {selectedNotebookTitle}?}}"
msgstr ""
#: generated/action-confirmations.ts:76
#: generated/action-confirmations.ts:82
msgid "{count, plural, one {Are you sure you want to permanently delete this item?} other {Are you sure you to permanently delete these items?}}"
msgstr ""
@@ -150,6 +150,10 @@ msgstr ""
msgid "{count, plural, one {Are you sure you want to permanently delete this notebook?} other {Are you sure you to permanently delete these notebooks?}}"
msgstr ""
#: generated/action-confirmations.ts:69
msgid "{count, plural, one {Are you sure you want to permanently delete this version?} other {Are you sure you to permanently delete these versions?}}"
msgstr ""
#: src/strings.ts:135
msgid "{count, plural, one {Attach image} other {Attach # images}}"
msgstr ""
@@ -162,7 +166,7 @@ msgstr ""
msgid "{count, plural, one {Attachment downloaded at {path}} other {#/{total} attachments downloaded as a zip file at {path}}}"
msgstr ""
#: generated/actions.ts:216
#: generated/actions.ts:222
msgid "{count, plural, one {Color renamed} other {# colors renamed}}"
msgstr ""
@@ -175,7 +179,7 @@ msgid "{count, plural, one {Delete color} other {Delete # colors}}"
msgstr ""
#: generated/do-actions.ts:12
#: generated/do-actions.ts:53
#: generated/do-actions.ts:59
msgid "{count, plural, one {Delete item} other {Delete # items}}"
msgstr ""
@@ -195,6 +199,10 @@ msgstr ""
msgid "{count, plural, one {Delete tag} other {Delete # tags}}"
msgstr ""
#: generated/do-actions.ts:36
msgid "{count, plural, one {Delete version} other {Delete # versions}}"
msgstr ""
#: generated/in-progress-actions.ts:16
msgid "{count, plural, one {Deleting attachment...} other {Deleting # attachments...}}"
msgstr ""
@@ -219,27 +227,27 @@ msgstr ""
msgid "{count, plural, one {Deleting tag...} other {Deleting # tags...}}"
msgstr ""
#: generated/do-actions.ts:274
#: generated/do-actions.ts:280
msgid "{count, plural, one {Download attachment} other {Download # attachments}}"
msgstr ""
#: generated/do-actions.ts:283
#: generated/do-actions.ts:289
msgid "{count, plural, one {Download item} other {Download # items}}"
msgstr ""
#: generated/do-actions.ts:208
#: generated/do-actions.ts:214
msgid "{count, plural, one {Edit item} other {Edit # items}}"
msgstr ""
#: generated/do-actions.ts:191
#: generated/do-actions.ts:197
msgid "{count, plural, one {Edit notebook} other {Edit # notebooks}}"
msgstr ""
#: generated/do-actions.ts:195
#: generated/do-actions.ts:201
msgid "{count, plural, one {Edit reminder} other {Edit # reminders}}"
msgstr ""
#: generated/do-actions.ts:187
#: generated/do-actions.ts:193
msgid "{count, plural, one {Edit tag} other {Edit # tags}}"
msgstr ""
@@ -255,41 +263,41 @@ msgstr ""
msgid "{count, plural, one {Item could not be unpublished} other {# items could not be unpublished}}"
msgstr ""
#: generated/actions.ts:208
#: generated/actions.ts:214
msgid "{count, plural, one {Item created} other {# items created}}"
msgstr ""
#: generated/actions.ts:41
#: generated/actions.ts:47
msgid "{count, plural, one {Item deleted} other {# items deleted}}"
msgstr ""
#: generated/actions.ts:179
#: generated/actions.ts:185
msgid "{count, plural, one {Item edited} other {# items edited}}"
msgstr ""
#: generated/actions.ts:64
#: generated/actions.ts:70
msgid "{count, plural, one {Item moved to trash} other {# items moved to trash}}"
msgstr ""
#: generated/actions.ts:80
#: generated/actions.ts:93
#: generated/actions.ts:86
#: generated/actions.ts:99
msgid "{count, plural, one {Item permanently deleted} other {# items permanently deleted}}"
msgstr ""
#: generated/actions.ts:110
#: generated/actions.ts:116
msgid "{count, plural, one {Item published} other {# items published}}"
msgstr ""
#: generated/actions.ts:225
#: generated/actions.ts:231
msgid "{count, plural, one {Item renamed} other {# items renamed}}"
msgstr ""
#: generated/actions.ts:143
#: generated/actions.ts:156
#: generated/actions.ts:149
#: generated/actions.ts:162
msgid "{count, plural, one {Item restored} other {# items restored}}"
msgstr ""
#: generated/actions.ts:127
#: generated/actions.ts:133
msgid "{count, plural, one {Item unpublished} other {# items unpublished}}"
msgstr ""
@@ -321,23 +329,23 @@ msgstr ""
msgid "{count, plural, one {Note exported} other {# notes exported}}"
msgstr ""
#: generated/actions.ts:49
#: generated/actions.ts:55
msgid "{count, plural, one {Note moved to trash} other {# notes moved to trash}}"
msgstr ""
#: generated/actions.ts:72
#: generated/actions.ts:78
msgid "{count, plural, one {Note permanently deleted} other {# notes permanently deleted}}"
msgstr ""
#: generated/actions.ts:101
#: generated/actions.ts:107
msgid "{count, plural, one {Note published} other {# notes published}}"
msgstr ""
#: generated/actions.ts:135
#: generated/actions.ts:141
msgid "{count, plural, one {Note restored} other {# notes restored}}"
msgstr ""
#: generated/actions.ts:118
#: generated/actions.ts:124
msgid "{count, plural, one {Note unpublished} other {# notes unpublished}}"
msgstr ""
@@ -345,7 +353,7 @@ msgstr ""
msgid "{count, plural, one {Note will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?} other {# notes will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?}}"
msgstr ""
#: generated/actions.ts:187
#: generated/actions.ts:193
msgid "{count, plural, one {Notebook created} other {# notebooks created}}"
msgstr ""
@@ -353,28 +361,28 @@ msgstr ""
msgid "{count, plural, one {Notebook deleted} other {# notebooks deleted}}"
msgstr ""
#: generated/actions.ts:168
#: generated/actions.ts:174
msgid "{count, plural, one {Notebook edited} other {# notebooks edited}}"
msgstr ""
#: generated/actions.ts:53
#: generated/actions.ts:59
msgid "{count, plural, one {Notebook moved to trash} other {# notebooks moved to trash}}"
msgstr ""
#: generated/actions.ts:76
#: generated/actions.ts:82
msgid "{count, plural, one {Notebook permanently deleted} other {# notebooks permanently deleted}}"
msgstr ""
#: generated/actions.ts:139
#: generated/actions.ts:145
msgid "{count, plural, one {Notebook restored} other {# notebooks restored}}"
msgstr ""
#: generated/do-actions.ts:141
#: generated/do-actions.ts:147
msgid "{count, plural, one {Permanently delete attachment} other {Permanently delete # attachments}}"
msgstr ""
#: generated/do-actions.ts:145
#: generated/do-actions.ts:156
#: generated/do-actions.ts:151
#: generated/do-actions.ts:162
msgid "{count, plural, one {Permanently delete item} other {Permanently delete # items}}"
msgstr ""
@@ -383,15 +391,15 @@ msgstr ""
msgid "{count, plural, one {permanently deleting item...} other {permanently deleting # items...}}"
msgstr ""
#: generated/do-actions.ts:99
#: generated/do-actions.ts:105
msgid "{count, plural, one {Pin item} other {Pin # items}}"
msgstr ""
#: generated/do-actions.ts:84
#: generated/do-actions.ts:90
msgid "{count, plural, one {Pin note} other {Pin # notes}}"
msgstr ""
#: generated/do-actions.ts:88
#: generated/do-actions.ts:94
msgid "{count, plural, one {Pin notebook} other {Pin # notebooks}}"
msgstr ""
@@ -399,11 +407,11 @@ msgstr ""
msgid "{count, plural, one {Prevent note from syncing} other {Prevent # notes from syncing}}"
msgstr ""
#: generated/do-actions.ts:133
#: generated/do-actions.ts:139
msgid "{count, plural, one {Publish item} other {Publish # items}}"
msgstr ""
#: generated/do-actions.ts:124
#: generated/do-actions.ts:130
msgid "{count, plural, one {Publish note} other {Publish # notes}}"
msgstr ""
@@ -411,47 +419,47 @@ msgstr ""
msgid "{count, plural, one {Reminder deleted} other {# reminders deleted}}"
msgstr ""
#: generated/do-actions.ts:253
#: generated/do-actions.ts:259
msgid "{count, plural, one {Remove attachment} other {Remove # attachments}}"
msgstr ""
#: generated/do-actions.ts:249
#: generated/do-actions.ts:255
msgid "{count, plural, one {Remove color} other {Remove # colors}}"
msgstr ""
#: generated/do-actions.ts:266
#: generated/do-actions.ts:272
msgid "{count, plural, one {Remove item} other {Remove # items}}"
msgstr ""
#: generated/do-actions.ts:245
#: generated/do-actions.ts:251
msgid "{count, plural, one {Remove shortcut} other {Remove # shortcuts}}"
msgstr ""
#: generated/do-actions.ts:216
#: generated/do-actions.ts:222
msgid "{count, plural, one {Rename attachment} other {Rename # attachments}}"
msgstr ""
#: generated/do-actions.ts:220
#: generated/do-actions.ts:226
msgid "{count, plural, one {Rename color} other {Rename # colors}}"
msgstr ""
#: generated/do-actions.ts:237
#: generated/do-actions.ts:243
msgid "{count, plural, one {Rename item} other {Rename # items}}"
msgstr ""
#: generated/do-actions.ts:224
#: generated/do-actions.ts:230
msgid "{count, plural, one {Rename tag} other {Rename # tags}}"
msgstr ""
#: generated/do-actions.ts:179
#: generated/do-actions.ts:185
msgid "{count, plural, one {Restore item} other {Restore # items}}"
msgstr ""
#: generated/do-actions.ts:164
#: generated/do-actions.ts:170
msgid "{count, plural, one {Restore note} other {Restore # notes}}"
msgstr ""
#: generated/do-actions.ts:168
#: generated/do-actions.ts:174
msgid "{count, plural, one {Restore notebook} other {Restore # notebooks}}"
msgstr ""
@@ -460,11 +468,11 @@ msgstr ""
msgid "{count, plural, one {Restoring item...} other {Restoring # items...}}"
msgstr ""
#: generated/actions.ts:195
#: generated/actions.ts:201
msgid "{count, plural, one {Shortcut created} other {# shortcuts created}}"
msgstr ""
#: generated/actions.ts:191
#: generated/actions.ts:197
msgid "{count, plural, one {Tag created} other {# tags created}}"
msgstr ""
@@ -472,30 +480,34 @@ msgstr ""
msgid "{count, plural, one {Tag deleted} other {# tags deleted}}"
msgstr ""
#: generated/actions.ts:164
#: generated/actions.ts:170
msgid "{count, plural, one {Tag edited} other {# tags edited}}"
msgstr ""
#: generated/do-actions.ts:76
#: generated/do-actions.ts:82
msgid "{count, plural, one {Unpin item} other {Unpin # items}}"
msgstr ""
#: generated/do-actions.ts:61
#: generated/do-actions.ts:67
msgid "{count, plural, one {Unpin note} other {Unpin # notes}}"
msgstr ""
#: generated/do-actions.ts:65
#: generated/do-actions.ts:71
msgid "{count, plural, one {Unpin notebook} other {Unpin # notebooks}}"
msgstr ""
#: generated/do-actions.ts:116
#: generated/do-actions.ts:122
msgid "{count, plural, one {Unpublish item} other {Unpublish # items}}"
msgstr ""
#: generated/do-actions.ts:107
#: generated/do-actions.ts:113
msgid "{count, plural, one {Unpublish note} other {Unpublish # notes}}"
msgstr ""
#: generated/actions.ts:28
msgid "{count, plural, one {Version deleted} other {# versions deleted}}"
msgstr ""
#: src/strings.ts:2510
msgid "{count} characters"
msgstr ""
@@ -1647,6 +1659,10 @@ msgstr ""
msgid "Clear default notebook"
msgstr ""
#: src/strings.ts:2803
msgid "Clear history"
msgstr ""
#: src/strings.ts:1015
msgid "Clear logs"
msgstr ""
@@ -2284,6 +2300,10 @@ msgstr ""
msgid "Delete all"
msgstr ""
#: src/strings.ts:2805
msgid "Delete all version history for this note?"
msgstr ""
#: src/strings.ts:2732
msgid "Delete attachment"
msgstr ""
@@ -3801,6 +3821,10 @@ msgstr ""
msgid "Keep open"
msgstr ""
#: src/strings.ts:2800
msgid "Keep screen on"
msgstr ""
#: src/strings.ts:1361
msgid "Keep your data safe"
msgstr ""
@@ -4683,6 +4707,10 @@ msgstr ""
msgid "Offline"
msgstr ""
#: src/strings.ts:2810
msgid "Offline mode"
msgstr ""
#: src/strings.ts:2688
msgid "OK"
msgstr ""
@@ -5165,6 +5193,10 @@ msgstr ""
msgid "Prevent note title from appearing in tab/window title."
msgstr ""
#: src/strings.ts:2802
msgid "Prevent the screen from turning off while the editor is focused."
msgstr ""
#: src/strings.ts:2325
msgid "Preview attachment"
msgstr ""
@@ -6102,7 +6134,7 @@ msgid "Select folder with backup files"
msgstr ""
#: src/strings.ts:112
msgid "Select how you would like to recieve the code"
msgid "Select how you would like to receive the code"
msgstr ""
#: src/strings.ts:2366
@@ -6415,10 +6447,6 @@ msgstr ""
msgid "Skip"
msgstr ""
#: src/strings.ts:1831
msgid "Skip & go directly to the app"
msgstr ""
#: src/strings.ts:673
msgid "Skip introduction"
msgstr ""
@@ -7366,6 +7394,10 @@ msgstr ""
msgid "Use native titlebar"
msgstr ""
#: src/strings.ts:1831
msgid "Use offline"
msgstr ""
#: src/strings.ts:1801
msgid "Use recovery key"
msgstr ""
@@ -7407,6 +7439,10 @@ msgstr ""
msgid "Using {instance} (v{version})"
msgstr ""
#: src/strings.ts:2812
msgid "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly."
msgstr ""
#: src/strings.ts:2265
msgid "Using official Notesnook instance"
msgstr ""
@@ -7475,6 +7511,10 @@ msgstr ""
msgid "Version"
msgstr ""
#: src/strings.ts:2804
msgid "Version history cleared"
msgstr ""
#: src/strings.ts:50
msgid "Vibrate"
msgstr ""
@@ -7686,7 +7726,7 @@ msgid "Yes, you can cancel your trial anytime. No questions asked."
msgstr ""
#: src/strings.ts:106
msgid "You also agree to recieve marketing emails from us which you can opt-out of from app settings."
msgid "You also agree to receive marketing emails from us which you can opt-out of from app settings."
msgstr ""
#: src/strings.ts:2604

View File

@@ -30,7 +30,8 @@ const DO_ACTIONS = [
"notebook",
"tag",
"color",
"attachment"
"attachment",
"version"
]
},
{ action: "unpin", label: "Unpin", dataTypes: ["note", "notebook"] },
@@ -73,7 +74,7 @@ const ACTIONS = [
{
action: "deleted",
label: "deleted",
dataTypes: ["attachment", "reminder", "tag", "note", "notebook"]
dataTypes: ["attachment", "reminder", "tag", "note", "notebook", "version"]
},
{
action: "movedToTrash",
@@ -121,12 +122,12 @@ const ACTION_CONFIRMATIONS = [
"tag",
"color",
"attachment"
],
]
},
{
action: "permanentlyDelete",
label: "permanently delete",
dataTypes: ["note", "notebook"]
dataTypes: ["note", "notebook", "version"]
}
];
@@ -209,6 +210,12 @@ const DATA_TYPES = {
singularCamelCase: `Shortcut`,
plural: `shortcuts`,
pluralCamelCase: `Shortcuts`
},
version: {
singular: `version`,
singularCamelCase: `Version`,
plural: `versions`,
pluralCamelCase: `Versions`
}
};

View File

@@ -103,13 +103,13 @@ export const strings = {
2: () => t`and `,
3: () => t`Privacy Policy. `,
4: () =>
t`You also agree to recieve marketing emails from us which you can opt-out of from app settings.`
t`You also agree to receive marketing emails from us which you can opt-out of from app settings.`
},
alreadyHaveAccount: () => t`Already have an account?`,
login: () => t`Login`,
"2fa": () => t`Two factor authentication`,
select2faMethod: () => t`Select method for two-factor authentication`,
select2faCodeHelpText: () => t`Select how you would like to recieve the code`,
select2faCodeHelpText: () => t`Select how you would like to receive the code`,
"2faCodeHelpText": {
email: () =>
t`Enter the 6 digit code sent to your email to continue logging in`,
@@ -1828,7 +1828,7 @@ For example:
cancelSub: () => t`Cancel subscription`,
unlockWithSecurityKey: () => t`Unlock with security key`,
reloginToYourAccount: () => t`Relogin to your account`,
skipAndGoToApp: () => t`Skip & go directly to the app`,
skipAndGoToApp: () => t`Use offline`,
startAccountRecovery: () => t`Start account recovery`,
dontHaveRecoveryKey: () => t`Don't have your account recovery key?`,
dontHaveBackupFile: () => t`Don't have backup file?`,
@@ -2796,5 +2796,18 @@ Continue without attachments?`,
t`Colornote password for ${filename}`,
colorNotPasswordForDesc: () =>
t`The password for decrypting the Colornote backup file.`,
deleteItem: () => t`Delete item`
deleteItem: () => t`Delete item`,
keepScreenOn: () => t`Keep screen on`,
keepScreenOnDesc: () =>
t`Prevent the screen from turning off while the editor is focused.`,
clearHistory: () => t`Clear history`,
historyCleared: () => t`Version history cleared`,
clearHistoryConfirmation: () => t`Delete all version history for this note?`,
deleteVersion: () => doActions.delete.version(1),
deleteVersionConfirmation: () =>
actionConfirmations.permanentlyDelete.version(1),
versionDeleted: () => actions.deleted.version(1),
offlineMode: () => t`Offline mode`,
offlineModeDesc: () =>
t`Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly.`
};