mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
95 Commits
web/auth-r
...
importer/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d65be7902 | ||
|
|
d63ce5a213 | ||
|
|
987671e59c | ||
|
|
0fc705ce1e | ||
|
|
b3805534d6 | ||
|
|
77c009e83d | ||
|
|
c9f1cba702 | ||
|
|
637cf914ee | ||
|
|
64877c519b | ||
|
|
229b620473 | ||
|
|
658e915589 | ||
|
|
d4658aa032 | ||
|
|
489c793806 | ||
|
|
eeae993736 | ||
|
|
c8cf4ce52a | ||
|
|
f6c31d19d8 | ||
|
|
a94e8d6852 | ||
|
|
2edbc3bf2e | ||
|
|
e4a7d63e07 | ||
|
|
76fd61af4f | ||
|
|
98386a6d71 | ||
|
|
539a630f77 | ||
|
|
6b65f8031d | ||
|
|
3c85cddfe7 | ||
|
|
e426426468 | ||
|
|
109e46bd06 | ||
|
|
281a973c37 | ||
|
|
2738207c20 | ||
|
|
1aa73c1f23 | ||
|
|
9ec22724e9 | ||
|
|
4a7aa27bd9 | ||
|
|
6051614d96 | ||
|
|
295a461dda | ||
|
|
a4cbe83211 | ||
|
|
afd63db043 | ||
|
|
aadb6e3a01 | ||
|
|
7c3fdab6ee | ||
|
|
14f727d6e6 | ||
|
|
688a646181 | ||
|
|
7a7f0db941 | ||
|
|
96be35c105 | ||
|
|
36bacce3ac | ||
|
|
c89e333c11 | ||
|
|
d521eb3bd8 | ||
|
|
20bf090c7e | ||
|
|
6cc9b8d1e1 | ||
|
|
e931423eba | ||
|
|
743d47e9e9 | ||
|
|
37c96177a7 | ||
|
|
834479d095 | ||
|
|
643515c6c4 | ||
|
|
0b0a4f1b6f | ||
|
|
48d88069c0 | ||
|
|
9d7754f1e1 | ||
|
|
b2ed115011 | ||
|
|
3447e8133c | ||
|
|
4473d8ec67 | ||
|
|
08cf21b05a | ||
|
|
10dccba5b6 | ||
|
|
23c9a98013 | ||
|
|
473fae4a30 | ||
|
|
e9808c08f0 | ||
|
|
4bb46ea149 | ||
|
|
01811372fc | ||
|
|
6d2097259b | ||
|
|
4c3e065928 | ||
|
|
e45b72a8c4 | ||
|
|
aceb6db8b8 | ||
|
|
5a0ca0bed9 | ||
|
|
8dd5779308 | ||
|
|
dc958098ea | ||
|
|
bf536eb183 | ||
|
|
eb3eea031e | ||
|
|
d376e00a1e | ||
|
|
aca36b60b7 | ||
|
|
033666795c | ||
|
|
c0872c98a8 | ||
|
|
f2173a42d9 | ||
|
|
cbddea4518 | ||
|
|
c69304781a | ||
|
|
412631bf67 | ||
|
|
8c2be00920 | ||
|
|
553e2060be | ||
|
|
7603de25c6 | ||
|
|
146216949d | ||
|
|
c4d8abef16 | ||
|
|
baf823264b | ||
|
|
7f0a517ee1 | ||
|
|
da48cf0bd1 | ||
|
|
4c41e13c5d | ||
|
|
82e2953bc8 | ||
|
|
fc244b1196 | ||
|
|
98ecad3450 | ||
|
|
3aa23d3215 | ||
|
|
e0b9932aba |
2
.github/workflows/android.e2e.yml
vendored
2
.github/workflows/android.e2e.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
|
||||
104
.github/workflows/android.preview.build.yml
vendored
Normal file
104
.github/workflows/android.preview.build.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Notesnook Android Preview Build
|
||||
|
||||
# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and
|
||||
# compiled with a read-only GITHUB_TOKEN and NO repository secrets. The release
|
||||
# APK is built without any signing secret (the same as before). Firebase
|
||||
# distribution and the PR comment happen in android.preview.publish.yml, which
|
||||
# runs in the trusted `workflow_run` context and never executes fork code.
|
||||
# Because no secrets are exposed here, the build runs automatically for every
|
||||
# PR (including forks) with no authorization gate.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/android.preview.build.yml"
|
||||
- ".github/workflows/android.preview.publish.yml"
|
||||
|
||||
concurrency:
|
||||
group: android-preview-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
STAGING_BUILD: true
|
||||
|
||||
steps:
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build arm64-v8a apk
|
||||
run: |
|
||||
cd apps/mobile/android
|
||||
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
|
||||
|
||||
- name: Stage build artifact
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/artifact"
|
||||
cp apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk \
|
||||
"$RUNNER_TEMP/artifact/app-preview.apk"
|
||||
{
|
||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||
echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}"
|
||||
} > "$RUNNER_TEMP/artifact/pr-meta.env"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-preview-build
|
||||
path: ${{ runner.temp }}/artifact
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/android/app/build/generated/sourcemaps/**/*.map
|
||||
135
.github/workflows/android.preview.firebase.yml
vendored
135
.github/workflows/android.preview.firebase.yml
vendored
@@ -1,135 +0,0 @@
|
||||
name: Notesnook Android Preview
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/android.preview.firebase.yml"
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
build:
|
||||
needs: authorize
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
STAGING_BUILD: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build arm64-v8a apk
|
||||
run: |
|
||||
cd apps/mobile/android
|
||||
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: saionaro/extract-package-version@master
|
||||
with:
|
||||
path: apps/mobile
|
||||
|
||||
- name: Publish to firebase CLI
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{secrets.FIREBASE_APP_ID}}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- android-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/android/app/build/generated/sourcemaps/**/*.map
|
||||
72
.github/workflows/android.preview.publish.yml
vendored
Normal file
72
.github/workflows/android.preview.publish.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Notesnook Android Preview Publish
|
||||
|
||||
# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so
|
||||
# it has the base repo's secrets and a write-scoped token. It downloads the APK
|
||||
# the build produced and distributes it + posts the PR comment. It never checks
|
||||
# out or executes fork code.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Notesnook Android Preview Build"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: android-preview-build
|
||||
path: ${{ runner.temp }}/artifact
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Load PR metadata
|
||||
run: cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Publish to Firebase
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: ${{ runner.temp }}/artifact/app-preview.apk
|
||||
releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- android-preview-comment -->';
|
||||
const prNumber = Number(process.env.PR_NUMBER);
|
||||
if (!prNumber) return;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.HEAD_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/core.tests.yml
vendored
2
.github/workflows/core.tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
needs: authorize
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
|
||||
12
.github/workflows/desktop.preview.yml
vendored
12
.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
|
||||
@@ -210,12 +210,12 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
outputs:
|
||||
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
|
||||
|
||||
25
.github/workflows/desktop.publish.yml
vendored
25
.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
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
target_commitish: ${{ github.ref }}
|
||||
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
if: inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
|
||||
@@ -127,6 +127,13 @@ jobs:
|
||||
files: ./generated-sources.json
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
- name: Upload generated-sources.json as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: inputs.build-linux
|
||||
with:
|
||||
name: generated-sources
|
||||
path: ./generated-sources.json
|
||||
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
@@ -135,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
|
||||
@@ -182,7 +189,9 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
@@ -237,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
|
||||
@@ -264,6 +273,7 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
@@ -304,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
|
||||
@@ -331,6 +341,7 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
@@ -363,11 +374,11 @@ jobs:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
|
||||
115
.github/workflows/desktop.tests.yml
vendored
115
.github/workflows/desktop.tests.yml
vendored
@@ -18,12 +18,13 @@ on:
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
runs-on: macos-15-intel
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -33,44 +34,10 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Generate desktop build
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
needs: build
|
||||
runs-on: macos-15-intel
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
@@ -87,33 +54,24 @@ jobs:
|
||||
|
||||
test-macos:
|
||||
name: Test macOS
|
||||
needs: build
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
@@ -130,39 +88,24 @@ jobs:
|
||||
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
@@ -179,44 +122,26 @@ jobs:
|
||||
|
||||
test-windows:
|
||||
name: Test for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
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
|
||||
|
||||
127
.github/workflows/ios.preview.build.yml
vendored
Normal file
127
.github/workflows/ios.preview.build.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
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: |
|
||||
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@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 WITHOUT signing. No certificates or secrets are present in this
|
||||
# job. The trusted publish workflow re-signs and exports this archive;
|
||||
# `-exportArchive` only packages/signs and does NOT re-run the app's build
|
||||
# phases, so fork code never runs alongside secrets.
|
||||
- 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="" \
|
||||
CODE_SIGN_ENTITLEMENTS="" \
|
||||
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,
|
||||
});
|
||||
}
|
||||
132
.github/workflows/ios.preview.publish.yml
vendored
Normal file
132
.github/workflows/ios.preview.publish.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
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 + exports + distributes it and posts the PR comment.
|
||||
# `xcodebuild -exportArchive` only packages and signs a prebuilt archive; it
|
||||
# does not run the app's build phases, so fork code is never executed here.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Notesnook iOS Preview Build"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# Only publish previews for builds that actually succeeded.
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
|
||||
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
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_ENV"
|
||||
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
|
||||
|
||||
- name: Export signed IPA
|
||||
env:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXPORT_PLIST="$RUNNER_TEMP/ExportOptions.plist"
|
||||
cp apps/mobile/ios/ExportOptionsStaging.plist "$EXPORT_PLIST"
|
||||
# Augment the checked-in profile map with the fields exportArchive needs.
|
||||
/usr/libexec/PlistBuddy -c "Add :method string ad-hoc" "$EXPORT_PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Set :method ad-hoc" "$EXPORT_PLIST"
|
||||
/usr/libexec/PlistBuddy -c "Add :signingStyle string manual" "$EXPORT_PLIST" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :signingCertificate string Apple Distribution" "$EXPORT_PLIST" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :teamID string $APPLE_TEAM_ID" "$EXPORT_PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Set :teamID $APPLE_TEAM_ID" "$EXPORT_PLIST"
|
||||
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$RUNNER_TEMP/Notesnook.xcarchive" \
|
||||
-exportOptionsPlist "$EXPORT_PLIST" \
|
||||
-exportPath "$RUNNER_TEMP/export"
|
||||
|
||||
# Normalize the IPA name for the distribution step.
|
||||
IPA="$(find "$RUNNER_TEMP/export" -name '*.ipa' | head -n1)"
|
||||
cp "$IPA" "$RUNNER_TEMP/Notesnook.ipa"
|
||||
|
||||
- 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/${{ 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 = '<!-- 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 }}
|
||||
|
||||
|
||||
@@ -17,10 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test } from "@nn/test";
|
||||
import { test, expect } from "@nn/test";
|
||||
import { gt, lt } from "semver";
|
||||
import { AppModel } from "../../web/__e2e__/models/app.model.js";
|
||||
import { expect } from "playwright/test";
|
||||
|
||||
test.extend({ options: { version: "3.0.0" } })(
|
||||
"update starts downloading if version is outdated",
|
||||
|
||||
@@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { mergeTests } from "@playwright/test";
|
||||
import { test } from "@playwright/test";
|
||||
import { mergeTests, test } from "@playwright/test";
|
||||
import type { CommonFixtures, CommonWorkerFixtures } from "./common-fixtures";
|
||||
import { commonFixtures } from "./common-fixtures";
|
||||
import { platformTest } from "./platform-fixtures";
|
||||
|
||||
@@ -21,7 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { AppContext, buildAndLaunchApp, TestOptions } from "./utils";
|
||||
import path from "path";
|
||||
import { version } from "../../package.json";
|
||||
import type { Browser, ElectronApplication, Page } from "@playwright/test";
|
||||
import type { ElectronApplication, Page } from "@playwright/test";
|
||||
import type { TraceViewerFixtures } from "./trace-viewer-fixtures";
|
||||
import { traceViewerFixtures } from "./trace-viewer-fixtures";
|
||||
import { PageTestFixtures, PageWorkerFixtures } from "./page-test-api";
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
/* eslint-disable header/header */
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/// @ts-ignore
|
||||
import { oop, client } from "playwright-core/lib/coreBundle";
|
||||
|
||||
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.
|
||||
export type TestModeName = "default" | "driver";
|
||||
|
||||
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/>.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
// @ts-ignore
|
||||
import { start } from "playwright-core/lib/outofprocess";
|
||||
// @ts-ignore
|
||||
import type { Playwright } from "playwright-core/lib/client/playwright";
|
||||
|
||||
export type TestModeName =
|
||||
| "default"
|
||||
| "driver"
|
||||
| "service"
|
||||
| "service2"
|
||||
| "wsl";
|
||||
const { start } = oop;
|
||||
|
||||
interface TestMode {
|
||||
setup(): Promise<Playwright>;
|
||||
setup(): Promise<client.Playwright>;
|
||||
teardown(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DriverTestMode implements TestMode {
|
||||
private _impl: { playwright: Playwright; stop: () => Promise<void> };
|
||||
private _impl: { playwright: client.Playwright; stop: () => Promise<void> };
|
||||
|
||||
async setup() {
|
||||
this._impl = await start({
|
||||
|
||||
@@ -20,7 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { execSync } from "child_process";
|
||||
import { cp } from "fs/promises";
|
||||
import path, { join, resolve } from "path";
|
||||
import { _electron as electron, ElectronApplication, Page } from "playwright";
|
||||
import {
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page
|
||||
} from "@playwright/test";
|
||||
import { existsSync } from "fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
|
||||
@@ -30,7 +34,7 @@ const root = path.resolve(__dirname, "..", "..");
|
||||
const SOURCE_DIR = resolve(root, "output", productName);
|
||||
|
||||
export interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
app: ElectronApplication;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
|
||||
10829
apps/desktop/package-lock.json
generated
10829
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.0-beta.1",
|
||||
"version": "3.4.4",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -54,18 +54,18 @@
|
||||
"zod": "3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron": "^37.10.3",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "0.21.5",
|
||||
"fkill": "^10.0.3",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
"playwright": "1.58.2",
|
||||
"playwright-core": "1.61.1",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -86,7 +86,7 @@
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package",
|
||||
"test": "playwright test --tsconfig tsconfig.json"
|
||||
"test": "playwright test --tsconfig __tests__/tsconfig.json --project notesnook-desktop"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
index 7d8468f..ab252ef 100644
|
||||
--- a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
+++ b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
@@ -16010,6 +16010,7 @@ var init_validator = __esm({
|
||||
args: tOptional(tArray(tString)),
|
||||
chromiumSandbox: tOptional(tBoolean),
|
||||
cwd: tOptional(tString),
|
||||
+ baseURL: tOptional(tString),
|
||||
env: tOptional(tArray(tType("NameValue"))),
|
||||
timeout: tFloat,
|
||||
acceptDownloads: tOptional(tEnum(["accept", "deny", "internal-browser-default"])),
|
||||
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
index 0bff9d3..d195697 100644
|
||||
--- a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
+++ b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
@@ -21813,6 +21813,7 @@ export interface Electron {
|
||||
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
|
||||
*/
|
||||
args?: Array<string>;
|
||||
+ baseURL?: string;
|
||||
|
||||
/**
|
||||
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory
|
||||
@@ -1,24 +0,0 @@
|
||||
diff --git a/node_modules/playwright-core/lib/protocol/validator.js b/node_modules/playwright-core/lib/protocol/validator.js
|
||||
index b36c979..9e8ca15 100644
|
||||
--- a/node_modules/playwright-core/lib/protocol/validator.js
|
||||
+++ b/node_modules/playwright-core/lib/protocol/validator.js
|
||||
@@ -2536,6 +2536,7 @@ import_validatorPrimitives.scheme.ElectronLaunchParams = (0, import_validatorPri
|
||||
executablePath: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
args: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)(import_validatorPrimitives.tString)),
|
||||
cwd: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
+ baseURL: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
env: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)((0, import_validatorPrimitives.tType)("NameValue"))),
|
||||
timeout: import_validatorPrimitives.tFloat,
|
||||
acceptDownloads: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tEnum)(["accept", "deny", "internal-browser-default"])),
|
||||
diff --git a/node_modules/playwright-core/types/types.d.ts b/node_modules/playwright-core/types/types.d.ts
|
||||
index f6e6165..4efd4d4 100644
|
||||
--- a/node_modules/playwright-core/types/types.d.ts
|
||||
+++ b/node_modules/playwright-core/types/types.d.ts
|
||||
@@ -19233,6 +19233,7 @@ export interface Electron {
|
||||
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
|
||||
*/
|
||||
args?: Array<string>;
|
||||
+ baseURL?: string;
|
||||
|
||||
/**
|
||||
* Toggles bypassing page's Content-Security-Policy. Defaults to `false`.
|
||||
@@ -46,7 +46,7 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions> = {
|
||||
globalTimeout: 5400000,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: process.env.CI
|
||||
? [["dot"], ["json", { outputFile: path.join(outputDir, "report.json") }]]
|
||||
: "line",
|
||||
|
||||
@@ -33,17 +33,38 @@ function generateId() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
function getStreamOrThrow(id: string, operation: "write" | "close") {
|
||||
const stream = activeStreams.get(id);
|
||||
if (!stream) {
|
||||
throw new Error(
|
||||
`Backup stream not found during ${operation}. The stream may have already been closed or was never opened. Stream id: ${id}`
|
||||
);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
export const backupsRouter = t.router({
|
||||
open: t.procedure
|
||||
.input(z.object({ filename: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const { filename } = input;
|
||||
if (!filename.trim()) {
|
||||
throw new Error("Invalid backup filename: filename cannot be empty.");
|
||||
}
|
||||
if (filename.includes(path.sep) || filename.includes("\\"))
|
||||
throw new Error("Invalid filename");
|
||||
throw new Error(
|
||||
`Invalid backup filename: expected a plain file name without path separators, received "${filename}".`
|
||||
);
|
||||
const resolvedBackupDir = resolvePath(config.backupDirectory);
|
||||
const backupPath = path.resolve(resolvedBackupDir, filename);
|
||||
if (!backupPath.startsWith(resolvedBackupDir))
|
||||
throw new Error("Invalid filename");
|
||||
const relativeBackupPath = path.relative(resolvedBackupDir, backupPath);
|
||||
if (
|
||||
relativeBackupPath.startsWith("..") ||
|
||||
path.isAbsolute(relativeBackupPath)
|
||||
)
|
||||
throw new Error(
|
||||
`Invalid backup filename: resolved path "${backupPath}" is outside the configured backup directory "${resolvedBackupDir}". The configured backup directory may be invalid.`
|
||||
);
|
||||
|
||||
mkdirSync(resolvedBackupDir, { recursive: true });
|
||||
const stream = createWriteStream(backupPath, { encoding: "utf-8" });
|
||||
@@ -55,8 +76,7 @@ export const backupsRouter = t.router({
|
||||
write: t.procedure
|
||||
.input(z.object({ id: z.string(), chunk: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
const stream = getStreamOrThrow(input.id, "write");
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
stream.write(Buffer.from(input.chunk, "base64"), (err) =>
|
||||
err ? reject(err) : resolve()
|
||||
@@ -67,8 +87,7 @@ export const backupsRouter = t.router({
|
||||
close: t.procedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
const stream = getStreamOrThrow(input.id, "close");
|
||||
return new Promise<void>((resolve) => {
|
||||
stream.end(() => {
|
||||
activeStreams.delete(input.id);
|
||||
|
||||
@@ -33,7 +33,7 @@ import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
import { getTheme, setTheme, Theme } from "../utils/theme";
|
||||
import { existsSync } from "fs";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { normalizePathString, resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak, isPortable, isSnap } from "../utils";
|
||||
@@ -60,7 +60,13 @@ export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
isSnap: t.procedure.query(() => isSnap()),
|
||||
isPortable: t.procedure.query(() => isPortable()),
|
||||
backupDirectory: t.procedure.query(() => config.backupDirectory),
|
||||
backupDirectory: t.procedure.query(() => {
|
||||
const backupDirectory = normalizePathString(config.backupDirectory);
|
||||
if (backupDirectory !== config.backupDirectory) {
|
||||
config.backupDirectory = backupDirectory;
|
||||
}
|
||||
return backupDirectory;
|
||||
}),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||
import { Messages, setI18nGlobal } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { PATHS } from "./constants";
|
||||
import { normalizePathString } from "./utils/resolve-path";
|
||||
|
||||
const locale =
|
||||
process.env.NODE_ENV === "development"
|
||||
@@ -294,7 +295,7 @@ async function migrateBackupDirectory() {
|
||||
`localStorage.getItem("backupStorageLocation")`
|
||||
);
|
||||
if (!oldPath || oldPath === PATHS.backupsDirectory) return;
|
||||
config.backupDirectory = oldPath;
|
||||
config.backupDirectory = normalizePathString(oldPath);
|
||||
} catch (e) {
|
||||
console.error("Failed to migrate backup directory", e);
|
||||
const pressedButton = dialog.showMessageBoxSync(globalThis.window, {
|
||||
|
||||
@@ -20,11 +20,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { app } from "electron";
|
||||
import { isAbsolute, join } from "path";
|
||||
|
||||
export function normalizePathString(_path: string) {
|
||||
let normalizedPath = _path.trim();
|
||||
|
||||
try {
|
||||
const parsedPath = JSON.parse(normalizedPath);
|
||||
if (typeof parsedPath === "string") normalizedPath = parsedPath;
|
||||
} catch {
|
||||
// ignore invalid JSON and keep the original path string
|
||||
}
|
||||
|
||||
if (normalizedPath === "~") return app.getPath("home");
|
||||
if (/^~[\\/]/.test(normalizedPath)) {
|
||||
return join(app.getPath("home"), normalizedPath.slice(2));
|
||||
}
|
||||
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
export function resolvePath(_path: string) {
|
||||
if (isAbsolute(_path)) return _path;
|
||||
const normalizedPath = normalizePathString(_path);
|
||||
if (isAbsolute(normalizedPath)) return normalizedPath;
|
||||
|
||||
return join(
|
||||
..._path.split("/").map((segment) => {
|
||||
...normalizedPath.split("/").map((segment) => {
|
||||
let resolved = segment;
|
||||
try {
|
||||
resolved = app.getPath(resolved as any);
|
||||
|
||||
@@ -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 3108
|
||||
versionCode 3113
|
||||
}
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
- Inbox API is now in beta. Learn more about it at https://help.notesnook.com/inbox-api/getting-started
|
||||
- Added close all tabs button in editor
|
||||
- Deeplinking support for nn:// urls
|
||||
- Set default image compression mode in Settings -> Behavior
|
||||
- Minor bug fixes and performance improvements
|
||||
- Add option to clear note version history
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
@@ -729,6 +729,13 @@ export const VaultDialog: React.FC = () => {
|
||||
>
|
||||
{(isChangePassword ||
|
||||
isClearVault ||
|
||||
requestType === VaultRequestType.LockNote ||
|
||||
requestType === VaultRequestType.UnlockNote ||
|
||||
requestType === VaultRequestType.PermanentUnlock ||
|
||||
isGoToEditor ||
|
||||
isShareNote ||
|
||||
isDeleteNote ||
|
||||
isEnableFingerprint ||
|
||||
(isDeleteVault && isUserLoggedIn) ||
|
||||
isCustomAction) &&
|
||||
!isRevokeFingerprint ? (
|
||||
|
||||
@@ -39,6 +39,7 @@ import Share from "react-native-share";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
|
||||
const ImagePreview = () => {
|
||||
const { colors } = useThemeColors("dialog");
|
||||
@@ -87,9 +88,8 @@ const ImagePreview = () => {
|
||||
return;
|
||||
}
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
const path = `${cacheDir}/${
|
||||
"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
|
||||
}`;
|
||||
const path = `${cacheDir}/${"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
|
||||
}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
@@ -136,17 +136,23 @@ const ImagePreview = () => {
|
||||
borderWidth: 0
|
||||
}}
|
||||
onPress={async () => {
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
await Share.open({
|
||||
url: image
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(false);
|
||||
if (!image) return;
|
||||
|
||||
try {
|
||||
if (Platform.OS === "android") {
|
||||
await FileViewer.open(image.replace("file://", ""), {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any);
|
||||
} else {
|
||||
await Share.open({
|
||||
url: image
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
|
||||
@@ -72,6 +72,7 @@ const MergeConflicts = () => {
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const content = useRef<UnencryptedContentItem>(null);
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const keepScreenOn = useSettingStore((state) => state.settings.keepScreenOn);
|
||||
|
||||
const applyChanges = async () => {
|
||||
const contentToSave = selectedContent;
|
||||
@@ -155,9 +156,9 @@ const MergeConflicts = () => {
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
@@ -182,9 +183,9 @@ const MergeConflicts = () => {
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
@@ -385,7 +386,7 @@ const MergeConflicts = () => {
|
||||
paddingTop: insets.top
|
||||
}}
|
||||
>
|
||||
<KeepAwake />
|
||||
{keepScreenOn && <KeepAwake />}
|
||||
{dialogVisible && (
|
||||
<BaseDialog visible={true}>
|
||||
<DialogContainer>
|
||||
|
||||
@@ -29,7 +29,7 @@ import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
@@ -39,15 +39,19 @@ import { Pressable } from "../ui/pressable";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import NotePreview from "./preview";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Dialog } from "../dialog";
|
||||
|
||||
const HistoryItem = ({
|
||||
index,
|
||||
items,
|
||||
note
|
||||
note,
|
||||
refresh
|
||||
}: {
|
||||
index: number;
|
||||
items?: VirtualizedGrouping<HistorySession>;
|
||||
note: Note;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const [item] = useDBItem(index, "noteHistory", items);
|
||||
const { colors } = useThemeColors();
|
||||
@@ -75,12 +79,13 @@ const HistoryItem = ({
|
||||
}}
|
||||
content={content}
|
||||
note={note}
|
||||
update={refresh}
|
||||
/>
|
||||
),
|
||||
context: "note_history"
|
||||
});
|
||||
},
|
||||
[note]
|
||||
[note, refresh]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -112,7 +117,8 @@ const HistoryItem = ({
|
||||
};
|
||||
|
||||
export default function NoteHistory({
|
||||
note
|
||||
note,
|
||||
fwdRef
|
||||
}: {
|
||||
note: Note;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
@@ -121,7 +127,7 @@ export default function NoteHistory({
|
||||
const [_loading, setLoading] = useState(true);
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = useCallback(() => {
|
||||
db.noteHistory
|
||||
.get(note.id)
|
||||
.sorted({
|
||||
@@ -137,17 +143,56 @@ export default function NoteHistory({
|
||||
});
|
||||
}, [note.id]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ index }: { index: number }) => (
|
||||
<HistoryItem index={index} items={history} note={note} />
|
||||
<HistoryItem
|
||||
index={index}
|
||||
items={history}
|
||||
note={note}
|
||||
refresh={refresh}
|
||||
/>
|
||||
),
|
||||
[history, note]
|
||||
[history, note, refresh]
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<SheetProvider context="note_history" />
|
||||
<DialogHeader title={strings.noteHistory()} padding={12} />
|
||||
<Dialog context="local" />
|
||||
<DialogHeader
|
||||
title={strings.noteHistory()}
|
||||
padding={12}
|
||||
button={
|
||||
history?.placeholders.length
|
||||
? {
|
||||
title: strings.clearHistory(),
|
||||
type: "secondary",
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
title: strings.clearHistory(),
|
||||
paragraph: strings.clearHistoryConfirmation(),
|
||||
positiveText: strings.clear(),
|
||||
negativeText: strings.cancel(),
|
||||
positiveType: "errorShade",
|
||||
context: "local",
|
||||
positivePress: async () => {
|
||||
await db.noteHistory.clearSessions(note.id);
|
||||
fwdRef.current?.hide();
|
||||
ToastManager.show({
|
||||
heading: strings.historyCleared(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<Seperator />
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ import {
|
||||
export default function NotePreview({
|
||||
session,
|
||||
content,
|
||||
note
|
||||
note,
|
||||
update
|
||||
}: {
|
||||
session?: HistorySession & { session: string };
|
||||
content:
|
||||
@@ -64,6 +65,7 @@ export default function NotePreview({
|
||||
>
|
||||
| undefined;
|
||||
note: TrashOrItem<Note>;
|
||||
update?: () => void;
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const [locked, setLocked] = useState(false);
|
||||
@@ -107,24 +109,35 @@ export default function NotePreview({
|
||||
}, [note]);
|
||||
|
||||
const deleteNote = async () => {
|
||||
const isSession = !!session;
|
||||
|
||||
presentDialog({
|
||||
title: strings.deleteNote(),
|
||||
paragraph: strings.deleteNoteConfirmation(),
|
||||
title: isSession ? strings.deleteVersion() : strings.deleteNote(),
|
||||
paragraph: isSession
|
||||
? strings.deleteVersionConfirmation()
|
||||
: strings.deleteNoteConfirmation(),
|
||||
positiveText: strings.delete(),
|
||||
negativeText: strings.cancel(),
|
||||
context: "local",
|
||||
context: "preview",
|
||||
positivePress: async () => {
|
||||
if (note) {
|
||||
if (isSession) {
|
||||
await db.noteHistory.remove(session.id);
|
||||
update?.();
|
||||
eSendEvent(eCloseSheet, "note_history");
|
||||
Navigation.queueRoutesForUpdate();
|
||||
} else if (note.type === "trash") {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().refresh();
|
||||
useSelectionStore.getState().setSelectionMode();
|
||||
ToastManager.show({
|
||||
heading: strings.noteDeleted(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
eSendEvent(eCloseSheet);
|
||||
}
|
||||
ToastManager.show({
|
||||
heading: isSession ? strings.versionDeleted() : strings.noteDeleted(),
|
||||
type: "success",
|
||||
context: isSession ? "local" : "global"
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
positiveType: "error"
|
||||
});
|
||||
@@ -137,7 +150,7 @@ export default function NotePreview({
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<Dialog context="preview" />
|
||||
<DialogHeader
|
||||
padding={12}
|
||||
title={content?.title || note.title || session?.session}
|
||||
|
||||
@@ -112,7 +112,8 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
}
|
||||
setStep(Steps.buy);
|
||||
}
|
||||
}, [pricingPlans, routeParams.state]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [routeParams.state]);
|
||||
|
||||
useEffect(() => {
|
||||
let listener: NativeEventSubscription;
|
||||
|
||||
@@ -170,147 +170,147 @@ export const SelectionHeader = React.memo(
|
||||
{!selectedItemsList.length
|
||||
? null
|
||||
: (focusedRouteId === "Monographs"
|
||||
? [
|
||||
{
|
||||
title: strings.unpublish(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
title: strings.doActions.unpublish.note(
|
||||
selectedItemsList.length
|
||||
),
|
||||
positiveText: strings.unpublish(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
for (const id of selectedItemsList) {
|
||||
await db.monographs.unpublish(id);
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
},
|
||||
visible: true,
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: strings.manageTags(),
|
||||
onPress: async () => {
|
||||
ManageTags.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "pound"
|
||||
},
|
||||
{
|
||||
title: strings.export(),
|
||||
onPress: async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "export"
|
||||
},
|
||||
{
|
||||
title: strings.addToNotebook(),
|
||||
onPress: async () => {
|
||||
Navigation.navigate("LinkNotebooks", {
|
||||
noteIds: selectedItemsList
|
||||
});
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "plus"
|
||||
},
|
||||
{
|
||||
title: strings.unlinkNotebook(),
|
||||
onPress: async () => {
|
||||
if (!id) return;
|
||||
await db.notes.removeFromNotebook(
|
||||
id,
|
||||
...selectedItemsList
|
||||
);
|
||||
updateNotebook(id);
|
||||
? [
|
||||
{
|
||||
title: strings.unpublish(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
title: strings.doActions.unpublish.note(
|
||||
selectedItemsList.length
|
||||
),
|
||||
positiveText: strings.unpublish(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
for (const id of selectedItemsList) {
|
||||
await db.monographs.unpublish(id);
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
visible: renderedInRoute === "Notebook",
|
||||
icon: "minus"
|
||||
},
|
||||
{
|
||||
title: strings.unfavorite(),
|
||||
onPress: addToFavorite,
|
||||
visible: focusedRouteId === "Favorites",
|
||||
icon: "star-off"
|
||||
},
|
||||
{
|
||||
title: strings.moveToTrash(),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible: type === "note" || type === "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.doActions.delete.unknown(
|
||||
type!,
|
||||
selectedItemsList.length
|
||||
),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible:
|
||||
type !== "trash" &&
|
||||
type !== "note" &&
|
||||
type !== "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.restore(),
|
||||
onPress: restoreItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete-restore"
|
||||
},
|
||||
{
|
||||
title: strings.delete(),
|
||||
onPress: deleteItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
).map((item) =>
|
||||
!item.visible ? null : (
|
||||
<IconButton
|
||||
size={AppFontSize.lg}
|
||||
type="plain"
|
||||
tooltipText={item.title}
|
||||
tooltipPosition={NativeTooltip.POSITIONS.TOP}
|
||||
testID={`select-${item.icon}`}
|
||||
name={item.icon}
|
||||
key={item.title}
|
||||
color={colors.primary.icon}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
if (Platform.OS === "ios") await sleep(300);
|
||||
item.onPress();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
},
|
||||
visible: true,
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: strings.manageTags(),
|
||||
onPress: async () => {
|
||||
ManageTags.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "pound"
|
||||
},
|
||||
{
|
||||
title: strings.export(),
|
||||
onPress: async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "export"
|
||||
},
|
||||
{
|
||||
title: strings.addToNotebook(),
|
||||
onPress: async () => {
|
||||
Navigation.navigate("LinkNotebooks", {
|
||||
noteIds: selectedItemsList
|
||||
});
|
||||
},
|
||||
visible: type === "note",
|
||||
icon: "plus"
|
||||
},
|
||||
{
|
||||
title: strings.unlinkNotebook(),
|
||||
onPress: async () => {
|
||||
if (!id) return;
|
||||
await db.notes.removeFromNotebook(
|
||||
id,
|
||||
...selectedItemsList
|
||||
);
|
||||
updateNotebook(id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
visible: renderedInRoute === "Notebook",
|
||||
icon: "minus"
|
||||
},
|
||||
{
|
||||
title: strings.unfavorite(),
|
||||
onPress: addToFavorite,
|
||||
visible: focusedRouteId === "Favorites",
|
||||
icon: "star-off"
|
||||
},
|
||||
{
|
||||
title: strings.moveToTrash(),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible: type === "note" || type === "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.doActions.delete.unknown(
|
||||
type!,
|
||||
selectedItemsList.length
|
||||
),
|
||||
onPress: async () => {
|
||||
const selection = useSelectionStore.getState();
|
||||
if (!selection.selectionMode) return;
|
||||
await deleteItems(
|
||||
selection.selectionMode as ItemType,
|
||||
selection.selectedItemsList
|
||||
);
|
||||
selection.clearSelection();
|
||||
selection.setSelectionMode(undefined);
|
||||
},
|
||||
visible:
|
||||
type !== "trash" &&
|
||||
type !== "note" &&
|
||||
type !== "notebook",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: strings.restore(),
|
||||
onPress: restoreItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete-restore"
|
||||
},
|
||||
{
|
||||
title: strings.delete(),
|
||||
onPress: deleteItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete"
|
||||
}
|
||||
]
|
||||
).map((item) =>
|
||||
!item.visible ? null : (
|
||||
<IconButton
|
||||
size={AppFontSize.lg}
|
||||
type="plain"
|
||||
tooltipText={item.title}
|
||||
tooltipPosition={NativeTooltip.POSITIONS.TOP}
|
||||
testID={`select-${item.icon}`}
|
||||
name={item.icon}
|
||||
key={item.title}
|
||||
color={colors.primary.icon}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
menuRef.current?.hide();
|
||||
if (Platform.OS === "ios") await sleep(300);
|
||||
item.onPress();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
|
||||
@@ -28,7 +28,7 @@ import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
@@ -40,6 +40,8 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
@@ -118,8 +120,8 @@ const ListBlockItem = ({
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: !item.content || item.content.trim() === ""
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
@@ -158,6 +160,7 @@ export default function LinkNote(props: {
|
||||
|
||||
const [selectedNote, setSelectedNote] = useState<Note>();
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string>();
|
||||
const [blocksLoading, setBlocksLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
|
||||
@@ -206,10 +209,12 @@ export default function LinkNote(props: {
|
||||
|
||||
const onSelectNote = async (note: Note) => {
|
||||
setSelectedNote(note);
|
||||
setBlocksLoading(true);
|
||||
inputRef.current?.clear();
|
||||
setTimeout(async () => {
|
||||
nodesRef.current = await db.notes.contentBlocks(note.id);
|
||||
setNodes(nodesRef.current);
|
||||
setBlocksLoading(false);
|
||||
});
|
||||
// Fetch and set note's nodes.
|
||||
};
|
||||
@@ -224,6 +229,7 @@ export default function LinkNote(props: {
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingBottom: DefaultAppStyles.GAP,
|
||||
minHeight: "100%",
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
@@ -330,28 +336,42 @@ export default function LinkNote(props: {
|
||||
windowSize={3}
|
||||
keyExtractor={(item) => item.id}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{blockLinking?.error}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
!blockLinking || blocksLoading ? (
|
||||
<ActivityIndicator size={25} color={colors.primary.accent} />
|
||||
) : !blockLinking.isAllowed ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
alignItems: "center"
|
||||
}}
|
||||
type="accent"
|
||||
/>
|
||||
</View>
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{blockLinking?.error}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
onPress={() => {
|
||||
Navigation.navigate("PayWall", {
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out",
|
||||
canGoBack: true
|
||||
});
|
||||
|
||||
props.close?.();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
data={blockLinking?.isAllowed ? nodes : []}
|
||||
/>
|
||||
|
||||
@@ -198,7 +198,7 @@ const PublishNoteSheet = ({
|
||||
};
|
||||
|
||||
const monographMetadata =
|
||||
monographData.result ?? lastMonographDataResult.current;
|
||||
monographData.result?.metadata ?? lastMonographDataResult.current;
|
||||
|
||||
useEffect(() => {
|
||||
const prevState = previousAppState.current;
|
||||
@@ -432,7 +432,7 @@ const PublishNoteSheet = ({
|
||||
{isFeatureAvailable?.isAllowed &&
|
||||
!selfDestruct &&
|
||||
monographMetadata &&
|
||||
monographMetadata?.totalViews > 0 ? (
|
||||
monographMetadata?.analytics?.totalViews > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -456,7 +456,9 @@ const PublishNoteSheet = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
|
||||
<Paragraph>{monographMetadata?.totalViews || 0}</Paragraph>
|
||||
<Paragraph>
|
||||
{monographMetadata?.analytics?.totalViews || 0}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -156,7 +156,8 @@ export function MenuItem({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{renderIcon ? (
|
||||
@@ -185,6 +186,7 @@ export function MenuItem({
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{ flexShrink: 1 }}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
@@ -195,6 +197,9 @@ export function MenuItem({
|
||||
color={
|
||||
isFocused ? colors.primary.paragraph : colors.secondary.paragraph
|
||||
}
|
||||
style={{
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{menuItemCount}
|
||||
</Paragraph>
|
||||
|
||||
@@ -72,7 +72,7 @@ export const NotebookItem = ({
|
||||
const notebook = item.notebook;
|
||||
const isFocused = focused;
|
||||
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
const getTotalNotesRef = React.useRef(getTotalNotes);
|
||||
getTotalNotesRef.current = getTotalNotes;
|
||||
const { colors } = useThemeColors();
|
||||
@@ -101,8 +101,8 @@ export const NotebookItem = ({
|
||||
item.depth === 0
|
||||
? undefined
|
||||
: item.depth < 6
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
width: "100%",
|
||||
marginTop: 2,
|
||||
opacity: item.disabled ? 0.5 : 1
|
||||
@@ -162,7 +162,8 @@ export const NotebookItem = ({
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@@ -191,8 +192,8 @@ export const NotebookItem = ({
|
||||
!item.hasChildren || disableExpand
|
||||
? "book-outline"
|
||||
: expanded
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -201,6 +202,7 @@ export const NotebookItem = ({
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{ flexShrink: 1 }}
|
||||
>
|
||||
{notebook?.title}
|
||||
</Paragraph>
|
||||
@@ -211,7 +213,8 @@ export const NotebookItem = ({
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{selectionEnabled ? (
|
||||
|
||||
@@ -56,7 +56,7 @@ const TagItem = (props: {
|
||||
const totalNotes = useTotalNotes("tag");
|
||||
const totalNotesRef = React.useRef(totalNotes);
|
||||
totalNotesRef.current = totalNotes;
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.id) {
|
||||
@@ -67,6 +67,8 @@ const TagItem = (props: {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 35,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop:
|
||||
(props.id as number) === 0 ? DefaultAppStyles.GAP_VERTICAL : 2
|
||||
@@ -123,7 +125,8 @@ const TagItem = (props: {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -146,6 +149,9 @@ const TagItem = (props: {
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item?.title}
|
||||
</Paragraph>
|
||||
@@ -173,6 +179,9 @@ const TagItem = (props: {
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{totalNotes.totalNotes(item?.id)}
|
||||
</Paragraph>
|
||||
|
||||
@@ -1333,7 +1333,7 @@ export const useActions = ({
|
||||
icon: "link",
|
||||
onPress: () => {
|
||||
const link = createInternalLink(item.type, item.id);
|
||||
Clipboard.setString(link);
|
||||
Clipboard.setHTML(`<a href="${link}" >${item.title}</a>`, link);
|
||||
ToastManager.show({
|
||||
heading: strings.linkCopied(),
|
||||
message: link,
|
||||
|
||||
@@ -766,6 +766,26 @@ export const useAppEvents = () => {
|
||||
db.eventManager.subscribe(EVENTS.uploadCanceled, (data) => {
|
||||
useAttachmentStore.getState().setUploading(data);
|
||||
}),
|
||||
db.eventManager.subscribeMulti(
|
||||
[EVENTS.vaultAutoLocked, EVENTS.vaultLocked],
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
message: strings.vaultLocked(),
|
||||
type: "info"
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
undefined
|
||||
),
|
||||
db.eventManager.subscribe(EVENTS.vaultUnlocked, () => {
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
message: strings.vaultUnlocked(),
|
||||
type: "info"
|
||||
});
|
||||
}, 300);
|
||||
}),
|
||||
EV.subscribe(EVENTS.migrationStarted, (name) => {
|
||||
if (
|
||||
name !== "notesnook" ||
|
||||
@@ -820,7 +840,13 @@ export const useAppEvents = () => {
|
||||
|
||||
return () => {
|
||||
emitterSubscriptions.forEach((sub) => sub?.remove?.());
|
||||
subscriptions.forEach((sub) => sub?.unsubscribe?.());
|
||||
subscriptions.forEach((sub) => {
|
||||
if (Array.isArray(sub)) {
|
||||
sub.forEach((s) => s.unsubscribe());
|
||||
} else {
|
||||
sub?.unsubscribe?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [onSyncComplete, onUserUpdated]);
|
||||
|
||||
|
||||
@@ -447,7 +447,9 @@ const onChangeTab = async (event: { i: number; from: number }) => {
|
||||
if (event.i === 2) {
|
||||
editorState().movedAway = false;
|
||||
editorState().isFocused = true;
|
||||
activateKeepAwake();
|
||||
if (useSettingStore.getState().settings.keepScreenOn) {
|
||||
activateKeepAwake();
|
||||
}
|
||||
eSendEvent(eOnEnterEditor);
|
||||
|
||||
if (
|
||||
|
||||
@@ -163,7 +163,20 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
|
||||
}, [noteIds]);
|
||||
|
||||
const onSave = async () => {
|
||||
const changedNotebooks = useNotebookSelectionStore.getState().selection;
|
||||
const selectionState = useNotebookSelectionStore.getState();
|
||||
const changedNotebooks = { ...selectionState.selection };
|
||||
|
||||
if (!selectionState.multiSelect) {
|
||||
for (const id in selectionState.initialState) {
|
||||
if (
|
||||
selectionState.initialState[id] === "selected" &&
|
||||
changedNotebooks[id] !== "selected"
|
||||
) {
|
||||
changedNotebooks[id] = "deselected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in changedNotebooks) {
|
||||
const item = await db.notebooks.notebook(id);
|
||||
if (!item) continue;
|
||||
@@ -207,11 +220,11 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
|
||||
rightButton={
|
||||
hasSelection
|
||||
? {
|
||||
name: "restore",
|
||||
onPress: () => {
|
||||
updateInitialSelectionState(noteIds);
|
||||
}
|
||||
name: "restore",
|
||||
onPress: () => {
|
||||
updateInitialSelectionState(noteIds);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -398,7 +398,8 @@ const TagItem = ({
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "flex-start",
|
||||
height: 40
|
||||
minHeight: 40,
|
||||
alignItems: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!tag) return;
|
||||
@@ -438,7 +439,9 @@ const TagItem = ({
|
||||
/>
|
||||
)}
|
||||
{tag ? (
|
||||
<Paragraph size={AppFontSize.sm}>{"#" + tag?.title}</Paragraph>
|
||||
<Paragraph style={{ flexShrink: 1 }} size={AppFontSize.sm}>
|
||||
{"#" + tag?.title}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -168,7 +168,7 @@ export const FailedInboxItems = () => {
|
||||
const result = usePromise(() => db.inboxItemsHistory.failed.items());
|
||||
|
||||
async function deleteItem(id: string) {
|
||||
await db.inboxItemsHistory.delete(id);
|
||||
await db.inboxItemsHistory.delete([id]);
|
||||
ToastManager.show({
|
||||
message: strings.itemDeleted(),
|
||||
type: "success"
|
||||
|
||||
@@ -210,7 +210,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
: status === SubscriptionStatus.ACTIVE
|
||||
? strings.subRenewOn(expiryDate)
|
||||
: status === SubscriptionStatus.CANCELED ||
|
||||
status === SubscriptionStatus.PAUSED
|
||||
status === SubscriptionStatus.PAUSED
|
||||
? strings.subEndsOn(expiryDate)
|
||||
: status === SubscriptionStatus.EXPIRED
|
||||
? subscriptionDaysLeft.time < -3
|
||||
@@ -965,6 +965,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
property: "checkForUpdates",
|
||||
icon: "update"
|
||||
},
|
||||
{
|
||||
id: "keep-screen-on",
|
||||
type: "switch",
|
||||
name: strings.keepScreenOn(),
|
||||
description: strings.keepScreenOnDesc(),
|
||||
property: "keepScreenOn",
|
||||
icon: "cellphone-screenshot",
|
||||
},
|
||||
{
|
||||
id: "image-compression",
|
||||
type: "component",
|
||||
@@ -1839,7 +1847,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: Platform.OS === "ios" ? "3.4.0-beta.1" : getVersion()
|
||||
description: getVersion()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -28,7 +28,17 @@ import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { scale, updateSize } from "../utils/size";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
ScreenGuardModule.initSettings();
|
||||
|
||||
let isScreenGuardModuleReady = false;
|
||||
async function callScreenGuard(callback: () => void) {
|
||||
try {
|
||||
if (!isScreenGuardModuleReady) {
|
||||
await ScreenGuardModule.initSettings();
|
||||
isScreenGuardModuleReady = true;
|
||||
}
|
||||
callback();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
const settings = get();
|
||||
@@ -129,15 +139,21 @@ function setPrivacyScreen(enabled?: boolean) {
|
||||
if (enabled) {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
ScreenGuardModule.registerWithoutEffect();
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.registerWithoutEffect();
|
||||
});
|
||||
} else {
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(false);
|
||||
}
|
||||
ScreenGuardModule.unregister();
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +272,8 @@ export const SettingsService = {
|
||||
canLockAppInBackground,
|
||||
appEnteredBackground,
|
||||
setPrivacyScreen,
|
||||
getBackgroundEnterTime
|
||||
getBackgroundEnterTime,
|
||||
callScreenGuard
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
@@ -104,6 +104,7 @@ export type Settings = {
|
||||
checkForUpdates?: boolean;
|
||||
defaultLineHeight: number;
|
||||
imageCompression: "ask-every-time" | "enabled" | "disabled";
|
||||
keepScreenOn?: boolean;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -213,7 +214,8 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
lastFullBackupDate: 0,
|
||||
checkForUpdates: true,
|
||||
defaultLineHeight: EDITOR_LINE_HEIGHT.DEFAULT,
|
||||
imageCompression: "ask-every-time"
|
||||
imageCompression: "ask-every-time",
|
||||
keepScreenOn: true
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
// Production iOS build identifiers
|
||||
IOS_CURRENT_PROJECT_VERSION = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
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 = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
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 = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
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
|
||||
|
||||
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.0-beta.1",
|
||||
"version": "3.4.7",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -18,7 +18,7 @@
|
||||
"e2e-android": "detox test --configuration android.emu.release --detectOpenHandles",
|
||||
"e2e-android-debug": "detox test --configuration android.emu.debug --detectOpenHandles",
|
||||
"e2e-ios": "detox test -c ios.sim.release --detectOpenHandles",
|
||||
"bump": "npx react-native bump-version --skip-semver-for android",
|
||||
"bump": "node ./scripts/bump-version.mjs",
|
||||
"release-android": "cd android && GITHUB_RELEASE=true ENVFILE=.env.public ./gradlew assembleRelease --no-daemon",
|
||||
"release-android-bundle": "cd android && ./gradlew bundleRelease --no-daemon",
|
||||
"lint": "eslint --ext .js,.jsx,.tsx,.ts ./app"
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/android/.settings/org.eclipse.buildship.core.prefs b/node_modules/@react-native-clipboard/clipboard/android/.settings/org.eclipse.buildship.core.prefs
|
||||
deleted file mode 100644
|
||||
index 1c6d50d..0000000
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/android/.settings/org.eclipse.buildship.core.prefs
|
||||
+++ /dev/null
|
||||
@@ -1,13 +0,0 @@
|
||||
-arguments=
|
||||
-auto.sync=false
|
||||
-build.scans.enabled=false
|
||||
-connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.4.2))
|
||||
-connection.project.dir=../example/android
|
||||
-eclipse.preferences.version=1
|
||||
-gradle.user.home=
|
||||
-java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home
|
||||
-jvm.arguments=
|
||||
-offline.mode=false
|
||||
-override.workspace.settings=true
|
||||
-show.console.view=true
|
||||
-show.executions.view=true
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/android/gradle/.settings/org.eclipse.buildship.core.prefs b/node_modules/@react-native-clipboard/clipboard/android/gradle/.settings/org.eclipse.buildship.core.prefs
|
||||
deleted file mode 100644
|
||||
index e889521..0000000
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/android/gradle/.settings/org.eclipse.buildship.core.prefs
|
||||
+++ /dev/null
|
||||
@@ -1,2 +0,0 @@
|
||||
-connection.project.dir=
|
||||
-eclipse.preferences.version=1
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java b/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java
|
||||
index b68b858..324b293 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java
|
||||
@@ -202,6 +202,37 @@ public class ClipboardModule extends NativeClipboardModuleSpec {
|
||||
}
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ public void setHTML(String html, String text) {
|
||||
+ try {
|
||||
+ ClipData clipdata = ClipData.newHtmlText(null, text, html);
|
||||
+ ClipboardManager clipboard = getClipboardService();
|
||||
+ clipboard.setPrimaryClip(clipdata);
|
||||
+ } catch (Exception e) {
|
||||
+ e.printStackTrace();
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ public void getHTML(Promise promise) {
|
||||
+ try {
|
||||
+ ClipboardManager clipboard = getClipboardService();
|
||||
+ ClipData clipData = clipboard.getPrimaryClip();
|
||||
+
|
||||
+ if (clipData == null
|
||||
+ || clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)
|
||||
+ || clipData.getItemCount() < 1) {
|
||||
+ promise.resolve("");
|
||||
+ return;
|
||||
+ }
|
||||
+ ClipData.Item item = clipData.getItemAt(0);
|
||||
+ promise.resolve(item.getHtmlText());
|
||||
+
|
||||
+ } catch (Exception e) {
|
||||
+ promise.reject(e);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@ReactMethod
|
||||
public void setListener() {
|
||||
try {
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts
|
||||
index 6c85195..afe3a5d 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts
|
||||
@@ -22,6 +22,9 @@ export declare const Clipboard: {
|
||||
* ```
|
||||
*/
|
||||
getStrings(): Promise<string[]>;
|
||||
+
|
||||
+ getHTML(): Promise<string>;
|
||||
+ setHTML(html: string, content: string): void;
|
||||
/**
|
||||
* Get clipboard image as PNG in base64, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js
|
||||
index ccdf309..03e7a42 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js
|
||||
@@ -30,6 +30,12 @@ const NativeClipboardModule_1 = __importStar(require("./NativeClipboardModule"))
|
||||
* `Clipboard` gives you an interface for setting and getting content from Clipboard on both iOS and Android
|
||||
*/
|
||||
exports.Clipboard = {
|
||||
+ getHTML() {
|
||||
+ return NativeClipboardModule_1.default.getHTML();
|
||||
+ },
|
||||
+ setHTML(html, text) {
|
||||
+ return NativeClipboardModule_1.default.setHTML(html, text);
|
||||
+ },
|
||||
/**
|
||||
* Get content of string type, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts
|
||||
index 5440157..75f346c 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts
|
||||
@@ -2,6 +2,8 @@ import type { EmitterSubscription } from 'react-native';
|
||||
import type { TurboModule } from 'react-native';
|
||||
export interface Spec extends TurboModule {
|
||||
getConstants: () => {};
|
||||
+ setHTML(html: string, content: string): void;
|
||||
+ getHTML(): Promise<string>;
|
||||
getString: () => Promise<string>;
|
||||
getStrings: () => Promise<string[]>;
|
||||
setString: (content: string) => void;
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts
|
||||
index 0b2111e..b395af2 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts
|
||||
@@ -20,6 +20,8 @@ export interface Spec extends TurboModule {
|
||||
* ```
|
||||
*/
|
||||
getStrings(): Promise<string[]>;
|
||||
+ setHTML(html: string, content: string): void;
|
||||
+ getHTML(): Promise<string>;
|
||||
/**
|
||||
* Get clipboard image as PNG in base64, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm b/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm
|
||||
index 4e38fd2..5dea6a7 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm
|
||||
@@ -87,6 +87,29 @@ - (void) listener:(NSNotification *) notification
|
||||
resolve((clipboard.strings ? : @[]));
|
||||
}
|
||||
|
||||
+RCT_EXPORT_METHOD(setHTML:(nonnull NSString *)html
|
||||
+ content:(nonnull NSString*)content
|
||||
+) {
|
||||
+ UIPasteboard *clipboard = [UIPasteboard generalPasteboard];
|
||||
+ NSDictionary *htmlDict = [NSDictionary dictionaryWithObjectsAndKeys:html, (NSString*)kUTTypeHTML, nil];
|
||||
+ NSDictionary *textDict = [NSDictionary dictionaryWithObjectsAndKeys:content, (NSString*)kUTTypePlainText, nil];
|
||||
+ clipboard.items = [NSArray arrayWithObjects:htmlDict, textDict, nil];
|
||||
+}
|
||||
+
|
||||
+RCT_EXPORT_METHOD(getHTML:(RCTPromiseResolveBlock)resolve
|
||||
+ reject:(__unused RCTPromiseRejectBlock)reject)
|
||||
+{
|
||||
+ UIPasteboard *clipboard = [UIPasteboard generalPasteboard];
|
||||
+ id value = [clipboard valueForPasteboardType:(NSString*)kUTTypeHTML];
|
||||
+ NSString *html = nil;
|
||||
+ if ([value isKindOfClass:[NSString class]]) {
|
||||
+ html = (NSString *)value;
|
||||
+ } else if ([value isKindOfClass:[NSData class]]) {
|
||||
+ html = [[NSString alloc] initWithData:(NSData *)value encoding:NSUTF8StringEncoding];
|
||||
+ }
|
||||
+ resolve((html ? : @""));
|
||||
+}
|
||||
+
|
||||
RCT_EXPORT_METHOD(setImage:(NSString *)content
|
||||
resolve:(RCTPromiseResolveBlock) resolve
|
||||
reject:(RCTPromiseRejectBlock) reject
|
||||
@@ -238,10 +261,11 @@ - (void) listener:(NSNotification *) notification
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
-RCT_EXPORT_METHOD(removeListeners : (double)count) {
|
||||
+RCT_EXPORT_METHOD(removeListeners : (long)count) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
+
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
||||
(const facebook::react::ObjCTurboModule::InitParams &)params
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts b/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts
|
||||
index d5e61fc..b010a22 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts
|
||||
@@ -31,6 +31,18 @@ export const Clipboard = {
|
||||
getStrings(): Promise<string[]> {
|
||||
return NativeClipboard.getStrings();
|
||||
},
|
||||
+
|
||||
+ /**
|
||||
+ * Get content of HTML type, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
+ * ```javascript
|
||||
+ * async _getContent() {
|
||||
+ * var content = await Clipboard.getHTML();
|
||||
+ * }
|
||||
+ * ```
|
||||
+ */
|
||||
+ getHTML(): Promise<string> {
|
||||
+ return NativeClipboard.getHTML();
|
||||
+ },
|
||||
/**
|
||||
* Get clipboard image as PNG in base64, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
@@ -106,6 +118,19 @@ export const Clipboard = {
|
||||
setStrings(content: string[]) {
|
||||
NativeClipboard.setStrings(content);
|
||||
},
|
||||
+ /**
|
||||
+ * Set content of HTML type. You can use following code to set clipboard content
|
||||
+ * ```javascript
|
||||
+ * _setContent() {
|
||||
+ * Clipboard.setHTML('<b>hello world</b>', 'hello world');
|
||||
+ * }
|
||||
+ * ```
|
||||
+ * @param {string} content the HTML to be stored in the clipboard.
|
||||
+ * @param {string} text text to be used inplace of HTML be stored in the clipboard.
|
||||
+ */
|
||||
+ setHTML(content: string, text: string) {
|
||||
+ NativeClipboard.setHTML(content, text);
|
||||
+ },
|
||||
/**
|
||||
* Returns whether the clipboard has content or is empty.
|
||||
* This method returns a `Promise`, so you can use following code to get clipboard content
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts b/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts
|
||||
index 2373747..a094285 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts
|
||||
@@ -78,6 +78,9 @@ export interface Spec extends TurboModule {
|
||||
* @param the content to be stored in the clipboard.
|
||||
*/
|
||||
setString(content: string): void;
|
||||
+
|
||||
+ setHTML(html: string, content: string): void;
|
||||
+ getHTML(): Promise<string>;
|
||||
/**
|
||||
* Set content of string array type. You can use following code to set clipboard content
|
||||
* ```javascript
|
||||
194
apps/mobile/scripts/bump-version.mjs
Normal file
194
apps/mobile/scripts/bump-version.mjs
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const PROJECT_ROOT = path.resolve(
|
||||
new URL(import.meta.url).pathname,
|
||||
"..",
|
||||
".."
|
||||
); // scripts/.. -> mobile
|
||||
|
||||
const REPO_ROOT = path.resolve(PROJECT_ROOT, "..", ".."); // mobile/../.. -> repo root
|
||||
const IOS_BUILD_CONFIGS_DIR = path.resolve(PROJECT_ROOT, "ios/build-configs");
|
||||
const ANDROID_GRADLE = path.resolve(PROJECT_ROOT, "android/app/build.gradle");
|
||||
const PACKAGE_JSON = path.resolve(PROJECT_ROOT, "package.json");
|
||||
const FDROID_CHANGELOGS_DIR = path.resolve(
|
||||
REPO_ROOT,
|
||||
"fastlane/metadata/android/en-US/changelogs"
|
||||
);
|
||||
|
||||
async function resolveVersion() {
|
||||
const provided = process.argv[2];
|
||||
if (provided) {
|
||||
if (!/^\d+\.\d+\.\d+$/.test(provided)) {
|
||||
console.error(`Invalid version "${provided}". Expected format: x.y.z`);
|
||||
process.exit(1);
|
||||
}
|
||||
return provided;
|
||||
}
|
||||
// No version provided: bump the current package.json version as a patch.
|
||||
const pkg = JSON.parse(await fs.readFile(PACKAGE_JSON, "utf8"));
|
||||
const match = String(pkg.version).match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!match) {
|
||||
console.error(
|
||||
`Cannot auto-bump: package.json version "${pkg.version}" is not x.y.z`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const [, major, minor, patch] = match;
|
||||
return `${major}.${minor}.${Number(patch) + 1}`;
|
||||
}
|
||||
|
||||
const version = await resolveVersion();
|
||||
|
||||
async function updatePackageJson() {
|
||||
const raw = await fs.readFile(PACKAGE_JSON, "utf8");
|
||||
const pkg = JSON.parse(raw);
|
||||
const from = pkg.version;
|
||||
pkg.version = version;
|
||||
// Preserve trailing newline if present.
|
||||
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
||||
await fs.writeFile(
|
||||
PACKAGE_JSON,
|
||||
JSON.stringify(pkg, null, 2) + trailingNewline
|
||||
);
|
||||
console.log(`package.json: version ${from} -> ${version}`);
|
||||
}
|
||||
|
||||
async function updateAndroid() {
|
||||
let content = await fs.readFile(ANDROID_GRADLE, "utf8");
|
||||
const match = content.match(/versionCode\s+(\d+)/);
|
||||
if (!match) {
|
||||
throw new Error("Could not find `versionCode <number>` in build.gradle");
|
||||
}
|
||||
const current = parseInt(match[1], 10);
|
||||
const next = current + 1;
|
||||
content = content.replace(match[0], `versionCode ${next}`);
|
||||
await fs.writeFile(ANDROID_GRADLE, content);
|
||||
console.log(`android/app/build.gradle: versionCode ${current} -> ${next}`);
|
||||
return next;
|
||||
}
|
||||
|
||||
// Derives the F-Droid release version code from build.gradle. F-Droid builds a
|
||||
// separate APK per ABI (see `applicationVariants.all` in build.gradle) using:
|
||||
// output.versionCodeOverride = defaultConfig.versionCode * <mult> + versionCodes[abi]
|
||||
// and publishes the highest of those. So the changelog file is named after
|
||||
// `versionCode * <mult> + max(versionCodes)`. Both are parsed here rather than
|
||||
// hardcoded so this stays in sync with the formula in build.gradle.
|
||||
async function computeFdroidVersionCode(versionCode) {
|
||||
const gradle = await fs.readFile(ANDROID_GRADLE, "utf8");
|
||||
|
||||
const multMatch = gradle.match(/defaultConfig\.versionCode\s*\*\s*(\d+)/);
|
||||
if (!multMatch) {
|
||||
throw new Error(
|
||||
"Could not find the F-Droid versionCode multiplier in build.gradle"
|
||||
);
|
||||
}
|
||||
const multiplier = parseInt(multMatch[1], 10);
|
||||
|
||||
const mapMatch = gradle.match(/versionCodes\s*=\s*\[([^\]]*)\]/);
|
||||
if (!mapMatch) {
|
||||
throw new Error("Could not find the ABI versionCodes map in build.gradle");
|
||||
}
|
||||
const abiCodes = [...mapMatch[1].matchAll(/:\s*(\d+)/g)].map((m) =>
|
||||
parseInt(m[1], 10)
|
||||
);
|
||||
if (abiCodes.length === 0) {
|
||||
throw new Error("ABI versionCodes map in build.gradle is empty");
|
||||
}
|
||||
const maxAbiCode = Math.max(...abiCodes);
|
||||
|
||||
return versionCode * multiplier + maxAbiCode;
|
||||
}
|
||||
|
||||
async function createFdroidChangelog(versionCode) {
|
||||
const fdroidVersionCode = await computeFdroidVersionCode(versionCode);
|
||||
const file = path.resolve(FDROID_CHANGELOGS_DIR, `${fdroidVersionCode}.txt`);
|
||||
const rel = path.relative(REPO_ROOT, file);
|
||||
|
||||
try {
|
||||
await fs.access(file);
|
||||
console.log(`${rel}: already exists, skipped`);
|
||||
return;
|
||||
} catch {
|
||||
// File does not exist yet — create it.
|
||||
}
|
||||
|
||||
const placeholder = "- Minor bug fixes and improvements\n\nThank you for using Notesnook!\n";
|
||||
await fs.mkdir(FDROID_CHANGELOGS_DIR, { recursive: true });
|
||||
await fs.writeFile(file, placeholder);
|
||||
console.log(`${rel}: created (edit with this release's changelog)`);
|
||||
}
|
||||
|
||||
async function updateIosConfig(file) {
|
||||
let content = await fs.readFile(file, "utf8");
|
||||
const rel = path.relative(PROJECT_ROOT, file);
|
||||
|
||||
const buildMatch = content.match(/IOS_CURRENT_PROJECT_VERSION\s*=\s*(\d+)/);
|
||||
if (!buildMatch) {
|
||||
throw new Error(`Could not find IOS_CURRENT_PROJECT_VERSION in ${rel}`);
|
||||
}
|
||||
const currentBuild = parseInt(buildMatch[1], 10);
|
||||
const nextBuild = currentBuild + 1;
|
||||
content = content.replace(
|
||||
buildMatch[0],
|
||||
`IOS_CURRENT_PROJECT_VERSION = ${nextBuild}`
|
||||
);
|
||||
|
||||
const marketingMatch = content.match(/IOS_MARKETING_VERSION\s*=\s*(\S+)/);
|
||||
if (!marketingMatch) {
|
||||
throw new Error(`Could not find IOS_MARKETING_VERSION in ${rel}`);
|
||||
}
|
||||
const currentMarketing = marketingMatch[1];
|
||||
content = content.replace(
|
||||
marketingMatch[0],
|
||||
`IOS_MARKETING_VERSION = ${version}`
|
||||
);
|
||||
|
||||
await fs.writeFile(file, content);
|
||||
console.log(
|
||||
`${rel}: version ${currentMarketing} -> ${version}, build ${currentBuild} -> ${nextBuild}`
|
||||
);
|
||||
}
|
||||
|
||||
async function updateIos() {
|
||||
const entries = await fs.readdir(IOS_BUILD_CONFIGS_DIR);
|
||||
const configs = entries.filter((e) => e.endsWith(".xcconfig"));
|
||||
if (configs.length === 0) {
|
||||
throw new Error("No .xcconfig files found in ios/build-configs");
|
||||
}
|
||||
for (const config of configs) {
|
||||
await updateIosConfig(path.resolve(IOS_BUILD_CONFIGS_DIR, config));
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log(`Bumping to version ${version}...\n`);
|
||||
await updatePackageJson();
|
||||
const androidVersionCode = await updateAndroid();
|
||||
await updateIos();
|
||||
await createFdroidChangelog(androidVersionCode);
|
||||
console.log("\nDone.");
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error("Failed:", err?.message || err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/vericrypt",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/platform": "^1.3.6",
|
||||
@@ -43,4 +43,4 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import { PasteEncryptedData, SyncRequestBody } from "./components/step-4";
|
||||
import { StepSeperator } from "./components/step-seperator";
|
||||
import { Footer } from "./components/footer";
|
||||
import { useState } from "react";
|
||||
import { NNCrypto } from "@notesnook/crypto";
|
||||
import { NNCrypto, Cipher } from "@notesnook/crypto";
|
||||
import { Code } from "./components/code";
|
||||
import { Accordion } from "./components/accordion";
|
||||
import { DecryptedResult } from "./components/decrypted-result";
|
||||
@@ -41,7 +41,7 @@ const instructions = [
|
||||
"Go to Notesnook",
|
||||
"Open Settings",
|
||||
<>
|
||||
Click on <Code text="Backup data recovery key" />
|
||||
Click on <Code text="Save data recovery key" />
|
||||
</>,
|
||||
"Enter your account password for verification",
|
||||
"Confirm that your generated encryption key matches"
|
||||
@@ -52,6 +52,7 @@ function App() {
|
||||
const [salt, setSalt] = useState<string>();
|
||||
const [key, setKey] = useState<string>();
|
||||
const [data, setData] = useState<SyncRequestBody | undefined>();
|
||||
const [accountDataKey, setAccountDataKey] = useState<Cipher<"base64"> | null | undefined>();
|
||||
const theme = useTheme({ accent: getDefaultAccentColor(), theme: "light" });
|
||||
|
||||
return (
|
||||
@@ -67,7 +68,7 @@ function App() {
|
||||
>
|
||||
<LoginToNotesnook />
|
||||
<StepSeperator />
|
||||
<GetAccountSalt onSaltSubmitted={setSalt} />
|
||||
<GetAccountSalt onSaltSubmitted={setSalt} setAccountDataKey={setAccountDataKey} />
|
||||
|
||||
{salt && (
|
||||
<>
|
||||
@@ -91,7 +92,7 @@ function App() {
|
||||
return true;
|
||||
}}
|
||||
popup={{
|
||||
title: "Your data encryption key",
|
||||
title: "Your master encryption key",
|
||||
body: key ? (
|
||||
<>
|
||||
<Text
|
||||
@@ -146,11 +147,13 @@ function App() {
|
||||
password={password}
|
||||
salt={salt}
|
||||
data={data}
|
||||
accountKey={accountDataKey}
|
||||
onRestartProcess={() => {
|
||||
setSalt(undefined);
|
||||
setPassword(undefined);
|
||||
setKey(undefined);
|
||||
setData(undefined);
|
||||
setAccountDataKey(undefined);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
BIN
apps/vericrypt/src/assets/screenshots/devtools_copy_users.png
Normal file
BIN
apps/vericrypt/src/assets/screenshots/devtools_copy_users.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Flex, Button, Text, Link } from "@theme-ui/components";
|
||||
import { StepContainer } from "./step-container";
|
||||
import { SyncRequestBody } from "./step-4";
|
||||
import { NNCrypto } from "@notesnook/crypto";
|
||||
import { NNCrypto, Cipher } from "@notesnook/crypto";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FcDataEncryption } from "react-icons/fc";
|
||||
import { Code } from "./code";
|
||||
@@ -32,6 +32,7 @@ type DecryptedResultProps = {
|
||||
password: string;
|
||||
salt: string;
|
||||
data: SyncRequestBody;
|
||||
accountKey: Cipher<"base64"> | null | undefined;
|
||||
onRestartProcess: () => void;
|
||||
};
|
||||
|
||||
@@ -51,10 +52,19 @@ export function DecryptedResult(props: DecryptedResultProps) {
|
||||
};
|
||||
const crypto = new NNCrypto();
|
||||
const key = await crypto.exportKey(props.password, props.salt);
|
||||
let encryptionKey = undefined;
|
||||
if (props.accountKey != null){
|
||||
// Need to decrypt the account key
|
||||
props.accountKey.format = "base64"; // account keys don't have the format set, but they're base64.
|
||||
const dataEncryptionKey = JSON.parse(await crypto.decrypt(key, props.accountKey, "text"))
|
||||
encryptionKey = dataEncryptionKey;
|
||||
} else {
|
||||
encryptionKey = key;
|
||||
}
|
||||
for (const arrayKey in data) {
|
||||
const array = data[arrayKey];
|
||||
for (const encryptedItem of (props.data as any)[arrayKey]) {
|
||||
const data = await crypto.decrypt(key, encryptedItem, "text");
|
||||
const data = await crypto.decrypt(encryptionKey, encryptedItem, "text");
|
||||
array.push(JSON.parse(data));
|
||||
}
|
||||
}
|
||||
@@ -150,19 +160,9 @@ export function DecryptedResult(props: DecryptedResultProps) {
|
||||
support@streetwriters.co
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<Link href="https://discord.gg/">joining our Discord community</Link>.
|
||||
<Link href="https://go.notesnook.com/discord">join our Discord community</Link>.
|
||||
We'll do our best to alleviate all your worries.
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mx: 2, mt: 2, fontWeight: "bold" }}>
|
||||
What about open sourcing Notesnook?
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mx: 2, my: 2 }}>
|
||||
Open sourcing is another part of garnering our users' trust. We
|
||||
have <Link href="https://notesnook.com/roadmap">plans</Link> to begin
|
||||
open sourcing in May but open sourcing will not make this tool
|
||||
obsolete. Verifying the integrity of encrypted data at any point in
|
||||
time is very important even if the software is open source.
|
||||
</Text>
|
||||
</Accordion>
|
||||
<Flex sx={{ alignSelf: "center", mt: 4 }}>
|
||||
<Button
|
||||
|
||||
@@ -38,11 +38,6 @@ export function LoginToNotesnook() {
|
||||
>
|
||||
<Text variant="title">Welcome to Vericrypt</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
<del>
|
||||
Trust is a huge problem in closed source end-to-end encrypted
|
||||
applications. How can you be sure that the app is actually encrypting
|
||||
your data?
|
||||
</del>
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
The only way to earn a user's trust is by allowing them to see how
|
||||
@@ -56,9 +51,10 @@ export function LoginToNotesnook() {
|
||||
</Link>
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
Yes, that's right. Notesnook is now 100% open source under the
|
||||
GPLv3 license. That includes the app, the encryption library, the
|
||||
backend server, and everything else.
|
||||
Yes, that's right. Notesnook is 100% open source under the
|
||||
GPLv3 license. That includes the app, the encryption library, and
|
||||
everything else. We've even open sourced the server, available under the
|
||||
AGPLv3 license.
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
However, even with an open source app, how can you be sure that the app
|
||||
@@ -86,7 +82,7 @@ export function LoginToNotesnook() {
|
||||
sx={{ alignSelf: "center", mt: 2 }}
|
||||
onClick={() => window.open("https://app.notesnook.com/login", "_blank")}
|
||||
>
|
||||
Login to Notesnook
|
||||
Get Started by Logging in
|
||||
</Button>
|
||||
</StepContainer>
|
||||
);
|
||||
|
||||
@@ -16,11 +16,11 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Flex, Text, Image, Input } from "@theme-ui/components";
|
||||
import { Flex, Text, Image, Textarea } from "@theme-ui/components";
|
||||
import { StepContainer } from "./step-container";
|
||||
import DevtoolsCopySaltChrome from "../assets/screenshots/devtools_copy_salt.png";
|
||||
import DevtoolsCopyUsersChrome from "../assets/screenshots/devtools_copy_users.png";
|
||||
|
||||
import DevtoolsCopySaltFirefox from "../assets/screenshots/firefox/firefox_copy_salt.png";
|
||||
import DevtoolsCopyUsersFirefox from "../assets/screenshots/firefox/firefox_copy_users.png";
|
||||
|
||||
import { Accordion } from "./accordion";
|
||||
import { getCombo } from "../utils/keycombos";
|
||||
@@ -29,9 +29,11 @@ import { KeyCombo } from "./key-combo";
|
||||
import { Code } from "./code";
|
||||
import { useState } from "react";
|
||||
import { getSourceUrl } from "../utils/links";
|
||||
import { Cipher } from "@notesnook/crypto";
|
||||
|
||||
type GetAccountSaltProps = {
|
||||
onSaltSubmitted: (salt: string) => void;
|
||||
setAccountDataKey: (accountDataKey: Cipher<"base64"> | null) => void;
|
||||
};
|
||||
|
||||
const steps = {
|
||||
@@ -52,10 +54,10 @@ const steps = {
|
||||
</Text>,
|
||||
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Follow the steps as shown in the image:</Text>
|
||||
<Image src={DevtoolsCopySaltChrome} width={"100%"} sx={{ mt: 1 }} />
|
||||
<Image src={DevtoolsCopyUsersChrome} width={"100%"} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<Flex key="copy-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Copy the salt and paste it below.</Text>
|
||||
<Text as="p">Copy everything in the response and paste it below.</Text>
|
||||
</Flex>
|
||||
],
|
||||
firefox: [
|
||||
@@ -75,10 +77,10 @@ const steps = {
|
||||
</Text>,
|
||||
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Follow the steps as shown in the image:</Text>
|
||||
<Image src={DevtoolsCopySaltFirefox} width={"100%"} sx={{ mt: 1 }} />
|
||||
<Image src={DevtoolsCopyUsersFirefox} width={"100%"} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<Flex key="copy-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Copy the salt and paste it below.</Text>
|
||||
<Text as="p">Copy everything in the response and paste it below.</Text>
|
||||
</Flex>
|
||||
]
|
||||
};
|
||||
@@ -114,14 +116,14 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
sx={{ flexDirection: "column" }}
|
||||
>
|
||||
<Flex sx={{ justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text variant="title">Account salt</Text>
|
||||
<Text variant="title">Account Key Data</Text>
|
||||
<Code
|
||||
text="src/components/step-2.tsx"
|
||||
href={getSourceUrl("src/components/step-2.tsx")}
|
||||
/>
|
||||
</Flex>
|
||||
<Accordion
|
||||
title="How to get your account salt?"
|
||||
title="How to get your key data?"
|
||||
sx={{
|
||||
border: "1px solid var(--border)",
|
||||
mt: 2,
|
||||
@@ -129,9 +131,9 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
}}
|
||||
>
|
||||
<Text variant="body" sx={{ mx: 2 }}>
|
||||
We'll be extracting your account's salt right from
|
||||
You'll be extracting your account's keys right from
|
||||
Notesnook's local database that lives in your web browser. So put
|
||||
on your seat belt and let's get some salt!
|
||||
on your seat belt and let's retrieve those keys.
|
||||
</Text>
|
||||
<Text as="ol" variant="body" sx={{ mb: 2 }}>
|
||||
{instructions?.map((item, index) => (
|
||||
@@ -141,12 +143,11 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
))}
|
||||
</Text>
|
||||
</Accordion>
|
||||
<Input
|
||||
<Textarea
|
||||
variant="forms.clean"
|
||||
id="salt"
|
||||
name="salt"
|
||||
type="text"
|
||||
placeholder="Enter your account salt"
|
||||
id="account-data"
|
||||
name="account-data"
|
||||
placeholder="Paste the response here"
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontSize: "subheading",
|
||||
@@ -162,13 +163,21 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
spellCheck={false}
|
||||
onChange={(e) => {
|
||||
setIsSaltValid(undefined);
|
||||
/*
|
||||
If there is no dataEncryptionKey, try reading the legacyDataEncryptionKey.
|
||||
If there is a legacyDataEncryptionKey, use this one.
|
||||
If there is no legacyDataEncryptionKey, it is a legacy account (pre DEK update) that has not
|
||||
changed passwords. (Use master key instead.)
|
||||
*/
|
||||
try {
|
||||
const value = e.target.value;
|
||||
const isValid = Buffer.from(value, "base64").length === 16;
|
||||
const value = JSON.parse(e.target.value);
|
||||
props.setAccountDataKey(value.dataEncryptionKey ?? value.legacyDataEncryptionKey ?? null);
|
||||
const salt = value.salt;
|
||||
const isValid = Buffer.from(salt, "base64").length === 16;
|
||||
if (!isValid) setIsSaltValid(false);
|
||||
else {
|
||||
setIsSaltValid(true);
|
||||
props.onSaltSubmitted(value);
|
||||
props.onSaltSubmitted(salt);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
85
apps/web/package-lock.json
generated
85
apps/web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"@lingui/react": "5.1.2",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/react": "1.6.1",
|
||||
"@notesnook-importer/core": "^2.4.5",
|
||||
"@notesnook-importer/core": "^2.5.0",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
@@ -63,6 +63,7 @@
|
||||
"katex": "0.16.11",
|
||||
"mac-scrollbar": "0.13.6",
|
||||
"mutative": "^1.1.0",
|
||||
"p-queue": "^9.3.0",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"phone": "^3.1.14",
|
||||
"platform": "^1.3.6",
|
||||
@@ -89,7 +90,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.5",
|
||||
"@cloudflare/workers-types": "^4.20250224.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@swc/plugin-react-remove-properties": "^6.0.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
@@ -133,7 +134,7 @@
|
||||
},
|
||||
"../../extensions/web-clipper": {
|
||||
"name": "@notesnook/web-clipper",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
@@ -302,7 +303,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/core": "^2.4.5",
|
||||
"@notesnook-importer/core": "^2.5.0",
|
||||
"@notesnook/common": "file:../common",
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
@@ -509,7 +510,7 @@
|
||||
},
|
||||
"../desktop": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.4.0-beta.0",
|
||||
"version": "3.4.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -529,16 +530,18 @@
|
||||
"zod": "3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.61.1",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron": "^37.10.3",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "0.21.5",
|
||||
"fkill": "^10.0.3",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
"playwright": "1.48.2",
|
||||
"playwright-core": "1.61.1",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -2873,14 +2876,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@notesnook-importer/core": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/core/-/core-2.4.5.tgz",
|
||||
"integrity": "sha512-7Oqkw07CykwC/xzGrC6St4C/87QmXqK8UtmSxwE05fpz8DWoKEviJSWFa5fKY52umTphGvmtY23252dZ9J9EJg==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/core/-/core-2.5.0.tgz",
|
||||
"integrity": "sha512-NaStLjp/ov/WEQZthmYZ0LTxVvk90kB6lWJaA4fz3hIrGxxAJlX5QLfc/L69l5ZPVv1TouMXLlFD6WP7koPX2Q==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/enex": "^2.3.5",
|
||||
"@notesnook-importer/storage": "^2.3.5",
|
||||
"@notesnook-importer/znel": "^2.3.5",
|
||||
"@notesnook-importer/enex": "^2.4.0",
|
||||
"@notesnook-importer/storage": "^2.4.0",
|
||||
"@notesnook-importer/znel": "^2.4.0",
|
||||
"@stablelib/chacha20poly1305": "^1.0.1",
|
||||
"@streamparser/json": "^0.0.10",
|
||||
"@zip.js/zip.js": "^2.7.32",
|
||||
@@ -2927,9 +2930,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@notesnook-importer/enex": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/enex/-/enex-2.3.5.tgz",
|
||||
"integrity": "sha512-0+08XVhsQQFoEUrar3k07DuTluNho/FHxJ25n/DT+ug8kVraHHVqI40a2P9h8bW8iWxYs02Zq1n+N2uTv5qb+g==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/enex/-/enex-2.4.0.tgz",
|
||||
"integrity": "sha512-DefPCH8D+pRarOeZtIwHOMB/CidtzOIdo2qbBPkQW3Uh685zEx81RlrCYtC/765HXUOKQlHLDNuNebRLPljkeg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -2942,15 +2945,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@notesnook-importer/storage": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/storage/-/storage-2.3.5.tgz",
|
||||
"integrity": "sha512-orvSK0XIcqlmiWmjQ3RzS3qZp7viBdCpAloX557Ml200LMSMGDsOtofYNv5IYllBETnqApERLPQaUzBSuviuvA==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/storage/-/storage-2.4.0.tgz",
|
||||
"integrity": "sha512-AUnr5cfG0q4hduNNDJ/M6SiPPAWLlbUpBQ/0qNM5wPVGFY7C2z0NzKGJY7tTTB7L4t9IABOw6g5yutj68dkOVw==",
|
||||
"license": "GPL-3.0-or-later"
|
||||
},
|
||||
"node_modules/@notesnook-importer/znel": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/znel/-/znel-2.3.5.tgz",
|
||||
"integrity": "sha512-s1kTA3EQ5OswBc0RvWnWgIm6/mIWvnh3iWmcOJcApIqhDwQziEkYiS+UMfuY9nOUyIWtepMe/WJ0Odk15uah9w==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/znel/-/znel-2.4.0.tgz",
|
||||
"integrity": "sha512-EM3yLZvvvbAPgkze82RyrFaArtdKUTp6m7IzDy7ySraDXdXfFGC8YeP9FcDkNfCrxzhPpkPwIsvHQw54QyKLGg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -6666,6 +6669,12 @@
|
||||
"integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exenv": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
||||
@@ -9800,6 +9809,34 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz",
|
||||
"integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.4",
|
||||
"p-timeout": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
|
||||
"integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.4",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@lingui/react": "5.1.2",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/react": "1.6.1",
|
||||
"@notesnook-importer/core": "^2.4.5",
|
||||
"@notesnook-importer/core": "^2.5.0",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
@@ -61,6 +61,7 @@
|
||||
"katex": "0.16.11",
|
||||
"mac-scrollbar": "0.13.6",
|
||||
"mutative": "^1.1.0",
|
||||
"p-queue": "^9.3.0",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"phone": "^3.1.14",
|
||||
"platform": "^1.3.6",
|
||||
|
||||
@@ -64,6 +64,11 @@ function attachListeners() {
|
||||
attachListener(AppEvents.updateError)
|
||||
);
|
||||
|
||||
desktop.bridge.onOpenLink.subscribe(
|
||||
undefined,
|
||||
attachListener(AppEvents.onOpenLink)
|
||||
);
|
||||
|
||||
TaskScheduler.register("updateCheck", "0 0 */12 * * * *", () => {
|
||||
checkForUpdate(settingStore.get().autoUpdates);
|
||||
});
|
||||
|
||||
@@ -142,7 +142,13 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
|
||||
<Flex sx={{ flexDirection: "column" }} p={4} pb={0}>
|
||||
{props.title && (
|
||||
<Flex
|
||||
sx={{ alignItems: "center", justifyContent: "space-between" }}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent:
|
||||
props.textAlignment === "center"
|
||||
? "center"
|
||||
: "space-between"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="heading"
|
||||
|
||||
@@ -589,7 +589,7 @@ export function Editor(props: EditorProps) {
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment)
|
||||
throw new Error("No attachment found with hash: " + hash);
|
||||
if (attachment.mimeType.startsWith("image/")) {
|
||||
if (attachment.mimeType.startsWith("image/") || type === "image") {
|
||||
await previewImageAttachment(attachment);
|
||||
} else if (
|
||||
onPreviewDocument &&
|
||||
@@ -633,7 +633,7 @@ export function Editor(props: EditorProps) {
|
||||
await insertAttachments(mime, (attachments) => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
if (!editor) return;
|
||||
attachments.forEach((a) => editor?.attachFile(a));
|
||||
editor?.attachFiles(...attachments);
|
||||
});
|
||||
}}
|
||||
onGetAttachmentData={async (attachment) => {
|
||||
@@ -662,7 +662,7 @@ export function Editor(props: EditorProps) {
|
||||
onDone: (attachments) => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
if (!editor) return;
|
||||
attachments.forEach((a) => editor?.attachFile(a));
|
||||
editor?.attachFiles(...attachments);
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -825,7 +825,7 @@ function DropZone(props: DropZoneProps) {
|
||||
.getState()
|
||||
.getEditor(activeEditorId)?.editor;
|
||||
if (!editor) return;
|
||||
attachments.forEach((a) => editor?.attachFile(a));
|
||||
editor?.attachFiles(...attachments);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -27,10 +27,10 @@ import {
|
||||
hashStream,
|
||||
writeEncryptedFile
|
||||
} from "../../interfaces/fs";
|
||||
import { compressImage, FileWithURI } from "../../utils/image-compressor";
|
||||
import { checkFeature } from "../../common";
|
||||
import { AttachFilesDialog } from "../../dialogs/attach-files-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Queue from "p-queue";
|
||||
|
||||
export async function insertAttachments(
|
||||
type: string,
|
||||
@@ -81,7 +81,6 @@ export async function reuploadAttachment(
|
||||
}
|
||||
|
||||
type AttachFilesMessage =
|
||||
| { type: "compressing"; index: number }
|
||||
| { type: "encrypting"; index: number }
|
||||
| {
|
||||
type: "done";
|
||||
@@ -90,60 +89,39 @@ type AttachFilesMessage =
|
||||
}
|
||||
| { type: "error"; index: number; error: string };
|
||||
|
||||
export async function* attachFiles(
|
||||
export async function attachFiles(
|
||||
files: File[],
|
||||
shouldCompress: boolean[],
|
||||
report: (progress: AttachFilesMessage) => void,
|
||||
skipSpecialImageHandling = false
|
||||
): AsyncGenerator<AttachFilesMessage> {
|
||||
) {
|
||||
const queue = new Queue({ concurrency: 8 });
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
const shouldCompressFile = shouldCompress[i];
|
||||
queue.add(async () => {
|
||||
const file = files[i];
|
||||
|
||||
report({ type: "encrypting", index: i });
|
||||
|
||||
if (shouldCompressFile) {
|
||||
yield { type: "compressing", index: i };
|
||||
try {
|
||||
const compressed = await compressImage(file, {
|
||||
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
|
||||
width: (naturalWidth) => naturalWidth,
|
||||
height: (_, naturalHeight) => naturalHeight,
|
||||
resize: "contain",
|
||||
quality: 0.7
|
||||
});
|
||||
file = new FileWithURI([compressed], file.name, {
|
||||
lastModified: file.lastModified,
|
||||
type: file.type
|
||||
const allowed = await checkFeature("fileSize", {
|
||||
value: file.size,
|
||||
type: "toast"
|
||||
});
|
||||
if (!allowed) {
|
||||
throw new Error(strings.fileSizeLimitExceededPleaseUpgrade());
|
||||
}
|
||||
|
||||
const attachment =
|
||||
!skipSpecialImageHandling && file.type.startsWith("image/")
|
||||
? await pickImage(file)
|
||||
: await pickFile(file);
|
||||
|
||||
report({ type: "done", index: i, attachment: attachment || undefined });
|
||||
} catch (e) {
|
||||
yield {
|
||||
type: "error",
|
||||
index: i,
|
||||
error: (e as Error).message || strings.compressionFailed()
|
||||
};
|
||||
continue;
|
||||
report({ type: "error", index: i, error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
yield { type: "encrypting", index: i };
|
||||
|
||||
try {
|
||||
const allowed = await checkFeature("fileSize", {
|
||||
value: file.size,
|
||||
type: "toast"
|
||||
});
|
||||
if (!allowed) {
|
||||
throw new Error(strings.fileSizeLimitExceededPleaseUpgrade());
|
||||
}
|
||||
|
||||
const attachment =
|
||||
!skipSpecialImageHandling && file.type.startsWith("image/")
|
||||
? await pickImage(file)
|
||||
: await pickFile(file);
|
||||
|
||||
yield { type: "done", index: i, attachment: attachment || undefined };
|
||||
} catch (e) {
|
||||
yield { type: "error", index: i, error: (e as Error).message };
|
||||
}
|
||||
});
|
||||
}
|
||||
await queue.onIdle();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,6 +183,7 @@ export type AttachmentProgress = {
|
||||
type: "encrypt" | "download" | "upload";
|
||||
total: number;
|
||||
loaded: number;
|
||||
file?: File;
|
||||
};
|
||||
|
||||
type AddAttachmentOptions = {
|
||||
@@ -250,7 +229,7 @@ async function addAttachment(
|
||||
hash,
|
||||
hashType,
|
||||
filename: exists?.filename || file.name,
|
||||
mimeType: exists?.type || file.type,
|
||||
mimeType: exists?.mimeType || file.type,
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
@@ -684,6 +684,12 @@ function TiptapWrapper(
|
||||
}
|
||||
}, [props.spellcheck]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorContainerRef.current) {
|
||||
editorContainerRef.current.style.fontSize = `${editorConfig.fontSize}px`;
|
||||
}
|
||||
}, [editorConfig.fontSize]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={containerRef}
|
||||
@@ -786,10 +792,8 @@ function toIEditor(editor: Editor): IEditor {
|
||||
})
|
||||
.run();
|
||||
},
|
||||
attachFile: (file: Attachment) =>
|
||||
file.type === "image"
|
||||
? editor.commands.insertImage(file)
|
||||
: editor.commands.insertAttachment(file),
|
||||
attachFiles: async (...files: Attachment[]) =>
|
||||
editor.commands.insertAttachment(...files),
|
||||
sendAttachmentProgress: (hash, progress) =>
|
||||
editor.commands.updateAttachment(
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface IEditor {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
updateContent: (content: string) => void;
|
||||
attachFile: (file: Attachment) => void;
|
||||
attachFiles: (...files: Attachment[]) => void;
|
||||
sendAttachmentProgress: (hash: string, progress: number) => void;
|
||||
startSearch: () => void;
|
||||
getContent: () => string;
|
||||
|
||||
@@ -496,6 +496,7 @@ export const noteMenuItems: (
|
||||
"text/html": `<a href="${url}">${note.title}</a>`,
|
||||
"text/markdown": `[${note.title}](${url})`
|
||||
});
|
||||
showToast("success", strings.linkCopied());
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -594,6 +595,7 @@ export const noteMenuItems: (
|
||||
"text/html": `<a href="${link}">${note.title}</a>`,
|
||||
"text/markdown": `[${note.title}](${link})`
|
||||
});
|
||||
showToast("success", strings.linkCopied());
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -67,8 +67,7 @@ function Toggle(props: ToggleProps) {
|
||||
sx={{
|
||||
m: 0,
|
||||
bg: isOn ? "accent" : "icon-secondary",
|
||||
flexShrink: 0,
|
||||
scale: 0.75
|
||||
flexShrink: 0
|
||||
}}
|
||||
checked={isOn}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -82,6 +82,29 @@ function PublishView(props: PublishViewProps) {
|
||||
};
|
||||
}, [note.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const monographsUpdatedEvent = db.eventManager.subscribe(
|
||||
EVENTS.monographsUpdated,
|
||||
async (ids?: string[]) => {
|
||||
if (ids && ids.length > 0 && !ids.includes(note.id)) return;
|
||||
|
||||
const m = await resolveMonograph(note.id);
|
||||
setMonograph(m);
|
||||
|
||||
// if monograph has been unpublished, reset all the fields
|
||||
if (!m) {
|
||||
setSelfDestruct(false);
|
||||
if (titleInput.current) titleInput.current.value = note.title;
|
||||
if (passwordInput.current) passwordInput.current.value = "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
monographsUpdatedEvent.unsubscribe();
|
||||
};
|
||||
}, [note.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{monograph?.id ? (
|
||||
@@ -171,7 +194,7 @@ function PublishView(props: PublishViewProps) {
|
||||
</Text>
|
||||
</Flex>
|
||||
) : null}
|
||||
{monograph?.id ? (
|
||||
{monograph?.id && !selfDestruct ? (
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
@@ -242,8 +265,7 @@ function PublishView(props: PublishViewProps) {
|
||||
sx={{
|
||||
m: 0,
|
||||
bg: selfDestruct ? "accent" : "icon-secondary",
|
||||
flexShrink: 0,
|
||||
scale: 0.75
|
||||
flexShrink: 0
|
||||
}}
|
||||
checked={selfDestruct}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -115,7 +115,7 @@ export function UnlockView(props: UnlockViewProps) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isWrong && <ErrorText error="Wrong password" />}
|
||||
{isWrong && <ErrorText sx={{ mt: 1 }} error="Wrong password" />}
|
||||
<Button
|
||||
mt={3}
|
||||
variant="accent"
|
||||
|
||||
@@ -17,10 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Dialog from "../components/dialog";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { Box, Flex, Image, Switch, Text } from "@theme-ui/components";
|
||||
import { Flex, Image, Switch, Text } from "@theme-ui/components";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -32,10 +31,11 @@ import { ImageCompressionOptions } from "../stores/setting-store";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
import {
|
||||
CheckCircle,
|
||||
Loading,
|
||||
File as FileIcon,
|
||||
CloseCircle
|
||||
} from "../components/icons";
|
||||
import { compressImage } from "../utils/image-compressor";
|
||||
import Queue from "p-queue";
|
||||
|
||||
type FileStatus = "pending" | "compressing" | "encrypting" | "done" | "error";
|
||||
|
||||
@@ -45,6 +45,7 @@ type FileState = {
|
||||
progress: number;
|
||||
error?: string;
|
||||
compress: boolean;
|
||||
compressedFile?: File;
|
||||
};
|
||||
|
||||
type AttachFilesDialogProps = BaseDialogProps<false> & {
|
||||
@@ -66,40 +67,46 @@ export const AttachFilesDialog = DialogManager.register(
|
||||
ImageCompressionOptions.ASK_EVERY_TIME
|
||||
);
|
||||
const [fileStates, setFileStates] = useState<FileState[]>(() =>
|
||||
files.map((file) => ({
|
||||
file,
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
compress: file.type.startsWith("image/")
|
||||
? imageCompressionConfig !== ImageCompressionOptions.DISABLE
|
||||
: false
|
||||
}))
|
||||
files
|
||||
.sort((a, b) => {
|
||||
const aIsImage = a.type.startsWith("image/");
|
||||
const bIsImage = b.type.startsWith("image/");
|
||||
if (aIsImage && !bIsImage) return -1;
|
||||
if (!aIsImage && bIsImage) return 1;
|
||||
return a.type.localeCompare(b.type);
|
||||
})
|
||||
.map((file) => ({
|
||||
file,
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
compress: file.type.startsWith("image/")
|
||||
? imageCompressionConfig !== ImageCompressionOptions.DISABLE
|
||||
: false
|
||||
}))
|
||||
);
|
||||
const [showCompressionPrompt, setShowCompressionPrompt] = useState(
|
||||
const isCompressionOptional =
|
||||
hasImages &&
|
||||
imageCompressionConfig === ImageCompressionOptions.ASK_EVERY_TIME
|
||||
imageCompressionConfig === ImageCompressionOptions.ASK_EVERY_TIME;
|
||||
const isProcessing = fileStates.some(
|
||||
(f) => f.status === "compressing" || f.status === "encrypting"
|
||||
);
|
||||
const isDone = fileStates.every(
|
||||
(f) => f.status === "done" || f.status === "error"
|
||||
);
|
||||
const processingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ type, total, loaded }: AttachmentProgress) => {
|
||||
({ type, total, loaded, file }: AttachmentProgress) => {
|
||||
if (type !== "encrypt") return;
|
||||
|
||||
setFileStates((prev) =>
|
||||
prev.map((s) => {
|
||||
/**
|
||||
* only one file is encrypted at a time, so we can just update progress of the state with "encrypting" status
|
||||
*/
|
||||
if (s.status !== "encrypting") return s;
|
||||
|
||||
return {
|
||||
...s,
|
||||
progress: Math.round((loaded / total) * 100)
|
||||
};
|
||||
})
|
||||
);
|
||||
setFileStates((prev) => {
|
||||
const index = prev.findIndex((s) => s.file === file);
|
||||
if (index === -1) return prev;
|
||||
return updateFileStates(prev, index, {
|
||||
progress: Math.round((loaded / total) * 100)
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
@@ -107,133 +114,166 @@ export const AttachFilesDialog = DialogManager.register(
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCompressionPrompt || processingRef.current) return;
|
||||
const compressFiles = useCallback(async () => {
|
||||
setFileStates((prev) =>
|
||||
prev.map((s) => ({
|
||||
...s,
|
||||
status: s.compressedFile || !s.compress ? s.status : "compressing"
|
||||
}))
|
||||
);
|
||||
|
||||
processingRef.current = true;
|
||||
const queue = new Queue({ concurrency: 8 });
|
||||
|
||||
const shouldCompress: boolean[] = fileStates.map((s) => !!s.compress);
|
||||
for (const [index, state] of fileStates.entries()) {
|
||||
if (state.compressedFile || !state.compress) continue;
|
||||
|
||||
(async () => {
|
||||
const attachments: Attachment[] = [];
|
||||
let hasError = false;
|
||||
await queue.add(async () => {
|
||||
try {
|
||||
const compressed = await compressImage(state.file, {
|
||||
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
|
||||
width: (naturalWidth) => naturalWidth,
|
||||
height: (_, naturalHeight) => naturalHeight,
|
||||
resize: "contain",
|
||||
quality: 0.7
|
||||
});
|
||||
const compressedFile = new File([compressed], state.file.name, {
|
||||
lastModified: state.file.lastModified,
|
||||
type: state.file.type
|
||||
});
|
||||
setFileStates((prev) =>
|
||||
updateFileStates(prev, index, {
|
||||
status: "pending",
|
||||
compressedFile
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
const error = (e as Error).message || strings.compressionFailed();
|
||||
setFileStates((prev) =>
|
||||
updateFileStates(prev, index, { status: "error", error })
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for await (const message of attachFiles(
|
||||
files,
|
||||
shouldCompress,
|
||||
skipSpecialImageHandling
|
||||
)) {
|
||||
const { index } = message;
|
||||
await queue.onIdle();
|
||||
}, [fileStates]);
|
||||
|
||||
const processFiles = useCallback(async () => {
|
||||
const attachments: Attachment[] = [];
|
||||
const validIndices = fileStates
|
||||
.map((s, i) => (s.status === "error" ? -1 : i))
|
||||
.filter((i) => i !== -1);
|
||||
const filesToAttach = validIndices.map((i) => {
|
||||
const state = fileStates[i];
|
||||
return state.compress && state.compressedFile
|
||||
? state.compressedFile
|
||||
: state.file;
|
||||
});
|
||||
|
||||
await attachFiles(
|
||||
filesToAttach,
|
||||
(message) => {
|
||||
const index = validIndices[message.index];
|
||||
switch (message.type) {
|
||||
case "compressing":
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? { ...s, status: "compressing" as FileStatus }
|
||||
: s
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "encrypting":
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? { ...s, status: "encrypting" as FileStatus, progress: 0 }
|
||||
: s
|
||||
)
|
||||
updateFileStates(prev, index, {
|
||||
status: "encrypting",
|
||||
progress: 0
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "done":
|
||||
if (message.attachment) attachments.push(message.attachment);
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? {
|
||||
...s,
|
||||
status: "done" as FileStatus
|
||||
}
|
||||
: s
|
||||
)
|
||||
updateFileStates(prev, index, { status: "done" })
|
||||
);
|
||||
break;
|
||||
case "error":
|
||||
hasError = true;
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? {
|
||||
...s,
|
||||
status: "error" as FileStatus,
|
||||
error: message.error
|
||||
}
|
||||
: s
|
||||
)
|
||||
updateFileStates(prev, index, {
|
||||
status: "error",
|
||||
error: message.error
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
skipSpecialImageHandling
|
||||
);
|
||||
|
||||
onDone(attachments);
|
||||
if (files.length === 1 && !hasError) onClose(false);
|
||||
})();
|
||||
}, [showCompressionPrompt]);
|
||||
onDone(attachments);
|
||||
|
||||
const hasError = fileStates.some((s) => s.status === "error");
|
||||
if (fileStates.length === 1 && !hasError) onClose(false);
|
||||
}, [fileStates, onClose, onDone, skipSpecialImageHandling]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={
|
||||
showCompressionPrompt
|
||||
? strings.imageCompression()
|
||||
: strings.attachingFiles()
|
||||
}
|
||||
description={
|
||||
showCompressionPrompt ? strings.imageCompressionDesc() : ""
|
||||
}
|
||||
onClose={() => onClose(false)}
|
||||
title={strings.attachingFiles()}
|
||||
onOpen={async () => {
|
||||
await compressFiles();
|
||||
if (!isCompressionOptional) await processFiles();
|
||||
}}
|
||||
onClose={() => {
|
||||
if (isProcessing) return;
|
||||
onClose(false);
|
||||
}}
|
||||
width={500}
|
||||
positiveButton={
|
||||
showCompressionPrompt
|
||||
isCompressionOptional && !isDone
|
||||
? {
|
||||
text: strings.done(),
|
||||
onClick: () => setShowCompressionPrompt(false)
|
||||
text: strings.insert(),
|
||||
disabled: isProcessing,
|
||||
onClick: async () => {
|
||||
await processFiles();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
negativeButton={{
|
||||
text: strings.close(),
|
||||
disabled: isProcessing,
|
||||
onClick: () => onClose(false)
|
||||
}}
|
||||
>
|
||||
<ScrollContainer
|
||||
style={{
|
||||
maxHeight: 350,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative"
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
px: 1,
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
{fileStates.map((state, index) => (
|
||||
<FileRow
|
||||
key={`${state.file.name}-${index}`}
|
||||
state={state}
|
||||
showDivider={fileStates.length > 1}
|
||||
showCompressionToggle={showCompressionPrompt}
|
||||
onToggleCompress={async () => {
|
||||
if (
|
||||
!(await checkFeature("fullQualityImages", { type: "toast" }))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
<Text variant="body" sx={{ color: "paragraph-secondary", flex: 1 }}>
|
||||
{strings.name()}
|
||||
</Text>
|
||||
<Text variant="body" sx={{ color: "paragraph-secondary" }}>
|
||||
{isCompressionOptional && !isProcessing && !isDone
|
||||
? strings.compress()
|
||||
: strings.status()}
|
||||
</Text>
|
||||
</Flex>
|
||||
{fileStates.map((state, index) => (
|
||||
<FileRow
|
||||
key={`${state.file.name}-${index}`}
|
||||
state={state}
|
||||
showDivider={fileStates.length > 1}
|
||||
showCompressionToggle={isCompressionOptional}
|
||||
onToggleCompress={async () => {
|
||||
if (
|
||||
!(await checkFeature("fullQualityImages", { type: "toast" }))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, idx) =>
|
||||
idx === index ? { ...s, compress: !s.compress } : s
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
setFileStates((prev) =>
|
||||
updateFileStates(prev, index, { compress: !state.compress })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -250,11 +290,12 @@ function FileRow({
|
||||
showCompressionToggle?: boolean;
|
||||
onToggleCompress?: () => void;
|
||||
}) {
|
||||
const { file, status, progress, error, compress } = state;
|
||||
const { file, status, progress, error, compress, compressedFile } = state;
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const activeFile = compress && compressedFile ? compressedFile : file;
|
||||
const thumbnail = useMemo(
|
||||
() => (isImage ? URL.createObjectURL(file) : undefined),
|
||||
[file, isImage]
|
||||
() => (isImage ? URL.createObjectURL(activeFile) : undefined),
|
||||
[activeFile, isImage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -309,28 +350,33 @@ function FileRow({
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{file.name}
|
||||
{activeFile.name}
|
||||
</Text>
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
{formatBytes(file.size)}
|
||||
{status === "compressing"
|
||||
? ` — ${strings.compressing()}...`
|
||||
: status === "encrypting"
|
||||
? ` — ${strings.encrypting()} ${progress}%`
|
||||
: status === "error"
|
||||
? ` — ${error}`
|
||||
: ""}
|
||||
{formatBytes(activeFile.size)}
|
||||
{status === "error" ? ` — ${error}` : ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex sx={{ flexShrink: 0, alignItems: "center" }}>
|
||||
{showCompressionToggle && isImage ? (
|
||||
{status === "error" ? (
|
||||
<CloseCircle size={20} color="accent-error" />
|
||||
) : status === "done" ? (
|
||||
<CheckCircle size={20} color="accent" />
|
||||
) : status === "encrypting" || status === "compressing" ? (
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
{status === "compressing"
|
||||
? `${strings.compressing()}`
|
||||
: status === "encrypting"
|
||||
? `${strings.encrypting()} ${progress}%`
|
||||
: ""}
|
||||
</Text>
|
||||
) : showCompressionToggle && isImage ? (
|
||||
<Switch
|
||||
sx={{
|
||||
m: 0,
|
||||
bg: compress ? "accent" : "icon-secondary",
|
||||
flexShrink: 0,
|
||||
scale: 0.75
|
||||
flexShrink: 0
|
||||
}}
|
||||
checked={compress}
|
||||
onChange={onToggleCompress}
|
||||
@@ -339,35 +385,18 @@ function FileRow({
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
N/A
|
||||
</Text>
|
||||
) : status === "done" ? (
|
||||
<CheckCircle size={20} color="accent" />
|
||||
) : status === "error" ? (
|
||||
<CloseCircle size={20} color="accent-error" />
|
||||
) : status === "encrypting" || status === "compressing" ? (
|
||||
<Flex sx={{ alignItems: "center", gap: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 60,
|
||||
height: 4,
|
||||
bg: "border",
|
||||
borderRadius: "full",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: `${status === "compressing" ? 50 : progress}%`,
|
||||
height: "100%",
|
||||
bg: "accent",
|
||||
transition: "width 0.2s ease"
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
<Loading size={16} color="icon" />
|
||||
)}
|
||||
) : null}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function updateFileStates(
|
||||
prev: FileState[],
|
||||
index: number,
|
||||
update: Partial<FileState>
|
||||
) {
|
||||
const clone = prev.slice();
|
||||
clone[index] = { ...clone[index], ...update };
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Text, Flex } from "@theme-ui/components";
|
||||
import { Text, Flex, Link } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { getHomeRoute, hardNavigate } from "../navigation";
|
||||
import { appVersion } from "../utils/version";
|
||||
@@ -27,8 +27,9 @@ import {
|
||||
Checkmark,
|
||||
Icon,
|
||||
Warn,
|
||||
File,
|
||||
InternalLink
|
||||
Inbox,
|
||||
LinkedTo,
|
||||
Plus
|
||||
} from "../components/icons";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
@@ -31,6 +31,7 @@ import ScrollContainer, {
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { ConfirmDialog } from "./confirm";
|
||||
|
||||
type InboxHistoryDialogProps = BaseDialogProps<boolean>;
|
||||
|
||||
@@ -105,6 +106,7 @@ function DetailsCell({ value }: { value: string }) {
|
||||
<Box
|
||||
as="pre"
|
||||
sx={{
|
||||
color: "paragraph",
|
||||
m: 0,
|
||||
fontSize: "0.72em",
|
||||
whiteSpace: "pre-wrap",
|
||||
@@ -152,7 +154,7 @@ export const InboxHistoryDialog = DialogManager.register(
|
||||
const result = usePromise(() => db.inboxItemsHistory.failed.items());
|
||||
|
||||
async function deleteItem(id: string) {
|
||||
await db.inboxItemsHistory.delete(id);
|
||||
await db.inboxItemsHistory.delete([id]);
|
||||
showToast("success", strings.itemDeleted());
|
||||
if (result.status !== "pending") {
|
||||
result.refresh();
|
||||
@@ -160,6 +162,14 @@ export const InboxHistoryDialog = DialogManager.register(
|
||||
}
|
||||
|
||||
async function deleteAll() {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: strings.deleteAll(),
|
||||
subtitle: strings.deleteAllFailedItemsDesc(),
|
||||
positiveButtonText: strings.yes(),
|
||||
negativeButtonText: strings.no()
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
await db.inboxItemsHistory.deleteFailed();
|
||||
showToast("success", strings.allItemsDeleted());
|
||||
if (result.status !== "pending") {
|
||||
@@ -265,7 +275,17 @@ export const InboxHistoryDialog = DialogManager.register(
|
||||
<Button
|
||||
variant="icon"
|
||||
title={strings.delete()}
|
||||
onClick={() => deleteItem(item.id)}
|
||||
onClick={async () => {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: strings.deleteItem(),
|
||||
subtitle: strings.areYouSure(),
|
||||
positiveButtonText: strings.yes(),
|
||||
negativeButtonText: strings.no()
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
sx={{ color: "accent-error", p: "2px" }}
|
||||
>
|
||||
<Trash size={16} />
|
||||
|
||||
@@ -59,6 +59,9 @@ export const AuthenticationSettings: SettingsGroup[] = [
|
||||
}
|
||||
},
|
||||
validate: async ({ oldPassword, newPassword }) => {
|
||||
const verify = await db.user.verifyPassword(oldPassword);
|
||||
if (!verify) return false;
|
||||
|
||||
try {
|
||||
if (!(await createBackup({ noVerify: true }))) return false;
|
||||
return (
|
||||
|
||||
@@ -401,10 +401,14 @@ const AddApiKeyDialog = DialogManager.register(function AddApiKeyDialog(
|
||||
setSelectedExpiry(Number(value));
|
||||
}}
|
||||
sx={{
|
||||
color: "var(--paragraph)",
|
||||
bg: "background-secondary",
|
||||
border: "1px solid",
|
||||
borderColor: "border",
|
||||
padding: "5px"
|
||||
padding: "5px",
|
||||
"& + svg": {
|
||||
fill: "var(--paragraph)"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{EXPIRY_OPTIONS.map((option) => (
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function writeEncryptedFile(
|
||||
total: file.size,
|
||||
loaded: done ? file.size : totalRead
|
||||
},
|
||||
{ type: "encrypt", hash }
|
||||
{ type: "encrypt", hash, file }
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -470,11 +470,12 @@ export async function checkUpload(
|
||||
|
||||
function reportProgress(
|
||||
ev: { total?: number; loaded?: number } | undefined,
|
||||
{ type, hash }: { type: string; hash: string }
|
||||
{ type, hash, file }: { type: string; hash: string; file?: File }
|
||||
) {
|
||||
AppEventManager.publish(AppEvents.UPDATE_ATTACHMENT_PROGRESS, {
|
||||
type,
|
||||
hash,
|
||||
file,
|
||||
total: ev?.total || 1,
|
||||
loaded: ev?.loaded || 1
|
||||
});
|
||||
|
||||
@@ -17,18 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export class FileWithURI extends File {
|
||||
uri: string;
|
||||
constructor(
|
||||
fileBits: BlobPart[],
|
||||
fileName: string,
|
||||
options?: FilePropertyBag
|
||||
) {
|
||||
super(fileBits, fileName, options);
|
||||
this.uri = URL.createObjectURL(this);
|
||||
}
|
||||
}
|
||||
|
||||
type DeriveDimension = (naturalWidth: number, naturalHeight: number) => number;
|
||||
|
||||
interface CompressorOptions {
|
||||
@@ -84,7 +72,7 @@ export function compressImage(file: File, options: Partial<CompressorOptions>) {
|
||||
naturalHeight: image.naturalHeight,
|
||||
naturalWidth: image.naturalWidth
|
||||
});
|
||||
resolve(compressed);
|
||||
resolve(compressed.size > file.size ? file : compressed);
|
||||
URL.revokeObjectURL(image.src);
|
||||
};
|
||||
image.onabort = () => {
|
||||
|
||||
@@ -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], [false])) {
|
||||
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,
|
||||
|
||||
@@ -21,7 +21,7 @@ On all three platforms we use the same exact library for all cryptographic funct
|
||||
|
||||
> info Fun story
|
||||
>
|
||||
> When we first added encryption, we used AES-GCM-256 across platforms but the cross-platform compatbility was abyssmal. That is when I found out about the great libsodium. Written in C, wrappers available for all platforms...what more could I want?
|
||||
> When we first added encryption, we used AES-GCM-256 across platforms but the cross-platform compatibility was abyssmal. That is when I found out about the great libsodium. Written in C, wrappers available for all platforms...what more could I want?
|
||||
|
||||
## Process
|
||||
|
||||
@@ -41,13 +41,7 @@ This process is repeated every time you sign in.
|
||||
|
||||
### 2. Key generation
|
||||
|
||||
After you are signed in, the app requests your user data which includes, among other things, your salt.
|
||||
|
||||
> info Salt generation
|
||||
>
|
||||
> When you create an account, the server generates a cryptographically secure random salt for you. This salt is used for key generation.
|
||||
|
||||
You password & salt is then used to derive a strong irreversible key using Argon2 as the password key derivation function (PKDF).
|
||||
When you first sign up for an account, your client generates two encryption keys. One is a unique data encryption key that encrypts all your notes and other data. The second is your master encryption key, this is derived by your password and predictable salt. This key protects all your encryption keys, like the aforementioned data encryption key. If you change your password, your client will re-encrypt your existing data encryption key with your new master key.
|
||||
|
||||
### 3. Encryption key storage
|
||||
|
||||
@@ -55,7 +49,7 @@ You password & salt is then used to derive a strong irreversible key using Argon
|
||||
|
||||
Instead of storing the key as plain text (and allowing anyone to copy/move it), we use browser's `IndexedDB` to store the key as a `CryptoKey`.
|
||||
|
||||
`CryptoKey` is stored securely by the browser and cannot be exported, viewed, copied except by the app & browser.
|
||||
`CryptoKey` is stored securely by the browser and cannot be exported, viewed, or copied except by the app & browser.
|
||||
|
||||
# [Mobile](#/tab/mobile)
|
||||
|
||||
@@ -65,12 +59,12 @@ On iOS and Android, the encryption key is stored in the phone's keychain.
|
||||
|
||||
### 4. Data encryption
|
||||
|
||||
Encryption only takes place when you sync. Each item in the database is encrypted seperately using XChaCha-Poly1305-IETF.
|
||||
Encryption only takes place when you sync. Each item in the database is encrypted separately using XChaCha-Poly1305-IETF.
|
||||
|
||||
#### How it works
|
||||
|
||||
1. The item is read from the database as JSON object and stringified (i.e. converted to a string).
|
||||
2. The string is encrypted using the encryption key generated earlier.
|
||||
2. The string is encrypted using the data encryption key generated earlier.
|
||||
3. The result is a JSON object which contains:
|
||||
1. A base64 encoded `cipher`
|
||||
2. A 192-bit nonce (`iv`)
|
||||
@@ -83,3 +77,9 @@ Encryption only takes place when you sync. Each item in the database is encrypte
|
||||
> See the whole process in action [here.](https://vericrypt.notesnook.com/)
|
||||
|
||||
This object is then sent to the server for storage. The server performs no further operation on this data (because it can't).
|
||||
|
||||
## Faqs
|
||||
|
||||
### I am an old user of Notesnook, I don't have a data encryption key.
|
||||
|
||||
Your data encryption key will be created when you change your password.
|
||||
|
||||
@@ -8,16 +8,19 @@ The following steps will help you import your notes from Standard notes easily.
|
||||
|
||||
1. Open Standard Notes app on Desktop or visit [https://app.standardnotes.org](https://app.standardnotes.org) and login to your account.
|
||||
2. Select all your notes, right click, and select `Export`.
|
||||
3. Open the Notesnook (app or desktop).
|
||||
4. Go to `Settings > Notesnook Importer` and select `Standard Notes` from list of apps.
|
||||
3. In the Export dialog, set export format to "HTML" and embedded files to "Export separately".
|
||||
4. Open the Notesnook (app or desktop).
|
||||
5. Go to `Settings > Notesnook Importer` and select `Standard Notes` from list of apps.
|
||||

|
||||
5. Drop the .zip file you exported earlier from Standard Notes in the box or click anywhere to open system file picker to select the backup.
|
||||
6. Drop the .zip file you exported earlier from Standard Notes in the box or click anywhere to open system file picker to select the backup.
|
||||

|
||||
6. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, [report it on github](https://github.com/streetwriters/notesnook).
|
||||
7. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, [report it on github](https://github.com/streetwriters/notesnook).
|
||||
|
||||
## Supported formats
|
||||
|
||||
- [x] Text files
|
||||
- [x] Authentication notes
|
||||
- [x] Super notes (all formatting supported)
|
||||
- [x] Authentication notes (as tables)
|
||||
- [x] Spreadsheets
|
||||
- [ ] Tags
|
||||
- [x] Code & markdown files
|
||||
- [ ] Tags & organization
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user