Compare commits

...

95 Commits

Author SHA1 Message Date
Abdullah Atta
4d65be7902 web: add support for standard notes importer 2026-07-22 13:12:41 +05:00
Ammar Ahmed
d63ce5a213 Merge pull request #10126 from streetwriters/mobile-release/3.4.7
mobile: release v3.4.7
2026-07-22 11:12:45 +05:00
Chloe Oletto
987671e59c web: fix web-clipper crash on clipped file. (#10127)
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-07-21 12:45:13 +05:00
Ammar Ahmed
0fc705ce1e Merge pull request #10130 from streetwriters/chore/update-workflows
ci: update mobile workflows
2026-07-21 12:41:02 +05:00
Ammar Ahmed
b3805534d6 ci: remove authorize step in android/ios preview builds 2026-07-21 12:35:08 +05:00
Ammar Ahmed
77c009e83d mobile: fix toast message shows under the sheet in global context 2026-07-21 12:14:08 +05:00
Ammar Ahmed
c9f1cba702 ci: update mobile workflows 2026-07-21 11:36:23 +05:00
Ammar Ahmed
637cf914ee mobile: fix app gets stuck after deleting history item on iOS 2026-07-21 10:25:12 +05:00
Ammar Ahmed
64877c519b Merge pull request #10128 from streetwriters/chore/update-workflows
ci: update workflows to use actions/checkout@v5
2026-07-21 10:04:20 +05:00
Ammar Ahmed
229b620473 ci: update workflows to use actions/checkout@v5 2026-07-21 09:27:49 +05:00
Ammar Ahmed
658e915589 mobile: release v3.4.7 2026-07-20 13:12:20 +05:00
Ammar Ahmed
d4658aa032 Merge pull request #10095 from kashaf-ansari-dev/fix-reminder-notification-permission
Improve the Android reminder notification permission flow by properly handling notification and exact alarm permissions.
2026-07-17 09:10:57 +05:00
Ammar Ahmed
489c793806 Merge pull request #10041 from kashaf-ansari-dev/feat/9723-clear-note-history
mobile: add option to clear note version history
2026-07-15 09:52:04 +05:00
kashaf-ansari-dev
eeae993736 mobile: add version history deletion strings
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
c8cf4ce52a mobile: fix individual note history version deletion
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
f6c31d19d8 mobile: add missing context prop in dialog
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
a94e8d6852 mobile: render clear-history dialog with local context
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
2edbc3bf2e mobile: remove clear version history action for multiple selected notes
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
e4a7d63e07 mobile: add option to clear note version history
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:30 +05:00
Ammar Ahmed
76fd61af4f Merge pull request #10085 from streetwriters/fix/651
mobile: fix unable to change plan on buy-plan screen
2026-07-14 08:43:53 +05:00
Ammar Ahmed
98386a6d71 Merge pull request #10094 from streetwriters/mobile-release/3.4.6
mobile: release v3.4.6
2026-07-13 22:44:13 +05:00
kashaf-ansari-dev
539a630f77 mobile: improve reminder permission settings flow on Android
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 18:07:40 +05:00
kashaf-ansari-dev
6b65f8031d mobile: fix reminder creation after granting notification permission
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 14:58:31 +05:00
01zulfi
3c85cddfe7 Merge pull request #10093 from streetwriters/release/3.4.4
web: bump version to 3.4.4
2026-07-13 14:14:25 +05:00
Ammar Ahmed
e426426468 mobile: release v3.4.6 2026-07-13 12:56:49 +05:00
01zulfi
109e46bd06 web: bump version to 3.4.4
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-13 12:28:07 +05:00
Ammar Ahmed
281a973c37 mobile: fix unable to change plan on buy-plan screen 2026-07-11 12:00:15 +05:00
Ammar Ahmed
2738207c20 Merge pull request #9998 from kashaf-ansari-dev/fix-9967-linking-note-blocks-loading-state
mobile: prevent upgrade prompt flash in note block linking list
2026-07-10 18:41:04 +05:00
01zulfi
1aa73c1f23 web: update editor's font size dynamically when default font size setting changes (#10047)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-10 09:39:21 +05:00
Masatoshi Ogiwara
9ec22724e9 fix: typo in some documents (#10071) 2026-07-09 10:34:09 +05:00
Kashaf Ansari
4a7aa27bd9 mobile: add keep-screen-on preference in settings-behaviour (#10043)
* mobile: add keep-screen-on preference in settings-behaviour

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

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

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

---------

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

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

* vericrypt: Clear account data on restart.

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

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

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

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

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

* docs: Explain what happens better.

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

* Fix typo in encryption key generation section

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

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

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

* docs: update wording

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

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-07-08 10:24:17 +05:00
Masatoshi Ogiwara
afd63db043 docs(help): typo in documents (#10064) 2026-07-08 08:41:17 +05:00
Ammar Ahmed
aadb6e3a01 mobile: prepare v3.4.5 for release (#10060) 2026-07-07 09:30:31 +05:00
Ammar Ahmed
7c3fdab6ee Merge pull request #10053 from kashaf-ansari-dev/fix-7348-unlink-notebook-on-relink
mobile: unlink note from previous notebook in single-select mode
2026-07-07 09:25:56 +05:00
Ammar Ahmed
14f727d6e6 Merge pull request #10016 from kashaf-ansari-dev/fix-9742-duplicate-pinned-note
core: do not preserve pin state when duplicating notes
2026-07-07 09:25:23 +05:00
01zulfi
688a646181 Merge pull request #10058 from streetwriters/release/3.4.3
web: bump version to 3.4.3
2026-07-06 15:10:47 +05:00
Kashaf Ansari
7a7f0db941 mobile: use FileViewer for image sharing on Android (#10018)
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-06 13:37:29 +05:00
Kashaf Ansari
96be35c105 mobile: fix long tag names overflow and display in tag items (#9950)
* mobile: fix long tag names overflow and display in tag items

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

* mobile: fix long title in side menu

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

* mobile: fix long notebook title display

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

* mobile: prevent tag list overlap on first render

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

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-06 13:36:31 +05:00
01zulfi
36bacce3ac web: bump version to 3.4.3
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-06 10:47:13 +05:00
kashaf-ansari-dev
c89e333c11 mobile: unlink note from previous notebook in single-select mode
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-03 13:12:10 +05:00
Abdullah Atta
d521eb3bd8 desktop: regenerate package-lock (#10051) 2026-07-03 05:45:48 +05:00
Abdullah Atta
20bf090c7e desktop: fix source generation for electron (#10046)
* desktop: fix source generation for electron

* ci: upload generated-sources as build artifact
2026-07-03 05:14:11 +05:00
01zulfi
6cc9b8d1e1 web: add top margin to unlock view error text (#10030)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-02 08:45:49 +05:00
01zulfi
e931423eba web: show success toast when copying note link in context menu (#10027)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-02 08:45:41 +05:00
01zulfi
743d47e9e9 web: verify old password before creating backup during password update (#10031)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-02 08:45:26 +05:00
Ammar Ahmed
37c96177a7 Mobile release/3.4.4 (#10040)
* mobile: add bump-version script

* mobile: bump version to v3.4.4
2026-07-01 15:54:10 +05:00
Abdullah Atta
834479d095 Release v3.4.2 (#10039)
* web: bump version to 3.4.2

* desktop: fix `Invalid filename` error on backup creation
2026-07-01 12:51:34 +05:00
Abdullah Atta
643515c6c4 desktop: fix Invalid filename error on backup creation 2026-07-01 12:28:45 +05:00
Abdullah Atta
0b0a4f1b6f web: bump version to 3.4.2 2026-07-01 11:52:50 +05:00
Ammar Ahmed
48d88069c0 core: make db migration more resilient and recoverable (#10037)
* core: do not throw error if inboxitemshistory table already exists during migration

* core: add ensure column check to avoid `column already exists` errors

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-07-01 11:51:24 +05:00
Abdullah Atta
9d7754f1e1 ci: fix macos x64 build (#10038) 2026-07-01 08:54:05 +05:00
Abdullah Atta
b2ed115011 Merge pull request #10034 from streetwriters/release/3.4.1
Release v3.4.1
2026-06-30 20:32:34 +05:00
Abdullah Atta
3447e8133c web: bump version to 3.4.1 2026-06-30 20:15:54 +05:00
Abdullah Atta
4473d8ec67 ci: fix linux builds 2026-06-30 20:14:53 +05:00
01zulfi
08cf21b05a web: compress image in realtime for better UX in attach files dialog (#9899)
* web: compress image in realtime for better UX in attach files dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: refactor

* web: fix "done" button is disabled

* theme: improve switch styles

* web: improve bulk attachment processing & ux

* web: fix `attachFile` is not a function

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-06-30 19:31:42 +05:00
01zulfi
10dccba5b6 docs: include ColorNote & UpNote pages in importer section (#10029)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 15:22:00 +05:00
01zulfi
23c9a98013 docs: remove inbox api beta warning (#10028)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 13:43:04 +05:00
01zulfi
473fae4a30 Merge pull request #9995 from streetwriters/release/3.4.0
web: bump version to 3.4.0
2026-06-30 13:16:48 +05:00
Ammar Ahmed
e9808c08f0 mobile: bump version 2026-06-30 11:13:02 +05:00
Abdullah Atta
4bb46ea149 ci: fix build for windows 2026-06-30 10:02:44 +05:00
Ammar Ahmed
01811372fc mobile: prepare for release 2026-06-30 09:43:29 +05:00
Abdullah Atta
6d2097259b editor: remove .replaceAll usage to support older Android 9 devices 2026-06-30 09:43:29 +05:00
Abdullah Atta
4c3e065928 core: add comment about why we aren't lowercasing email for old password generation 2026-06-30 09:43:29 +05:00
Abdullah Atta
e45b72a8c4 ci: install sqlite modules in apps/desktop dir 2026-06-30 09:43:29 +05:00
Abdullah Atta
aceb6db8b8 core: always lowercase email address when resetting password 2026-06-30 09:43:29 +05:00
Abdullah Atta
5a0ca0bed9 ci: fix sqlite modules not being found in desktop builds 2026-06-30 09:43:29 +05:00
Abdullah Atta
8dd5779308 ci: fix desktop tests 2026-06-30 09:43:29 +05:00
Abdullah Atta
dc958098ea desktop: fix tests 2026-06-30 09:43:29 +05:00
Ammar Ahmed
bf536eb183 mobile: fix event subscription 2026-06-30 09:43:29 +05:00
Ammar Ahmed
eb3eea031e mobile: add support for copying links as html links with proper title. 2026-06-30 09:43:29 +05:00
Ammar Ahmed
d376e00a1e editor: cleanup nn:// links when pasted
Ensure that we remove any styles added to deep links. This is done especially for iOS where pasted link html get's wrapped into custom div with some hard coded styles making the link not match style with the rest of content.
2026-06-30 09:43:29 +05:00
Ammar Ahmed
aca36b60b7 editor: fix note links not detected on paste/edit 2026-06-30 09:43:29 +05:00
Ammar Ahmed
033666795c mobile: fix screenguard module error 2026-06-30 09:43:29 +05:00
Abdullah Atta
c0872c98a8 desktop: fix deep linking not working 2026-06-30 09:43:29 +05:00
01zulfi
f2173a42d9 web: add confirmation before deleting failed inbox items
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:29 +05:00
Ammar Ahmed
cbddea4518 mobile: show toast message on vault locked/unlocked events 2026-06-30 09:43:03 +05:00
Ammar Ahmed
c69304781a mobile: fix password input is not shown in vault dialog 2026-06-30 09:43:03 +05:00
Ammar Ahmed
412631bf67 core: dispatch vaultUnlocked event when password is updated. 2026-06-30 09:43:03 +05:00
Abdullah Atta
8c2be00920 core: do not send PushCompletedV2 if no items sent 2026-06-30 09:43:03 +05:00
01zulfi
553e2060be editor: copy html for internal links in copy link tool
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
Abdullah Atta
7603de25c6 ci: fix Could not find any Visual Studio installation to use 2026-06-30 09:43:03 +05:00
01zulfi
146216949d web: update publish view popup when monographs sync (#10007)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
Abdullah Atta
c4d8abef16 setup: add node-gyp-build to postinstall whitelist 2026-06-30 09:43:03 +05:00
Abdullah Atta
baf823264b core: remove debug tests 2026-06-30 09:43:03 +05:00
Abdullah Atta
7f0a517ee1 config: add better-sqlite3 to postinstall whitelist 2026-06-30 09:43:03 +05:00
01zulfi
da48cf0bd1 web: don't show monograph view count when self destruct enabled
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
Ammar Ahmed
4c41e13c5d mobile: fix ts errors 2026-06-30 09:43:03 +05:00
01zulfi
82e2953bc8 web: fix paragraph color in inbox settings for dark mode
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
01zulfi
fc244b1196 core: delete linked inboxHistoryItem's on permanent note deletion
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
01zulfi
98ecad3450 web: bump version to 3.4.0
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
kashaf-ansari-dev
3aa23d3215 core: do not preserve pin state when duplicating notes
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-25 17:33:36 +05:00
kashaf-ansari-dev
e0b9932aba mobile: prevent upgrade prompt flash in note block linking list
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-19 22:31:56 +05:00
137 changed files with 5458 additions and 9360 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -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

View File

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

View File

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

View File

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

127
.github/workflows/ios.preview.build.yml vendored Normal file
View 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

View File

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

View File

@@ -0,0 +1,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,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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";

View File

@@ -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";

View File

@@ -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({

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.4.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",

View File

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

View File

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

View File

@@ -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",

View File

@@ -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);

View File

@@ -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 }) => {

View File

@@ -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, {

View File

@@ -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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ? (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ? (

View File

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

View File

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

View File

@@ -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]);

View File

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

View File

@@ -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
}
/>

View File

@@ -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={{

View File

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.4.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"

View File

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

View 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;
});

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
});

View File

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

View File

@@ -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) {

View File

@@ -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
});
}

View File

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

View File

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

View File

@@ -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());
}
},
{

View File

@@ -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()}

View File

@@ -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()}

View File

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

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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} />

View File

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

View File

@@ -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) => (

View File

@@ -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
});

View File

@@ -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 = () => {

View File

@@ -31,6 +31,7 @@ import { getFormattedDate } from "@notesnook/common";
import { useStore as useThemeStore } from "../stores/theme-store";
import { isCipher } from "@notesnook/core";
import { attachFiles } from "../components/editor/picker";
import { BaseAttachment } from "@notesnook/editor";
export class WebExtensionServer implements Server {
async login() {
@@ -88,11 +89,16 @@ export class WebExtensionServer implements Server {
}
);
let attachment;
for await (const message of attachFiles([clippedFile], [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,

View File

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

View File

@@ -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.
![](/static/standard-notes-importer/3.png)
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.
![](/static/standard-notes-importer/4.png)
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