mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 02:58:34 +02:00
Compare commits
59 Commits
3.4.5-andr
...
fix/xcode-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8a73a9e1 | ||
|
|
f7c755f2d6 | ||
|
|
10a08d5719 | ||
|
|
6e10ce8d8e | ||
|
|
b5140d9947 | ||
|
|
2288b946ad | ||
|
|
5fd43ae08e | ||
|
|
729c410602 | ||
|
|
bb30336f5c | ||
|
|
0dc89ba945 | ||
|
|
bb51244fa3 | ||
|
|
b5ac41c29b | ||
|
|
aafb6049b2 | ||
|
|
32070327f9 | ||
|
|
771a8bb4e3 | ||
|
|
4f816b47ba | ||
|
|
1071249baa | ||
|
|
d4775ad340 | ||
|
|
0d8c855cd0 | ||
|
|
d63ce5a213 | ||
|
|
a1d6bb91e5 | ||
|
|
a20f6960d7 | ||
|
|
6b2b224f41 | ||
|
|
054a023142 | ||
|
|
441a6396be | ||
|
|
987671e59c | ||
|
|
0fc705ce1e | ||
|
|
b3805534d6 | ||
|
|
77c009e83d | ||
|
|
c9f1cba702 | ||
|
|
637cf914ee | ||
|
|
64877c519b | ||
|
|
229b620473 | ||
|
|
658e915589 | ||
|
|
d4658aa032 | ||
|
|
489c793806 | ||
|
|
eeae993736 | ||
|
|
c8cf4ce52a | ||
|
|
f6c31d19d8 | ||
|
|
a94e8d6852 | ||
|
|
2edbc3bf2e | ||
|
|
e4a7d63e07 | ||
|
|
76fd61af4f | ||
|
|
98386a6d71 | ||
|
|
539a630f77 | ||
|
|
6b65f8031d | ||
|
|
3c85cddfe7 | ||
|
|
e426426468 | ||
|
|
109e46bd06 | ||
|
|
281a973c37 | ||
|
|
2738207c20 | ||
|
|
1aa73c1f23 | ||
|
|
9ec22724e9 | ||
|
|
4a7aa27bd9 | ||
|
|
6051614d96 | ||
|
|
295a461dda | ||
|
|
a4cbe83211 | ||
|
|
afd63db043 | ||
|
|
e0b9932aba |
2
.github/workflows/android.e2e.yml
vendored
2
.github/workflows/android.e2e.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
|
||||
104
.github/workflows/android.preview.build.yml
vendored
Normal file
104
.github/workflows/android.preview.build.yml
vendored
Normal 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
|
||||
135
.github/workflows/android.preview.firebase.yml
vendored
135
.github/workflows/android.preview.firebase.yml
vendored
@@ -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
|
||||
72
.github/workflows/android.preview.publish.yml
vendored
Normal file
72
.github/workflows/android.preview.publish.yml
vendored
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/core.tests.yml
vendored
2
.github/workflows/core.tests.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
10
.github/workflows/desktop.preview.yml
vendored
10
.github/workflows/desktop.preview.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/desktop.publish.yml
vendored
10
.github/workflows/desktop.publish.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/desktop.tests.yml
vendored
8
.github/workflows/desktop.tests.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/editor.tests.yml
vendored
2
.github/workflows/editor.tests.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/help.publish.yml
vendored
2
.github/workflows/help.publish.yml
vendored
@@ -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
152
.github/workflows/ios.preview.build.yml
vendored
Normal 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
|
||||
173
.github/workflows/ios.preview.firebase.yml
vendored
173
.github/workflows/ios.preview.firebase.yml
vendored
@@ -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,
|
||||
});
|
||||
}
|
||||
222
.github/workflows/ios.preview.publish.yml
vendored
Normal file
222
.github/workflows/ios.preview.publish.yml
vendored
Normal 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,
|
||||
});
|
||||
}
|
||||
2
.github/workflows/ios.publish.yml
vendored
2
.github/workflows/ios.publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/monograph.publish.yml
vendored
2
.github/workflows/monograph.publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/theme-builder.publish.yml
vendored
2
.github/workflows/theme-builder.publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/themes.publish.yml
vendored
2
.github/workflows/themes.publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/vericrypt.publish.yml
vendored
2
.github/workflows/vericrypt.publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/web.preview.yml
vendored
2
.github/workflows/web.preview.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/web.publish.yml
vendored
2
.github/workflows/web.publish.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/web.tests.yml
vendored
4
.github/workflows/web.tests.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
572
apps/desktop/package-lock.json
generated
572
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.4.3",
|
||||
"version": "3.4.5",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- Add option to clear note version history
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 : []}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
161
apps/mobile/app/components/side-menu/sync-status-button.tsx
Normal file
161
apps/mobile/app/components/side-menu/sync-status-button.tsx
Normal 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;
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 })
|
||||
}));
|
||||
|
||||
Binary file not shown.
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
77
apps/mobile/ios/scripts/patch_fmt_consteval.rb
Normal file
77
apps/mobile/ios/scripts/patch_fmt_consteval.rb
Normal 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
|
||||
6
apps/mobile/package-lock.json
generated
6
apps/mobile/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.4.5",
|
||||
"version": "3.4.7",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
BIN
apps/vericrypt/src/assets/screenshots/devtools_copy_users.png
Normal file
BIN
apps/vericrypt/src/assets/screenshots/devtools_copy_users.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -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'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' 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
|
||||
|
||||
@@ -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'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'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's right. Notesnook is 100% open source under the
|
||||
GPLv3 license. That includes the app, the encryption library, and
|
||||
everything else. We'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>
|
||||
);
|
||||
|
||||
@@ -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'll be extracting your account's salt right from
|
||||
You'll be extracting your account's keys right from
|
||||
Notesnook's local database that lives in your web browser. So put
|
||||
on your seat belt and let's get some salt!
|
||||
on your seat belt and let'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);
|
||||
|
||||
923
apps/web/package-lock.json
generated
923
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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/",
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||

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

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

|
||||
> info What if I didn't receive an email?
|
||||
>
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/15564.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15564.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
4
fastlane/metadata/android/en-US/changelogs/15569.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/15569.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Add option to clear note version history
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -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);
|
||||
|
||||
50
packages/core/__tests__/vaults.test.ts
Normal file
50
packages/core/__tests__/vaults.test.ts
Normal 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();
|
||||
}));
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
packages/editor-mobile/package-lock.json
generated
2
packages/editor-mobile/package-lock.json
generated
@@ -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",
|
||||
|
||||
35
packages/editor/package-lock.json
generated
35
packages/editor/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
@@ -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 => {
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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.`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user