mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
49 Commits
fix/651
...
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 |
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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
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 &&
|
||||
|
||||
@@ -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()}
|
||||
|
||||
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"
|
||||
@@ -4713,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"
|
||||
@@ -6136,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"
|
||||
@@ -6457,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"
|
||||
@@ -7415,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"
|
||||
@@ -7465,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"
|
||||
@@ -7533,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"
|
||||
@@ -7744,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 ""
|
||||
@@ -4687,6 +4707,10 @@ msgstr ""
|
||||
msgid "Offline"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2810
|
||||
msgid "Offline mode"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2688
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
@@ -6110,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
|
||||
@@ -6423,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 ""
|
||||
@@ -7374,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 ""
|
||||
@@ -7415,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 ""
|
||||
@@ -7483,6 +7511,10 @@ msgstr ""
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2804
|
||||
msgid "Version history cleared"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:50
|
||||
msgid "Vibrate"
|
||||
msgstr ""
|
||||
@@ -7694,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?`,
|
||||
@@ -2799,5 +2799,15 @@ Continue without attachments?`,
|
||||
deleteItem: () => t`Delete item`,
|
||||
keepScreenOn: () => t`Keep screen on`,
|
||||
keepScreenOnDesc: () =>
|
||||
t`Prevent the screen from turning off while the editor is focused.`
|
||||
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