Compare commits

..

89 Commits

Author SHA1 Message Date
Abdullah Atta
4906c4f293 core: update @streetwriters/showdown 2023-12-21 11:15:57 +05:00
Abdullah Atta
02e44d9502 editor: add support for simple checklists 2023-12-21 11:08:54 +05:00
Ammar Ahmed
d9d82232c0 editor: implement simple checklists 2023-12-08 12:21:10 +05:00
Ammar Ahmed
ed81319d2c mobile: use index based grouping 2023-12-08 10:13:43 +05:00
Abdullah Atta
d65917c85b web: changes to use new grouping api 2023-12-05 15:34:55 +05:00
Abdullah Atta
528aefca46 core: implement new high perf indexed based grouping 2023-12-05 15:34:55 +05:00
Ammar Ahmed
356142bb22 mobile: fix share extension in release 2023-12-04 12:32:40 +05:00
Abdullah Atta
dad1742e80 web: fix router types 2023-12-04 11:35:42 +05:00
Abdullah Atta
c68ed0dc52 web: enable vite hot reloading 2023-12-04 11:35:29 +05:00
Abdullah Atta
978afa0899 web: fix minor build issues 2023-11-30 15:22:02 +05:00
Abdullah Atta
14d57f200b web: migrate everything to react@18 2023-11-30 15:02:37 +05:00
Abdullah Atta
2f4f351fb6 web: fix notebook tests 2023-11-30 13:04:37 +05:00
Ammar Ahmed
1344ba3821 mobile: share extension cleanup 2023-11-30 12:20:22 +05:00
Ammar Ahmed
21f9930cd0 mobile: cleanup 2023-11-30 11:21:57 +05:00
Ammar Ahmed
0789f0c075 mobile: migrate share extension 2023-11-30 11:21:57 +05:00
Ammar Ahmed
8e1d7e097c mobile: fix navigation 2023-11-30 11:21:57 +05:00
Abdullah Atta
f3a977c070 web: send noteId when decrypting real-time locked content 2023-11-28 13:56:08 +05:00
Abdullah Atta
c6fad05c56 web: fix tag tests 2023-11-28 13:56:08 +05:00
Ammar Ahmed
72a99ff751 mobile: pass tests 2023-11-27 16:33:09 +05:00
Abdullah Atta
1fcc4aa0b4 web: fix html export snapshot 2023-11-27 10:42:30 +05:00
Abdullah Atta
3a97cdeb3d core: fix md with frontmatter export 2023-11-27 10:36:16 +05:00
Ammar Ahmed
7cf6441b9c core: fix ts error 2023-11-27 10:03:24 +05:00
Ammar Ahmed
cdd3075e4f mobile: fix webpack build 2023-11-27 10:03:24 +05:00
Ammar Ahmed
162d16b2c7 mobile: push changes 2023-11-27 10:03:24 +05:00
Abdullah Atta
8e0559ca85 web: fix notes tests 2023-11-27 10:03:23 +05:00
Abdullah Atta
e92e54684e core: prevent duplicate colors based on color code 2023-11-27 10:03:23 +05:00
Abdullah Atta
9321aa1732 core: fix error on locking notes 2023-11-27 10:03:23 +05:00
Ammar Ahmed
5daeda890c mobile: push changes 2023-11-27 10:03:23 +05:00
Abdullah Atta
f6dbf253fa web: reset search results on navigation 2023-11-27 10:03:23 +05:00
Abdullah Atta
acf993f06e web: reset prepared sql stmt on error 2023-11-27 10:03:23 +05:00
Abdullah Atta
4b4a388f5f web: reuse search query 2023-11-27 10:03:23 +05:00
Abdullah Atta
3fd216bfd4 core: use porter trigram tokenizer for fts 2023-11-27 10:03:23 +05:00
Abdullah Atta
a2bbde30e6 core: properly escape fts query 2023-11-27 10:03:23 +05:00
Abdullah Atta
2e4035cd7b web: add support for assigning tags to notes 2023-11-27 10:03:23 +05:00
Abdullah Atta
e705793e0a core: fix "type" does not exist 2023-11-27 10:03:23 +05:00
Abdullah Atta
37c0107660 core: fix build error 2023-11-27 10:03:22 +05:00
Abdullah Atta
5bfcf93713 core: skip attachment if it already exists on backup restore 2023-11-27 10:03:22 +05:00
Ammar Ahmed
920ff08f16 mobile: migrate to sqlite 2023-11-27 10:03:22 +05:00
Abdullah Atta
6b85150acb web: fix note editor properties 2023-11-27 10:01:20 +05:00
Abdullah Atta
0e243fbeb4 common: type is now optional in getFormattedDate 2023-11-27 10:01:20 +05:00
Abdullah Atta
24a5953e0f core: return FilteredSelector from attachments.ofNote 2023-11-27 10:01:20 +05:00
Abdullah Atta
ec1e868510 web: show notebook & tag shortcuts when assigning notes 2023-11-27 10:01:20 +05:00
Abdullah Atta
7450585737 web: implement virtualized search for all items 2023-11-27 10:01:20 +05:00
Abdullah Atta
9a09332182 web: use lazy loading to load attachments 2023-11-27 10:01:12 +05:00
Abdullah Atta
3992aa5dc0 web: load tag & color title lazily 2023-11-27 10:01:12 +05:00
Abdullah Atta
14f9d0ac7f web: show all nested notebooks as tree when moving notes 2023-11-27 10:01:12 +05:00
Abdullah Atta
f1a615515b web: add support for nested notebooks 2023-11-27 10:01:11 +05:00
Abdullah Atta
d4ff1dd46c web: use new virtualized grouping for reminders 2023-11-27 10:01:11 +05:00
Abdullah Atta
6276bd5db3 web: use new virtualized grouping for trash 2023-11-27 10:01:11 +05:00
Abdullah Atta
34736307c3 web: use new virtualized grouping for monographs 2023-11-27 10:01:11 +05:00
Abdullah Atta
60056f3fab core: add support for fetching trash as grouped 2023-11-27 10:01:11 +05:00
Abdullah Atta
f99a00789b web: install missing @hazae41/foras 2023-11-27 10:01:11 +05:00
Abdullah Atta
fcfda03fb2 core: fix attachment extraction for locked notes 2023-11-27 10:01:11 +05:00
Abdullah Atta
cd4c2934e0 global: update lockfiles 2023-11-27 10:01:10 +05:00
Abdullah Atta
396508d4b9 web: start initial migration to sqlite 2023-11-27 10:01:10 +05:00
Abdullah Atta
ffc03460b9 ui: add support for lazily loading menu items 2023-11-27 10:01:10 +05:00
Abdullah Atta
4315503893 common: remove total-notes utility 2023-11-27 10:01:10 +05:00
Abdullah Atta
1c9e42fbfc core: changes according to client needs 2023-11-27 10:01:10 +05:00
Abdullah Atta
f8f992d11e core: fix database migration 2023-11-27 10:01:10 +05:00
Abdullah Atta
8aa36f632a core: add support for incremental grouping 2023-11-27 10:01:10 +05:00
Abdullah Atta
ac89e3851a core: fix size of nanoid in benchmarks 2023-11-27 10:01:10 +05:00
Abdullah Atta
430ad22263 core: optimize bson objectid generation & add benchmarks 2023-11-27 10:01:09 +05:00
Abdullah Atta
c46ba25408 core: fix monographs 2023-11-27 10:01:09 +05:00
Abdullah Atta
ddcddbc8a8 core: fix sync tests 2023-11-27 10:01:09 +05:00
Abdullah Atta
24b1ada636 core: remove temporary sqlite db 2023-11-27 10:01:09 +05:00
Abdullah Atta
98a3b12bb8 core: migrate everything to sqlite 2023-11-27 10:01:09 +05:00
Abdullah Atta
26dca8b1d8 core: migrate all database writes to sqlite 2023-11-27 10:00:24 +05:00
Abdullah Atta
7dc9529981 global: update lockfiles 2023-11-27 10:00:05 +05:00
Abdullah Atta
32e36325b3 core: remove outbox 2023-11-27 10:00:05 +05:00
Abdullah Atta
7b554bf781 core: convert everything else to typescript 2023-11-27 10:00:05 +05:00
Abdullah Atta
4bcc08d046 core: remove unnecessary sync tests 2023-11-27 09:57:25 +05:00
Abdullah Atta
77bb9726ab core: migrate monographs to typescript 2023-11-27 09:57:25 +05:00
Abdullah Atta
94f3dc0c52 core: make migration tests less flaky 2023-11-27 09:57:25 +05:00
Abdullah Atta
be19ff1171 core: add tests for migrations 2023-11-27 09:57:25 +05:00
Abdullah Atta
6d52f9a6a3 core: migrate settings to its own collection 2023-11-27 09:57:25 +05:00
Abdullah Atta
b3c6bedc13 core: get rid of topics; move to sub notebooks 2023-11-27 09:57:25 +05:00
Abdullah Atta
94bc61f636 core: migrate attachments to use relations for linking to notes 2023-11-27 09:57:25 +05:00
Abdullah Atta
68406a2a3a core: minor updates and fixes 2023-11-27 09:57:25 +05:00
Abdullah Atta
8a0bf20e11 core: fix types & syncing issues 2023-11-27 09:57:25 +05:00
ammarahm-ed
1055b6f13a mobile: pass all tests 2023-11-27 09:55:56 +05:00
ammarahm-ed
4994ad77b4 mobile: fix filesystem not loaded 2023-11-27 09:55:56 +05:00
ammarahm-ed
e0712ce2f3 mobile: fix navigating to tags 2023-11-27 09:55:56 +05:00
ammarahm-ed
559801ba80 mobile: migrate to typescript 2023-11-27 09:55:56 +05:00
ammarahm-ed
b69dd3b217 common: fix ts errors 2023-11-27 09:50:01 +05:00
Abdullah Atta
8328575806 mobile: use new types from core 2023-11-27 09:50:01 +05:00
Abdullah Atta
30de9cabe0 web: use new types from core 2023-11-27 09:50:00 +05:00
Abdullah Atta
506cb2dd47 core: fix more typings 2023-11-27 09:49:31 +05:00
Abdullah Atta
2f5a3db1c1 core: fix typing errors 2023-11-27 09:49:11 +05:00
Abdullah Atta
06f4fee088 core: convert almost everything to typescript 2023-11-27 09:49:11 +05:00
970 changed files with 74475 additions and 296657 deletions

View File

@@ -15,8 +15,6 @@ apps/mobile/e2e/
# web
apps/web/public/an.js
apps/web/src/common/sqlite/wa-sqlite-async.js
apps/web/src/common/sqlite/wa-sqlite.js
# editor
packages/editor/styles/

View File

@@ -5,9 +5,9 @@ runs:
using: "composite"
steps:
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 20.x
node-version: 16.x
cache: "npm"
cache-dependency-path: |
package-lock.json

View File

@@ -6,11 +6,6 @@ jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
NDK_CCACHE: ccache
USE_CCACHE: 1
steps:
- name: Checkout
@@ -29,128 +24,30 @@ jobs:
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
- name: Setup CCache
run: |
ln -s $(which ccache) /usr/local/bin/gcc
ln -s $(which ccache) /usr/local/bin/g++
ln -s $(which ccache) /usr/local/bin/cc
ln -s $(which ccache) /usr/local/bin/c++
ln -s $(which ccache) /usr/local/bin/clang
ln -s $(which ccache) /usr/local/bin/clang++
ccache --set-config=base_dir=$PWD
ccache --set-config=inode_cache=true
ccache --set-config=compiler_check=content
ccache --set-config=depend_mode=true
ccache --set-config=file_clone=true
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
ccache -p
- name: Cache gradle
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Patch CPP Projects to use CCache
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
- name: Build unsigned app bundle
run: yarn release:android:bundle
- name: CCache Stats after build
run: ccache -sv
- name: Sign app bundle for Playstore release
id: sign_app
uses: r0adkll/sign-android-release@master
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Build apks for Github release
run: yarn release:android
- name: Sign apk files
id: sign_apk
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Rename apk files
run: |
cd apps/mobile/native/android/app/build/outputs/apk/release/
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
mv app-x86_64-release-unsigned-signed.apk notesnook-x86_64.apk
- name: Get app version
id: package-version
uses: martinbeentjes/npm-get-version-action@master
with:
path: apps/mobile
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: alpha
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
track: internal
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
repository: streetwriters/notesnook
token: ${{ secrets.GITHUB_TOKEN }}
files: |
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -1,4 +1,4 @@
name: Publish @notesnook/android
name: Publish @notesnook/mobile
on: workflow_dispatch
@@ -6,11 +6,6 @@ jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
NDK_CCACHE: ccache
USE_CCACHE: 1
steps:
- name: Checkout
@@ -33,65 +28,29 @@ jobs:
- name: Make Gradlew Executable
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
- name: Setup CCache
run: |
ln -s $(which ccache) /usr/local/bin/gcc
ln -s $(which ccache) /usr/local/bin/g++
ln -s $(which ccache) /usr/local/bin/cc
ln -s $(which ccache) /usr/local/bin/c++
ln -s $(which ccache) /usr/local/bin/clang
ln -s $(which ccache) /usr/local/bin/clang++
ccache --set-config=base_dir=$PWD
ccache --set-config=inode_cache=true
ccache --set-config=compiler_check=content
ccache --set-config=depend_mode=true
ccache --set-config=file_clone=true
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
ccache -p
- name: Cache gradle
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Patch CPP Projects to use CCache
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
- name: Build unsigned app bundle
run: yarn release:android:bundle
- name: CCache Stats after build
run: ccache -sv
- name: Sign app bundle for Playstore release
id: sign_app
uses: r0adkll/sign-android-release@master
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Build apks for Github release
run: yarn release:android
@@ -105,8 +64,6 @@ jobs:
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Rename apk files
run: |
@@ -122,17 +79,6 @@ jobs:
with:
path: apps/mobile
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -146,11 +92,3 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -18,21 +18,6 @@ on:
required: true
default: true
description: "Publish on GitHub releases?"
build-windows:
type: boolean
required: true
default: true
description: "Build for Windows?"
build-linux:
type: boolean
required: true
default: true
description: "Build for Linux?"
build-mac:
type: boolean
required: true
default: true
description: "Build for macOS?"
jobs:
build:
@@ -58,7 +43,7 @@ jobs:
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: build
path: apps/web/build/**/*
@@ -82,13 +67,11 @@ jobs:
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
build-macos:
name: Build for macOS
needs: build
if: inputs.build-mac
runs-on: macos-14
runs-on: macos-12
steps:
- name: Check out Git repository
@@ -97,9 +80,6 @@ jobs:
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install setuptools
run: brew install python-setuptools
- name: Setup notarization
run: |
mkdir -p ~/private_keys/
@@ -126,7 +106,7 @@ jobs:
platform: "MAC_OS"
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -147,7 +127,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npx nx run release --project @notesnook/desktop -- --variant=mas
yarn electron-builder --mac mas --universal -p never
npx electron-builder --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
@@ -157,13 +137,13 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npx nx run release --project @notesnook/desktop
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
npx electron-builder --mac zip dmg --arm64 --x64 -p always
else
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
npx electron-builder --mac zip dmg --arm64 --x64 -p never
fi
working-directory: ./apps/desktop
@@ -183,7 +163,6 @@ jobs:
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
steps:
@@ -202,7 +181,7 @@ jobs:
if: inputs.publish-snap
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -219,7 +198,7 @@ jobs:
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --linux snap:x64 -p never
npx electron-builder --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
@@ -227,9 +206,9 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
else
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
@@ -242,27 +221,17 @@ jobs:
build-windows:
name: Build for Windows
needs: build
if: inputs.build-windows
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Install TrustedSigning
run: Install-Module -Name TrustedSigning -RequiredVersion 0.3.15 -Force -Repository PSGallery
shell: pwsh
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -278,9 +247,6 @@ jobs:
- name: Publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
if ($${{ inputs.publish-github }} -eq $true) {
yarn electron-builder --win --publish always

View File

@@ -1,107 +0,0 @@
name: Publish @notesnook/ios
on: workflow_dispatch
jobs:
build:
runs-on: macos-13
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npx nx run @notesnook/mobile:build
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/native/ios/Podfile.lock')) }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
- name: Setup ccache
run: |
ccache --set-config=base_dir=$PWD
ccache --set-config=inode_cache=true
ccache --set-config=compiler_check=content
ccache --set-config=depend_mode=true
ccache --set-config=file_clone=true
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
ln -s $(which ccache) /usr/local/bin/gcc
ln -s $(which ccache) /usr/local/bin/g++
ln -s $(which ccache) /usr/local/bin/cc
ln -s $(which ccache) /usr/local/bin/c++
ln -s $(which ccache) /usr/local/bin/clang
ln -s $(which ccache) /usr/local/bin/clang++
ccache -p
- name: Cache Pods
uses: actions/cache@v2
id: pods-cache
with:
path: apps/mobile/native/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
- name: Install Pods
run: npm run prepare:ios
- name: CCache Stats Before Build
run: ccache -sv
- 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/native/ios/ExportOptions.plist
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/native/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_APP }}
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
- name: CCache Stats After Build
run: ccache -sv
- name: "Upload app to TestFlight"
uses: apple-actions/upload-testflight-build@v1
with:
app-path: Notesnook.ipa
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
api-key-id: ${{ secrets.API_KEY_ID }}
api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v4
with:
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

67
.github/workflows/web.beta.publish.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Publish @notesnook/web Beta
on:
workflow_dispatch:
push:
branches:
- master
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
pull_request:
types:
- closed
- ready_for_review
- reopened
- opened
- synchronize
branches:
- master
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate beta build
run: npm run build:beta:web
- name: Publish to Cloudflare Pages
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
id: pages
with:
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-project-name: notesnook-beta-app
directory: ./apps/web/build
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}

View File

@@ -40,7 +40,7 @@ jobs:
run: npm run build:test:web
- name: Archive build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: build
path: apps/web/build/**/*
@@ -56,7 +56,7 @@ jobs:
uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -67,7 +67,7 @@ jobs:
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm install -D @playwright/test otplib dayjs @zip.js/zip.js
npm install -D @playwright/test otplib
- name: Validate build extraction
run: ls -ld ./apps/web/build/index.html
@@ -88,7 +88,7 @@ jobs:
working-directory: apps/web
- name: Upload test results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
if: failure()
with:
name: test-results-${{ matrix.shard }}

3
.gitignore vendored
View File

@@ -9,5 +9,4 @@ nx-cloud.env
.env.local
.nyc_output
site
node_modules.backup
.nx
node_modules.backup

View File

@@ -11,5 +11,4 @@ apps/mobile/native/
apps/web/public/an.js
# editor
packages/editor/styles/
/.nx/cache
packages/editor/styles/

View File

@@ -1,5 +1,4 @@
node_modules
build
output
dist
_catalog
dist

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 36 KiB

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.0.3",
"version": "2.6.12",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
@@ -14,11 +14,9 @@
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.38.3",
"@trpc/server": "10.38.3",
"better-sqlite3-multiple-ciphers": "^9.5.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.4",
"icojs": "^0.17.1",
"sodium-native": "^4.1.1",
"typed-emitter": "^2.1.0",
"yargs": "^17.6.2",
"zod": "^3.21.4"
@@ -27,13 +25,12 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "^29.3.1",
"electron-builder": "^24.13.3",
"esbuild": "^0.20.0",
"node-gyp-build": "^4.8.0",
"prebuildify": "^6.0.1",
"electron": "25.9.3",
"electron-builder": "^24.6.4",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"tree-kill": "^1.2.2",
"undici": "^6.14.1"
"undici": "^5.23.0"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -43,7 +40,7 @@
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "tsc",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --minify --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"
},
@@ -57,7 +54,7 @@
"productName": "Notesnook",
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
"artifactName": "notesnook_${os}_${arch}.${ext}",
"generateUpdatesFilesForAllChannels": true,
"npmRebuild": false,
"asar": false,
"files": [
"!*.chunk.js.map",
@@ -67,17 +64,9 @@
"!build/banner.jpg",
"!build/*.ico",
"!build/*.png",
"!node_modules${/*}",
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
"node_modules/better-sqlite3-multiple-ciphers/lib",
"node_modules/better-sqlite3-multiple-ciphers/package.json",
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
"!node_modules${/*}"
],
"afterSign": "electron-builder-notarize",
"afterPack": "./scripts/removeLocales.js",
"mac": {
"bundleVersion": "240",
@@ -105,8 +94,7 @@
"entitlements": "assets/entitlements.mac.plist",
"entitlementsInherit": "assets/entitlements.mac.plist",
"gatekeeperAssess": false,
"icon": "assets/icons/app.icns",
"notarize": true
"icon": "assets/icons/app.icns"
},
"dmg": {
"contents": [
@@ -147,10 +135,6 @@
]
}
],
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {

View File

@@ -1,46 +0,0 @@
diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js
index 3fbcdf0..7ca3ab5 100644
--- a/node_modules/node-gyp-build/bin.js
+++ b/node_modules/node-gyp-build/bin.js
@@ -16,7 +16,8 @@ if (!buildFromSource()) {
}
function build () {
- var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
+ var win32 = os.platform() === 'win32'
+ var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
try {
var pkg = require('node-gyp/package.json')
@@ -27,7 +28,7 @@ function build () {
]
} catch (_) {}
- proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) {
+ proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell: win32, windowsHide: true }).on('exit', function (code) {
if (code || !process.argv[3]) process.exit(code)
exec(process.argv[3]).on('exit', function (code) {
process.exit(code)
@@ -45,15 +46,18 @@ function preinstall () {
function exec (cmd) {
if (process.platform !== 'win32') {
- var shell = os.platform() === 'android' ? 'sh' : '/bin/sh'
- return proc.spawn(shell, ['-c', '--', cmd], {
+ var shell = os.platform() === 'android' ? 'sh' : true
+ return proc.spawn(cmd, [], {
+ shell,
stdio: 'inherit'
})
}
- return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], {
+ return proc.spawn(cmd, [], {
windowsVerbatimArguments: true,
- stdio: 'inherit'
+ stdio: 'inherit',
+ shell: true,
+ windowsHide: true
})
}

View File

@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import fs from "fs/promises";
import { existsSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
@@ -28,76 +28,90 @@ import { fileURLToPath } from "url";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
);
const sodiumNativePrebuildPath = (arch) =>
path.join(
`node_modules`,
`@notesnook`,
`crypto`,
`node_modules`,
`@notesnook`,
`sodium`,
`node_modules`,
`sodium-native`,
`prebuilds`,
`${os.platform()}-${arch}`
);
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(
"yarn nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
await exec(`cd ${webAppPath} && npm run build:desktop`);
}
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
if (os.platform() === "win32")
await exec(
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
path.join(__dirname, "..", "node_modules", "sodium-native")
);
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
});
if (args.variant === "mas") {
await exec(`yarn run bundle:mas`);
await exec(`npm run bundle:mas`);
} else {
await exec(`yarn run bundle`);
await exec(`npm run bundle`);
}
await exec(`yarn tsc`);
await exec(`npx tsc`);
if (existsSync(sodiumNativePrebuildPath("x64"))) {
console.log("copying sodium-native-x64");
await fs.cp(
sodiumNativePrebuildPath("x64"),
path.join("build", "prebuilds", `${process.platform}-x64`),
{
recursive: true,
force: true
}
);
}
if (existsSync(sodiumNativePrebuildPath("ia32"))) {
console.log("copying sodium-native-ia32");
await fs.cp(
sodiumNativePrebuildPath("ia32"),
path.join("build", "prebuilds", `${process.platform}-ia32`),
{
recursive: true,
force: true
}
);
}
if (existsSync(sodiumNativePrebuildPath("arm64"))) {
console.log("copying sodium-native-arm64");
await fs.cp(
sodiumNativePrebuildPath("arm64"),
path.join("build", "prebuilds", `${process.platform}-arm64`),
{
recursive: true,
force: true
}
);
}
if (args.run) {
await exec(`yarn electron-builder --dir --x64`);
await exec(`npx electron-builder --dir --x64`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
await exec(`./output/darwin-unpacked/Notesnook`);
} else {
await exec(`./output/linux-unpacked/Notesnook`);
}
}
async function exec(cmd, cwd) {
async function exec(cmd) {
return childProcess.execSync(cmd, {
env: { ...process.env, NOTESNOOK_STAGING: true },
stdio: "inherit",
cwd: cwd || process.cwd()
env: process.env,
stdio: "inherit"
});
}
async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "9.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}

View File

@@ -27,15 +27,21 @@ import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sodiumNativePrebuildPath = path.join(
`node_modules`,
`@notesnook`,
`crypto`,
`node_modules`,
`@notesnook`,
`sodium`,
`node_modules`,
`sodium-native`,
`prebuilds`
);
const RUNNING_PROCESSES = [];
const RESTARTABLE_PROCESSES = [];
let lastBundleHash = null;
const ENV = {
...process.env,
NO_COLOR: "true",
FORCE_COLOR: "false",
COLOR: "0"
};
await onChange(true);
console.log("Watching...");
@@ -52,21 +58,26 @@ process.on("SIGINT", async (s) => {
async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("yarn electron-builder install-app-deps");
}
await exec(`yarn run bundle`);
execAsync(`yarn`, [`tsc`]);
await exec(`npm run bundle`);
execAsync(`npx`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
return;
}
if (first) {
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
recursive: true,
force: true
});
}
if (first) {
await spawnAndWaitUntil(
["yarn", "nx", "start:desktop", "@notesnook/web"],
["npx", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
@@ -78,7 +89,7 @@ async function onChange(first) {
}
execAsync(
"yarn",
"npx",
["electron", path.join("build", "electron.js")],
true,
cleanup
@@ -91,7 +102,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
const s = spawn(cmd[0], cmd.slice(1), {
cwd,
env: ENV,
env: { ...process.env, NO_COLOR: "true" },
shell: false
});
@@ -105,15 +116,14 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
});
}
async function exec(cmd, cwd) {
async function exec(cmd) {
try {
console.log(">", cmd);
return execSync(cmd, {
env: ENV,
env: process.env,
stdio: "inherit",
shell: false,
cwd: cwd || process.cwd()
shell: false
});
} catch {
//ignore
@@ -126,7 +136,7 @@ function execAsync(cmd, args, restartable, onExit) {
const proc = spawn(cmd, args, {
stdio: "inherit",
env: ENV,
env: process.env,
shell: false
});
@@ -169,10 +179,9 @@ async function cleanup() {
}
async function isBundleSame() {
const bundle = Buffer.concat([
await fs.readFile(path.join(__dirname, "..", "build", "electron.js")),
await fs.readFile(path.join(__dirname, "..", "build", "preload.js"))
]);
const bundle = await fs.readFile(
path.join(__dirname, "..", "build", "electron.js")
);
const hashSum = crypto.createHash("sha256");
hashSum.update(bundle);

View File

@@ -1,59 +0,0 @@
/*
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/>.
*/
const { writeFileSync, rmSync } = require("fs");
const { execSync } = require("child_process");
const { relative, join } = require("path");
module.exports = async function (configuration) {
if (process.env.NOTESNOOK_STAGING) return;
const Endpoint = "https://weu.codesigning.azure.net";
const CodeSigningAccountName = "Notesnook";
const CertificateProfileName = "Notesnook";
const FileDigest = configuration.hash.toUpperCase();
const TimestampRfc3161 = "http://timestamp.acs.microsoft.com";
const TimestampDigest = configuration.hash.toUpperCase();
const Description = "The Notesnook app";
const DescriptionUrl = "https://notesnook.com/";
const FilesCatalog = createCatalog(configuration.path);
const command = `Invoke-TrustedSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
console.debug("Signing", configuration.path, "using command", command);
psexec(command);
console.debug("Signed", configuration.path);
rmSync(FilesCatalog);
};
function createCatalog(path) {
const catalogPath = join(__dirname, "_catalog");
writeFileSync(catalogPath, relative(__dirname, path));
return catalogPath;
}
function psexec(cmd) {
return execSync(cmd, {
env: process.env,
stdio: "inherit",
shell: "pwsh"
});
}

View File

@@ -23,8 +23,6 @@ import { osIntegrationRouter } from "./os-integration";
import { spellCheckerRouter } from "./spell-checker";
import { updaterRouter } from "./updater";
import { bridgeRouter } from "./bridge";
import { safeStorageRouter } from "./safe-storage";
import { windowRouter } from "./window";
const t = initTRPC.create();
@@ -33,9 +31,7 @@ export const router = t.router({
integration: osIntegrationRouter,
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter,
safeStorage: safeStorageRouter,
window: windowRouter
bridge: bridgeRouter
});
export const api = router.createCaller({});

View File

@@ -31,8 +31,6 @@ import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
const t = initTRPC.create();
@@ -56,23 +54,6 @@ export const osIntegrationRouter = t.router({
config.zoomFactor = factor;
}),
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean().optional())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = !!customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure
.input(z.string().optional())
.mutation(({ input: proxyRules }) => {
globalThis.window?.webContents.session.setProxy({ proxyRules });
config.proxyRules = proxyRules || "";
}),
privacyMode: t.procedure.query(() => config.privacyMode),
setPrivacyMode: t.procedure
.input(z.object({ enabled: z.boolean() }))
@@ -145,13 +126,10 @@ export const osIntegrationRouter = t.router({
.input(z.object({ filePath: z.string() }))
.query(({ input }) => {
const { filePath } = input;
if (!filePath) return;
return resolvePath(filePath);
}),
deleteFile: t.procedure.input(z.string()).query(async ({ input }) => {
await rm(input);
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {
@@ -180,42 +158,16 @@ export const osIntegrationRouter = t.router({
}),
bringToFront: t.procedure.query(() => bringToFront()),
changeTheme: t.procedure
.input(
z.object({
theme: Theme,
windowControlsIconColor: z.string().optional(),
backgroundColor: z.string().optional()
})
)
.mutation(
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor;
if (process.platform === "win32")
globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor
});
}
if (backgroundColor) {
config.backgroundColor = backgroundColor;
}
setTheme(theme);
}
),
.input(Theme)
.mutation(({ input }) => setTheme(input)),
onThemeChanged: t.procedure.subscription(() =>
observable<"dark" | "light">((emit) => {
const updated = () => {
nativeTheme.on("updated", () => {
if (getTheme() === "system") {
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
}
};
nativeTheme.on("updated", updated);
return () => {
nativeTheme.off("updated", updated);
};
});
})
)
});

View File

@@ -1,44 +0,0 @@
/*
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 { safeStorage } from "electron";
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const safeStorageRouter = t.router({
isEncryptionAvailable: t.procedure.query(() => {
return (
!process.env.PORTABLE_EXECUTABLE_DIR &&
safeStorage.isEncryptionAvailable()
);
}),
/**
* Takes a string and returns an encrypted base64 string
*/
encryptString: t.procedure.input(z.string()).query(({ input }) => {
return safeStorage.encryptString(input).toString("base64");
}),
/**
* Takes an encrypted base64 string and returns a string
*/
decryptString: t.procedure.input(z.string()).query(({ input }) => {
return safeStorage.decryptString(Buffer.from(input, "base64"));
})
});

View File

@@ -118,13 +118,5 @@ export const spellCheckerRouter = t.router({
.mutation(({ input: { enabled } }) => {
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
config.isSpellCheckerEnabled = enabled;
}),
words: t.procedure.query(() =>
globalThis.window?.webContents.session.listWordsInSpellCheckerDictionary()
),
deleteWord: t.procedure.input(z.string()).mutation(({ input: word }) => {
globalThis.window?.webContents.session.removeWordFromSpellCheckerDictionary(
word
);
})
})
});

View File

@@ -28,20 +28,16 @@ type UpdateInfo = { version: string };
type Progress = { percent: number };
const t = initTRPC.create();
let cancellationToken: CancellationToken | undefined = undefined;
export const updaterRouter = t.router({
autoUpdates: t.procedure.query(() => config.automaticUpdates),
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
if (cancellationToken) return;
cancellationToken = new CancellationToken();
await autoUpdater
.downloadUpdate(cancellationToken)
.finally(() => (cancellationToken = undefined));
const cancellationToken = new CancellationToken();
await autoUpdater.downloadUpdate(cancellationToken);
}),
check: t.procedure.query(async () => {
await autoUpdater.checkForUpdates().catch(console.error);
await autoUpdater.checkForUpdates();
}),
toggleAutoUpdates: t.procedure
@@ -75,7 +71,6 @@ function createSubscription<
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
emit.next(args[0]);
};
autoUpdater.removeAllListeners(eventName);
autoUpdater.addListener(eventName, listener);
return () => {
autoUpdater.removeListener(eventName, listener);

View File

@@ -1,83 +0,0 @@
/*
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 { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
const t = initTRPC.create();
export const windowRouter = t.router({
maximize: t.procedure.mutation(() => {
globalThis.window?.maximize();
}),
restore: t.procedure.mutation(() => {
globalThis.window?.restore();
}),
minimze: t.procedure.mutation(() => {
globalThis.window?.minimize();
}),
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
onWindowStateChanged: t.procedure.subscription(() => {
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
function listener() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: !!globalThis.window?.isFullScreen()
});
}
function enterFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: true
});
}
function leaveFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: false
});
}
globalThis.window?.addListener("maximize", listener);
globalThis.window?.addListener("minimize", listener);
globalThis.window?.addListener("unmaximize", listener);
globalThis.window?.addListener("restore", listener);
globalThis.window?.addListener("enter-full-screen", enterFullscreen);
globalThis.window?.addListener("leave-full-screen", leaveFullscreen);
globalThis.window?.addListener("leave-html-full-screen", leaveFullscreen);
globalThis.window?.addListener("enter-html-full-screen", enterFullscreen);
return () => {
globalThis.window?.removeListener("maximize", listener);
globalThis.window?.removeListener("minimize", listener);
globalThis.window?.removeListener("unmaximize", listener);
globalThis.window?.removeListener("restore", listener);
globalThis.window?.removeListener("enter-full-screen", enterFullscreen);
globalThis.window?.removeListener("leave-full-screen", leaveFullscreen);
globalThis.window?.removeListener(
"leave-html-full-screen",
leaveFullscreen
);
globalThis.window?.removeListener(
"enter-html-full-screen",
enterFullscreen
);
};
});
})
});

View File

@@ -20,4 +20,3 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
export * from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";

View File

@@ -35,7 +35,6 @@ import path from "path";
import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -83,19 +82,6 @@ async function createWindow() {
size: 512,
format: process.platform === "win32" ? "ico" : "png"
}),
titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
},
webPreferences: {
zoomFactor: config.zoomFactor,
nodeIntegration: true,
@@ -126,7 +112,6 @@ async function createWindow() {
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
mainWindow.once("closed", () => {
globalThis.window = null;
@@ -136,7 +121,7 @@ async function createWindow() {
setupJumplist();
if (isDevelopment())
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
@@ -152,9 +137,6 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
if (config.customDns) enableCustomDns();
else disableCustomDns();
if (!isDevelopment()) registerProtocol();
await createWindow();
configureAutoUpdater();

View File

@@ -23,13 +23,11 @@ import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
import type { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
import { platform } from "os";
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: RendererGlobalElectronTRPC;
var NativeNNCrypto: (new () => NNCrypto) | undefined;
var createSQLite3Database: (filename: string) => Database;
}
process.once("loaded", async () => {
@@ -42,6 +40,11 @@ process.once("loaded", async () => {
globalThis.electronTRPC = electronTRPC;
});
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
globalThis.NativeNNCrypto =
process.platform === "win32" &&
process.arch !== "x64" &&
process.arch !== "ia32"
? undefined
: require("@notesnook/crypto").NNCrypto;
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -25,11 +25,7 @@ Type=Application
Version=${app.getVersion()}
Name=${app.getName()}
Comment=${app.getName()} startup script
Exec=${
process.env.APPIMAGE
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
: `${process.execPath}${hidden ? " --hidden" : ""}`
}
Exec=${process.execPath}${hidden ? " --hidden" : ""}
StartupNotify=false
Terminal=false`;

View File

@@ -20,15 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { autoUpdater } from "electron-updater";
import { config } from "./config";
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
? "beta"
: "latest";
async function configureAutoUpdater() {
autoUpdater.setFeedURL({
provider: "generic",
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
useMultipleRangeRequest: false,
channel: CHANNEL
url: `https://notesnook.com/releases/${process.platform}/`,
useMultipleRangeRequest: false
});
autoUpdater.autoDownload = config.automaticUpdates;

View File

@@ -41,13 +41,7 @@ export const config = {
isSpellCheckerEnabled: true,
zoomFactor: 1,
theme: nativeTheme.themeSource,
automaticUpdates: true,
proxyRules: "",
customDns: true,
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:
nativeTheme.themeSource === "dark" ? "#ffffff" : "#000000"
automaticUpdates: true
};
type ConfigKey = keyof typeof config;

View File

@@ -1,37 +0,0 @@
/*
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 { app } from "electron";
export function enableCustomDns() {
app.configureHostResolver({
secureDnsServers: [
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true
});
}
export function disableCustomDns() {
app.configureHostResolver({
secureDnsServers: [],
enableBuiltInResolver: true
});
}

View File

@@ -131,17 +131,6 @@ function setupMenu() {
);
}
if (params.mediaType === "image")
menu.append(
new MenuItem({
id: "copy-image",
label: "Copy Image",
click() {
globalThis.window?.webContents.copyImageAt(params.x, params.y);
}
})
);
if (params.isEditable)
menu.append(
new MenuItem({

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 { net, protocol } from "electron";
import { protocol } from "electron";
import { isDevelopment } from "./index";
import { createReadStream } from "fs";
import { extname, normalize } from "path";
@@ -62,7 +62,26 @@ function registerProtocol() {
headers: { "Content-Type": extensionToMimeType[fileExtension] }
});
} else {
return net.fetch(request, { bypassCustomProtocolHandlers: true });
if (request.headers.has("X-Content-Length")) {
request.headers.set(
"Content-Length",
request.headers.get("X-Content-Length") || "0"
);
request.headers.delete("X-Content-Length");
}
const headers = Object.fromEntries(request.headers.entries());
return await fetch(request.url, {
signal: request.signal,
mode: request.mode,
headers,
method: request.method,
body: request.body,
credentials: request.credentials,
referrer: (request as any).referrer,
duplex: "half",
redirect: "manual"
});
}
});
console.info(`${SCHEME} protocol inteception "successful"`);

View File

@@ -41,10 +41,7 @@ function setTheme(theme: Theme) {
}
function getBackgroundColor() {
return (
config.backgroundColor ||
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
);
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
}
function getSystemTheme() {

View File

@@ -16,13 +16,15 @@ 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 "@azure/core-asynciterator-polyfill";
import "react-native-gesture-handler";
import SettingsService from "./services/settings";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { I18nManager, View } from "react-native";
import { View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
@@ -30,25 +32,19 @@ import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
import { themeTrpcClient } from "./screens/settings/theme-selector";
const App = () => {
const init = useAppEvents();
useEffect(() => {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
const { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -17,39 +17,16 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import Sodium from "@ammarahmed/react-native-sodium";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
// app lock with password enabled.
export const CipherStorage = new MMKVLoader()
.withInstanceID("cipher_storage")
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
const KEYSTORE_CONFIG = Platform.select({
ios: {
@@ -60,231 +37,61 @@ const KEYSTORE_CONFIG = Platform.select({
android: {}
});
function generatePassword() {
const length = 80;
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
"Crypto API is not supported. Please upgrade your web browser"
);
}
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
const indexes = crypto.getRandomValues(new Uint32Array(length));
let secret = "";
for (const index of indexes) {
secret += charset[index % charset.length];
}
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = await encrypt(appLockCredentials, key);
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
MMKV.removeItem(DB_KEY_CIPHER);
return true;
}
export async function setAppLockVerificationCipher(appLockPassword) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = await encrypt(appLockCredentials, generatePassword());
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
console.log(e);
}
}
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const decrypted = await decrypt(
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
appLockCipher
);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = credentials.password;
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
);
if (userKeyCredentials) {
const userKeyCipher = await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_USER_KEY_SALT
},
userKeyCredentials.password
);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
return DB_KEY;
} catch (e) {
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKey(data) {
export async function deriveCryptoKey(name, data) {
try {
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
{
key: await getDatabaseKey(),
salt: NOTESNOOK_USER_KEY_SALT
},
credentials.key
await Keychain.setInternetCredentials(
"notesnook",
name,
credentials.key,
KEYSTORE_CONFIG
);
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
console.error(e);
}
}
async function upgradeIOSKeychain(username, password) {
if (Platform.OS !== "ios") return;
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
await Keychain.setInternetCredentials(
"notesnook",
username,
password,
KEYSTORE_CONFIG
);
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
}
}
export async function getCryptoKey(_name) {
try {
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
if (await Keychain.hasInternetCredentials("notesnook")) {
let credentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
);
// upgrades ios keychain to use accessGroups
// so we have access to keychain in share extension.
await upgradeIOSKeychain(credentials.username, credentials.password);
return credentials.password;
} else {
return null;
}
const key = await decrypt(
{
key: await getDatabaseKey()
},
keyCipher
);
DatabaseLogger.info("User key decrypted: ", !!key);
return key;
} catch (e) {
console.log("getCryptoKey", e);
DatabaseLogger.error(e);
console.error(e);
}
}
export async function removeCryptoKey(_name) {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
let result = await Keychain.resetInternetCredentials("notesnook");
return result;
} catch (e) {
DatabaseLogger.error(e);
console.error(e);
}
}
@@ -302,7 +109,7 @@ export async function generateCryptoKey(password, salt) {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
DatabaseLogger.error(e);
console.log("generateCryptoKey: ", e);
}
}
@@ -316,8 +123,6 @@ export async function decrypt(password, data) {
return undefined;
let _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
return await Sodium.decrypt(password, _data);
}
@@ -330,11 +135,6 @@ export async function decryptMulti(password, data) {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
return await Sodium.decryptMulti(password, data);
}

View File

@@ -27,7 +27,6 @@ import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
database.host(
__DEV__
@@ -37,11 +36,11 @@ database.host(
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: "http://192.168.43.5:5264",
// AUTH_HOST: "http://192.168.43.5:8264",
// SSE_HOST: "http://192.168.43.5:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
// ISSUES_HOST: "http://192.168.43.5:2624"
// API_HOST: "http://192.168.43.108:5264",
// AUTH_HOST: "http://192.168.43.108:8264",
// SSE_HOST: "http://192.168.43.108:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.108:9264",
// ISSUES_HOST: "http://192.168.43.108:2624"
}
: {
API_HOST: "https://api.notesnook.com",
@@ -52,37 +51,30 @@ database.host(
}
);
export async function setupDatabase(password) {
const key = await getDatabaseKey(password);
if (!key)
throw new Error("Database setup failed, could not get database key");
console.log("Opening database with key:", !!key);
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({
createDriver: () => {
return new RNSqliteDriver({ async: true, dbName: name });
},
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}),
tempStore: "memory",
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL",
password: key
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 500,
sqliteOptions: {
dialect: {
createDriver: () =>
new RNSqliteDriver({ async: true, dbName: "test.db" }),
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}
});
}
// journalMode: "MEMORY",
// synchronous: "normal",
// pageSize: 8192,
// cacheSize: -16000,
// lockingMode: "exclusive"
}
});
export const db = database;
export const DatabaseLogger = dbLogger;

View File

@@ -45,15 +45,15 @@ export class RNSqliteDriver implements Driver {
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("BEGIN TRANSACTION"));
await connection.executeQuery(CompiledQuery.raw("begin"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("COMMIT"));
await connection.executeQuery(CompiledQuery.raw("commit"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("ROLLBACK"));
await connection.executeQuery(CompiledQuery.raw("rollback"));
}
async releaseConnection(): Promise<void> {
@@ -106,9 +106,9 @@ class RNSqliteConnection implements DatabaseConnection {
: query.kind === "RawNode"
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);
// console.log("SQLITE result:", result?.rows?._array);
if (mode === "query" || !result.insertId)
return {
rows: result.rows?._array || []

View File

@@ -91,8 +91,7 @@ export class KV {
}
async removeMulti(keys) {
if (!keys) return true;
return this.storage.removeItems(keys);
return this.storage.removeItems(...keys);
}
async clear() {

View File

@@ -16,61 +16,20 @@ 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 { Dimensions, Image, Platform } from "react-native";
import { Dimensions, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "react-native-blob-util";
console.log("hello world");
/**
* Scale down & compress images to screen width
* for loading in editor.
* @returns
*/
export async function compressToBase64(path, type) {
const { width: screenWidth, scale } = Dimensions.get("window");
console.log("COMPRESSING TO BASE64...");
return new Promise((resolve) => {
console.log(path, "image path...");
Image.getSize(path, async (width) => {
console.log("image width", width);
const response = await ImageResizer.createResizedImage(
path,
screenWidth * scale,
99999,
type,
width <= 1920 ? 100 : 90,
0,
undefined,
true,
{
mode: "contain",
onlyScaleDown: true
}
);
const base64 = await RNFetchBlob.fs.readFile(
Platform.OS === "ios"
? response.uri?.replace("file://", "")
: response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs
.unlink(response.uri.replace("file://", ""))
.catch(console.log);
resolve(base64);
});
});
}
export async function compressToFile(path, type) {
const { width, scale } = Dimensions.get("window");
const response = await ImageResizer.createResizedImage(
path,
1920,
99999,
width * scale,
9999,
type,
80,
0,
@@ -81,6 +40,11 @@ export async function compressToFile(path, type) {
onlyScaleDown: true
}
);
const base64 = await RNFetchBlob.fs.readFile(
Platform.OS === "ios" ? response.uri?.replace("file://", "") : response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
return response.uri;
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
return base64;
}

View File

@@ -31,11 +31,10 @@ import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { createCacheDir, exists } from "./io";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
export const FileDownloadStatus = {
Success: 1,
@@ -46,15 +45,15 @@ export const FileDownloadStatus = {
* Download all user's attachments
* @returns
*/
export async function downloadAllAttachments() {
const attachments = await db.attachments.all.ids();
export function downloadAllAttachments() {
const attachments = db.attachments.all;
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachments
* @param attachments
* @param onProgress
* @returns
*/
@@ -84,15 +83,13 @@ export async function downloadAttachments(
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
if (await RNFetchBlob.fs.exists(zipSourceFolder))
await RNFetchBlob.fs.unlink(zipSourceFolder);
}
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
let attachment = attachments[i];
const hash = attachment.metadata.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -116,20 +113,19 @@ export async function downloadAttachments(
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
});
} catch (e) {
result.set(hash, {
filename: attachment.filename,
filename: attachment.metadata.filename,
status: FileDownloadStatus.Fail,
reason: e
});
ToastManager.error(e, "Error downloading attachment");
console.log("Error downloading attachment", e);
}
}
if (canceled.current) {
@@ -166,7 +162,7 @@ export async function downloadAttachments(
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
console.log("Error zipping attachments", e);
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -185,16 +181,14 @@ export default async function downloadAttachment(
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
groupId: undefined
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
DatabaseLogger.log("Attachment not found");
console.log("attachment not found");
return;
}
@@ -209,42 +203,41 @@ export default async function downloadAttachment(
}
try {
console.log(
"starting download attachment",
attachment.metadata.hash,
options.groupId
);
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options.base64 || options.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
}
if (
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
)
return;
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
attachment.metadata.filename,
attachment.metadata.type
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
length: attachment.length,
alg: attachment.alg,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType,
mime: attachment.metadata.type,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
chunkSize: attachment.chunkSize
};
let fileUri = await Sodium.decryptFile(
key,
info,
@@ -261,11 +254,11 @@ export default async function downloadAttachment(
if (
attachment.dateUploaded &&
!isImage(attachment.mimeType) &&
!isDocument(attachment.mimeType)
!isImage(attachment.metadata?.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
@@ -280,23 +273,20 @@ export default async function downloadAttachment(
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.hash,
context: global ? null : attachment.metadata.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
return fileUri;
} catch (e) {
DatabaseLogger.error(e);
console.log("download attachment error: ", e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.hash);
useAttachmentStore.getState().remove(attachment.metadata.hash);
if (options.throwError) {
throw e;
}

View File

@@ -22,24 +22,20 @@ import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) {
if (!data) {
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
return false;
}
if (!data) return false;
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
return true;
}
@@ -47,24 +43,13 @@ export async function downloadFile(filename, data, cancelToken) {
method: "GET",
headers
});
if (!res.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
);
if (!res.ok)
throw new Error(`${res.status}: Unable to resolve download url`);
}
const downloadUrl = await res.text();
if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error("Unable to resolve download url");
}
if (!downloadUrl) throw new Error("Unable to resolve download url");
let totalSize = 0;
DatabaseLogger.log(`Download starting: ${filename}`);
console.log("Download starting");
let request = RNFetchBlob.config({
path: path,
IOSBackgroundTask: true
@@ -75,15 +60,13 @@ export async function downloadFile(filename, data, cancelToken) {
.getState()
.setProgress(0, total, filename, recieved, "download");
totalSize = total;
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
console.log("downloading: ", recieved, total);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
DatabaseLogger.log(`Download cancelled: ${filename}`);
};
let response = await request;
await fileCheck(response, totalSize);
let status = response.info().status;
@@ -107,10 +90,7 @@ export async function downloadFile(filename, data, cancelToken) {
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e, {
url,
headers
});
console.log("Download file error:", e, url, headers);
return false;
}
}
@@ -119,17 +99,18 @@ export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return -1;
return 0;
}
}
@@ -138,14 +119,12 @@ export async function checkAttachment(hash) {
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return { success: true };
const attachment = await db.attachments.attachment(hash);
const attachment = db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await getUploadedFileSize(hash);
if (size === -1) return { success: true };
if (size === 0) return { failed: "File length is 0." };
if (size <= 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e?.message };
}

View File

@@ -17,16 +17,17 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "react-native-blob-util";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
console.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
@@ -34,6 +35,10 @@ export async function readEncrypted(filename, key, cipherData) {
return false;
}
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
console.log("decrypting....");
let output = await Sodium.decryptFile(
key,
{
@@ -41,15 +46,25 @@ export async function readEncrypted(filename, key, cipherData) {
hash: filename,
appGroupId: IOS_APPGROUPID
},
cipherData.outputType === "base64" ? "base64" : "text"
cipherData.outputType === "base64"
? isPng || isJpeg
? "cache"
: "base64"
: "text"
);
DatabaseLogger.log("File decrypted...");
console.log("file decrypted...");
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
const dCachePath = `${cacheDir}/${output}`;
output = await compressToBase64(
`file://${dCachePath}`,
isPng ? "PNG" : "JPEG"
);
}
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e);
console.log("readEncrypted", e);
return false;
}
}
@@ -66,7 +81,7 @@ export async function hashBase64(data) {
};
}
export async function writeEncryptedBase64(data, key) {
export async function writeEncryptedBase64({ data, key }) {
await createCacheDir();
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
@@ -76,8 +91,6 @@ export async function writeEncryptedBase64(data, key) {
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
output.size = output.length;
delete output.length;
return {
...output,
alg: "xcha-stream"
@@ -127,7 +140,7 @@ export async function clearFileStorage() {
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
console.log("Cache directory created");
}
}
@@ -177,19 +190,16 @@ export async function exists(filename) {
}
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const attachment = db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
const expectedFileSize = attachment.length + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path
);
if (stat.size !== expectedFileSize) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log);

View File

@@ -25,7 +25,6 @@ import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
import { getUploadedFileSize } from "./download";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -34,18 +33,6 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
DatabaseLogger.log("Upload verification failed.");
return false;
}
const isUploaded = uploadedFileSize !== 0;
if (isUploaded) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
method: "PUT",
headers
@@ -63,15 +50,7 @@ export async function uploadFile(filename, data, cancelToken) {
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({
@@ -108,9 +87,12 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(
`File upload status: ${filename}, ${status}, ${text}`
);
let attachment = await db.attachments.attachment(filename);
let attachment = db.attachments.attachment(filename);
if (!attachment) return result;
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
if (
!isImage(attachment.metadata.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
} else {

View File

@@ -17,20 +17,18 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { useMessageStore } from "../../stores/use-message-store";
import { FlatList, View } from "react-native";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useMessageStore } from "../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { allowedOnPlatform, renderItem } from "./functions";
import { getContainerBorder } from "../../utils/colors";
export const Announcement = ({ color }) => {
const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements);
let announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !announcement || selectionMode ? null : (
<View
style={{
@@ -47,8 +45,7 @@ export const Announcement = ({ color }) => {
borderRadius: 10,
overflow: "hidden",
backgroundColor: colors.secondary.background,
paddingBottom: 12,
...getContainerBorder(colors.secondary.background)
paddingBottom: 12
}}
>
<View

View File

@@ -20,13 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Linking, View } from "react-native";
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { useThemeColors } from "@notesnook/theme";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans";
import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions";
@@ -54,6 +56,13 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
/>
)
});
} else if (item.type === "force-sync") {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
eSendEvent(eCloseSheet);
});
}
};
return (
@@ -94,7 +103,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
key={item.title}
title={item.title}
fontSize={SIZE.sm}
type="plain"
type="gray"
onPress={() => onPress(item)}
width={null}
height={30}
@@ -140,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
key={item.title}
title={item.title}
fontSize={SIZE.xs}
type="plain"
type="gray"
onPress={() => onPress(item)}
width={null}
height={30}

View File

@@ -54,7 +54,7 @@ export const Title = ({ text, style = {}, inline }) => {
</Heading>
<Button
type="plain"
type="gray"
icon="close"
height={null}
onPress={() => {

View File

@@ -18,24 +18,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef } from "react";
import { AppStateStatus, Platform, TextInput, View } from "react-native";
import { Platform, TextInput, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import { enabled } from "react-native-privacy-snapshot";
import { DatabaseLogger } from "../../common/database";
import {
decrypt,
encrypt,
getCryptoKey,
getDatabaseKey,
setAppLockVerificationCipher,
validateAppLockPassword
} from "../../common/database/encryption";
import { MMKV } from "../../common/database/mmkv";
import { db } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { NotesnookModule } from "../../utils/notesnook-module";
import { SIZE } from "../../utils/size";
@@ -46,67 +36,20 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
const getUser = () => {
const user = MMKV.getString("user");
if (user) {
return JSON.parse(user);
}
};
const verifyUserPassword = async (password: string) => {
try {
await getDatabaseKey();
const key = await getCryptoKey();
const user = getUser();
const cipher = await encrypt(
{
key: key,
salt: user.salt
},
"notesnook"
);
const plainText = await decrypt({ password }, cipher);
return plainText === "notesnook";
} catch (e) {
DatabaseLogger.error(e as Error);
return false;
}
};
const AppLockedOverlay = () => {
const initialLaunchBiometricRequest = useRef(true);
const { colors } = useThemeColors();
const user = getUser();
const user = useUserStore((state) => state.user);
const appLocked = useUserStore((state) => state.appLocked);
const lockApp = useUserStore((state) => state.lockApp);
const deviceMode = useSettingStore((state) => state.deviceMode);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>();
const appState = useAppState();
const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false);
const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType
);
const appLockHasPasswordSecurity = useSettingStore(
(state) => state.settings.appLockHasPasswordSecurity
);
const biometricsAuthEnabled = useSettingStore(
(state) =>
state.settings.biometricsAuthEnabled === true ||
(state.settings.biometricsAuthEnabled === undefined &&
!state.settings.appLockHasPasswordSecurity)
);
const onUnlockAppRequested = useCallback(async () => {
if (
!biometricsAuthEnabled ||
!(await BiometricService.isBiometryAvailable())
)
return;
biometricUnlockAwaitingUserInput.current = true;
if (!(await BiometricService.isBiometryAvailable())) return;
if (Platform.OS === "android") {
const activityName = await NotesnookModule.getActivityName();
if (activityName !== "MainActivity") return;
@@ -122,29 +65,17 @@ const AppLockedOverlay = () => {
enabled(false);
password.current = undefined;
}
biometricUnlockAwaitingUserInput.current = false;
setTimeout(() => {
if (biometricUnlockAwaitingUserInput.current) return;
biometricUnlockAwaitingUserInput.current = false;
useSettingStore.getState().setRequestBiometrics(false);
}, 500);
}, [biometricsAuthEnabled, lockApp]);
}, 1);
}, [lockApp]);
const onSubmit = async () => {
if (!password.current) return;
try {
const unlocked = appLockHasPasswordSecurity
? await validateAppLockPassword(password.current)
: await verifyUserPassword(password.current);
const unlocked = await db.user.verifyPassword(password.current);
if (unlocked) {
if (!appLockHasPasswordSecurity) {
await setAppLockVerificationCipher(password.current);
SettingsService.set({
appLockHasPasswordSecurity: true,
applockKeyboardType: "default"
});
DatabaseLogger.info("App lock migrated to password security");
}
lockApp(false);
enabled(false);
password.current = undefined;
@@ -155,161 +86,120 @@ const AppLockedOverlay = () => {
};
useEffect(() => {
if (appState === "active") {
enabled(false);
}
const prevState = lastAppState.current;
lastAppState.current = appState;
if (
appState === "active" &&
(prevState === "background" || initialLaunchBiometricRequest.current)
) {
if (SettingsService.shouldLockAppOnEnterForeground()) {
DatabaseLogger.info("Locking app on entering foreground");
useUserStore.getState().lockApp(true);
}
if (
!(
biometricUnlockAwaitingUserInput.current ||
useUserStore.getState().disableAppLockRequests ||
!appLocked ||
useSettingStore.getState().requestBiometrics ||
useSettingStore.getState().appDidEnterBackgroundForAction
)
) {
initialLaunchBiometricRequest.current = false;
DatabaseLogger.info("Biometric unlock request");
onUnlockAppRequested();
}
enabled(false);
} else {
SettingsService.appEnteredBackground();
if (
SettingsService.get().privacyScreen ||
SettingsService.getProperty("appLockEnabled")
) {
enabled(true);
}
biometricUnlockAwaitingUserInput.current ||
useUserStore.getState().disableAppLockRequests
)
return;
if (appLocked && appState === "active") {
biometricUnlockAwaitingUserInput.current = true;
onUnlockAppRequested();
}
}, [appState, onUnlockAppRequested, appLocked]);
return appLocked ? (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
}}
>
return (
appLocked && (
<View
style={{
flex: 1,
justifyContent: "center",
width:
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,
alignSelf: "center"
backgroundColor: colors.primary.background,
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
}}
>
<IconButton
name="fingerprint"
size={100}
style={{
width: 100,
height: 100,
marginBottom: 20,
marginTop: user ? 0 : 50
}}
onPress={onUnlockAppRequested}
color={colors.primary.border}
/>
<Heading
color={colors.primary.heading}
style={{
alignSelf: "center",
textAlign: "center"
}}
>
Unlock your notes
</Heading>
<Paragraph
style={{
alignSelf: "center",
textAlign: "center",
maxWidth: "90%"
}}
>
{"Please verify it's you"}
</Paragraph>
<Seperator />
<View
style={{
width: "100%",
padding: 12,
backgroundColor: colors.primary.background
flex: 1,
justifyContent: "center",
width:
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,
alignSelf: "center"
}}
>
{user || appLockHasPasswordSecurity ? (
<>
<Input
fwdRef={passwordInputRef}
secureTextEntry
keyboardType={
appLockHasPasswordSecurity ? keyboardType : "default"
}
placeholder={`Enter ${
appLockHasPasswordSecurity
? `app lock ${
keyboardType === "numeric" ? "pin" : "password"
}`
: "account password"
}`}
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();
}}
/>
</>
) : null}
<View
<IconButton
name="fingerprint"
size={100}
customStyle={{
width: 100,
height: 100,
marginBottom: 20,
marginTop: user ? 0 : 50
}}
onPress={onUnlockAppRequested}
color={colors.primary.border}
/>
<Heading
color={colors.primary.heading}
style={{
marginTop: user ? 25 : 25
alignSelf: "center",
textAlign: "center"
}}
>
{user || appLockHasPasswordSecurity ? (
Unlock your notes
</Heading>
<Paragraph
style={{
alignSelf: "center",
textAlign: "center",
maxWidth: "90%"
}}
>
{"Please verify it's you"}
</Paragraph>
<Seperator />
<View
style={{
width: "100%",
padding: 12,
backgroundColor: colors.primary.background
}}
>
{user ? (
<>
<Button
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
height={45}
style={{
borderRadius: 150,
marginBottom: 10
<Input
fwdRef={passwordInputRef}
secureTextEntry
placeholder="Enter account password"
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();
}}
fontSize={SIZE.md}
/>
</>
) : null}
{biometricsAuthEnabled ? (
<View
style={{
marginTop: user ? 25 : 25
}}
>
{user ? (
<>
<Button
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
height={45}
style={{
borderRadius: 150,
marginBottom: 10
}}
fontSize={SIZE.md}
/>
</>
) : null}
<Button
title="Unlock with Biometrics"
width={250}
@@ -317,12 +207,12 @@ const AppLockedOverlay = () => {
icon={"fingerprint"}
type="transparent"
/>
) : null}
</View>
</View>
</View>
</View>
</View>
) : null;
)
);
};
export default AppLockedOverlay;

View File

@@ -40,8 +40,7 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
import {
eCloseAttachmentDialog,
eCloseSheet,
eDBItemUpdate,
eOnLoadNote
eDBItemUpdate
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
@@ -51,22 +50,15 @@ import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
const Actions = ({
attachment,
close,
setAttachments,
fwdRef
}: {
attachment: Attachment;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
@@ -84,7 +76,7 @@ const Actions = ({
name: "Download",
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.hash);
await db.fs().cancel(attachment.hash, "download");
useAttachmentStore.getState().remove(attachment.hash);
}
downloadAttachment(attachment.hash, false);
@@ -107,7 +99,7 @@ const Actions = ({
reupload: true,
hash: attachment.hash,
context: contextId,
type: attachment.mimeType.startsWith("image") ? "image" : "file"
type: attachment.type
});
},
icon: "upload"
@@ -118,7 +110,7 @@ const Actions = ({
setLoading({
name: "Run file check"
});
const res = await filesystem.checkAttachment(attachment.hash);
let res = await filesystem.checkAttachment(attachment.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
@@ -138,7 +130,6 @@ const Actions = ({
});
}
setAttachments();
setLoading({
name: undefined
});
@@ -161,7 +152,6 @@ const Actions = ({
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
}
},
@@ -173,27 +163,9 @@ const Actions = ({
{
name: "Delete",
onPress: async () => {
const relations = await db.relations.to(attachment, "note").get();
await db.attachments.remove(attachment.hash, false);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
relations
.map((relation) => relation.fromId)
.forEach(async (id) => {
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
const isFocused = useTabStore.getState().currentTab === tab;
if (isFocused) {
eSendEvent(eOnLoadNote, {
item: await db.notes.note(id),
forced: true
});
} else {
editorController.current.commands.setLoading(true, tab);
}
}
});
close?.();
eSendEvent(eCloseSheet, contextId);
},
icon: "delete-outline"
}
@@ -219,11 +191,10 @@ const Actions = ({
}}
>
<Dialog context={contextId} />
<SheetProvider context={contextId} />
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
borderBottomColor: colors.secondary.background,
marginBottom: notes && notes.length > 0 ? 0 : 12
}}
>
@@ -240,20 +211,36 @@ const Actions = ({
style={{
flexDirection: "row",
marginBottom: 10,
paddingHorizontal: 12,
marginTop: 6,
gap: 10
paddingHorizontal: 12
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{attachment.mimeType}
<Paragraph
size={SIZE.xs}
style={{
marginRight: 10
}}
color={colors.secondary.paragraph}
>
{attachment.type}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{formatBytes(attachment.size)}
</Paragraph>
{notes.length ? (
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notes.length} note
{notes.length > 1 ? "s" : ""}
</Paragraph>
@@ -281,7 +268,7 @@ const Actions = ({
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
borderBottomColor: colors.secondary.background,
marginBottom: 12,
paddingVertical: 12
}}
@@ -297,7 +284,7 @@ const Actions = ({
</Heading>
{notes.map((item) => (
<Pressable
<PressableButton
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
await sleep(150);
@@ -305,7 +292,7 @@ const Actions = ({
await sleep(300);
openNote(item, (item as any).type === "trash");
}}
style={{
customStyle={{
paddingVertical: 12,
alignItems: "flex-start",
@@ -314,7 +301,7 @@ const Actions = ({
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
</Pressable>
</PressableButton>
))}
</>
</View>
@@ -325,7 +312,7 @@ const Actions = ({
key={item.name}
buttonType={{
text:
item.name === "Delete"
item.name === "Delete" || item.name === "PermDelete"
? colors.error.paragraph
: colors.primary.paragraph
}}
@@ -333,7 +320,7 @@ const Actions = ({
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type="plain"
type="gray"
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -352,7 +339,7 @@ const Actions = ({
{failed ? (
<Notice
type="alert"
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
size="small"
/>
) : null}
@@ -363,19 +350,12 @@ const Actions = ({
Actions.present = (
attachment: Attachment,
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
set: (attachments: VirtualizedGrouping<Attachment>) => void,
context?: string
) => {
presentSheet({
context: context,
component: (ref, close) => (
<Actions
fwdRef={ref}
setAttachments={set}
close={close}
attachment={attachment}
/>
)
component: (ref) => <Actions fwdRef={ref} attachment={attachment} />
});
};

View File

@@ -67,8 +67,9 @@ export const AttachmentItem = ({
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
return (hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value)) ||
!attachment ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -79,111 +80,104 @@ export const AttachmentItem = ({
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.secondary.background,
minHeight: 45
backgroundColor: colors.secondary.background
}}
>
{!attachment ? null : (
<>
<View
<View
style={{
flexShrink: 1,
flexDirection: "row",
alignItems: "center"
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.static.white}
style={{
flexShrink: 1,
flexDirection: "row",
alignItems: "center"
position: "absolute"
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
{getFileExtension(attachment.filename).toUpperCase()}
</Paragraph>
</View>
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.static.white}
style={{
position: "absolute"
}}
>
{getFileExtension(attachment.filename).toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1,
marginLeft: 10
}}
>
<Paragraph
size={SIZE.sm - 1}
style={{
flexWrap: "wrap",
marginBottom: 2.5
}}
numberOfLines={1}
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.filename}
</Paragraph>
<View
style={{
flexShrink: 1,
marginLeft: 10
}}
>
<Paragraph
size={SIZE.sm - 1}
style={{
flexWrap: "wrap",
marginBottom: 2.5
}}
numberOfLines={1}
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
</View>
</View>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
</View>
</View>
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs().cancel(attachment.hash);
setCurrentProgress(undefined);
}}
style={{
justifyContent: "center",
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={
currentProgress?.value ? currentProgress?.value / 100 : 0
}
showsText
textStyle={{
fontSize: 10
}}
color={colors.primary.accent}
formatText={(progress) => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton
onPress={onPress}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
</>
)}
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs().cancel(attachment.hash, currentProgress?.type as any);
setCurrentProgress(undefined);
}}
style={{
justifyContent: "center",
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
showsText
textStyle={{
fontSize: 10
}}
color={colors.primary.accent}
formatText={(progress) => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton
onPress={onPress}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
</>
)}
</TouchableOpacity>

View File

@@ -63,7 +63,7 @@ const DownloadAttachments = ({
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
await attachments.ids(),
attachments,
(progress: number, statusText: string) =>
setProgress({ value: progress, statusText }),
canceled,
@@ -84,27 +84,34 @@ const DownloadAttachments = ({
canceled.current = true;
if (!groupId.current) return;
console.log(groupId.current, "canceling groupId downloads");
await db.fs().cancel(groupId.current);
await db.fs().cancel(groupId.current, "download");
setDownloading(false);
setResult(new Map());
groupId.current = undefined;
};
const successResults = () => {
const results = [];
for (const [key, value] of result.entries()) {
if (value.status === 1) results.push(db.attachments.attachment(key));
}
return results;
};
const failedResults = () => {
const results = [];
for (const value of result.values()) {
if (value.status === 0) results.push(value.attachment);
for (const [key, value] of result.entries()) {
if (value.status === 0) results.push(db.attachments.attachment(key));
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments?.placeholders?.length - failedResults().length;
attachments?.ids?.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments?.placeholders.length
attachments?.ids.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
@@ -167,9 +174,7 @@ const DownloadAttachments = ({
animated={true}
useNativeDriver
progress={
progress.value
? progress.value / attachments.placeholders?.length
: 0
progress.value ? progress.value / attachments.ids?.length : 0
}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
@@ -187,7 +192,7 @@ const DownloadAttachments = ({
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.placeholders : undefined}
data={downloading ? attachments.ids : undefined}
ListEmptyComponent={
<View
style={{
@@ -202,7 +207,7 @@ const DownloadAttachments = ({
</Paragraph>
</View>
}
keyExtractor={(index) => "attachment_download" + index}
keyExtractor={(item, index) => "attachment" + index}
renderItem={({ index }) => {
return (
<AttachmentItem
@@ -246,7 +251,7 @@ const DownloadAttachments = ({
onPress={() => {
close?.();
}}
type="secondary"
type="grayBg"
title="No"
/>
<Button

View File

@@ -61,21 +61,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
const refresh = React.useCallback(() => {
if (note) {
db.attachments
.ofNote(note.id, "all")
.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => {
setAttachments(attachments);
});
db.attachments.ofNote(note.id, "all").sorted(DEFAULT_SORTING);
} else {
db.attachments.all
.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.sorted(DEFAULT_SORTING)
.then((attachments) => setAttachments(attachments));
}
}, [note]);
@@ -84,13 +73,12 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
refresh();
}, [note, refresh]);
const onChangeText = async (text: string) => {
const onChangeText = (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments(await filterAttachments(currentFilter));
refresh();
}
clearTimeout(searchTimer.current);
@@ -98,18 +86,15 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
if (results.length === 0) return;
setAttachments(results);
}, 300);
};
const renderItem = ({ index }: { item: boolean; index: number }) => (
const renderItem = ({ item }: { item: string | number }) => (
<AttachmentItem
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
}}
setAttachments={setAttachments}
attachments={attachments}
id={index}
id={item}
context="attachments-list"
/>
);
@@ -117,10 +102,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
for (let i = 0; i < attachments.placeholders.length; i++) {
const attachment = (await attachments.item(i))?.item;
for (const id of attachments.ids) {
const attachment = (await attachments.item(id))?.item;
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
if (result.failed) {
await db.attachments.markAsFailed(attachment.hash, result.failed);
@@ -160,9 +145,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
switch (type) {
case "all":
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.all;
items = db.attachments.all;
break;
case "images":
items = note
@@ -171,24 +154,19 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
break;
case "video":
items = items = note
? db.attachments.ofNote(note.id, "videos")
? db.attachments.ofNote(note.id, "all")
: db.attachments.videos;
break;
case "audio":
items = note
? db.attachments.ofNote(note.id, "audio")
: db.attachments.audios;
items = db.attachments.all;
break;
case "documents":
items = note
? db.attachments.ofNote(note.id, "documents")
? db.attachments.ofNote(note.id, "all")
: db.attachments.documents;
}
return await items.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
});
return await items.sorted(DEFAULT_SORTING);
};
return (
@@ -227,7 +205,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
) : (
<IconButton
name="check-all"
style={{
customStyle={{
height: 40,
width: 40,
marginRight: 10
@@ -240,7 +218,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
<IconButton
name="download"
style={{
customStyle={{
height: 40,
width: 40
}}
@@ -283,9 +261,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
>
{attachmentTypes.map((item) => (
<Button
type={
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
}
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={item.title}
style={{
@@ -330,7 +306,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
/>
}
estimatedItemSize={50}
data={attachments?.placeholders}
data={attachments?.ids}
renderItem={renderItem}
/>

View File

@@ -17,16 +17,14 @@ 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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { useThemeColors } from "@notesnook/theme";
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
@@ -36,6 +34,7 @@ import { IconButton } from "../ui/icon-button";
import { hideAuth, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
export const AuthMode = {
login: 0,
@@ -69,9 +68,6 @@ const AuthModal = () => {
}
const close = () => {
useUserStore.setState({
disableAppLockRequests: false
});
actionSheetRef.current?.hide();
setCurrentAuthMode(AuthMode.login);
setVisible(false);
@@ -81,11 +77,6 @@ const AuthModal = () => {
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
onShow={() => {
useUserStore.setState({
disableAppLockRequests: true
});
}}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
@@ -95,7 +86,6 @@ const AuthModal = () => {
transparent={false}
animated={false}
centered={false}
enableSheetKeyboardHandler
>
<KeyboardAwareScrollView
style={{
@@ -158,7 +148,7 @@ const AuthModal = () => {
hideAuth();
}}
iconSize={16}
type="plain"
type="gray"
iconPosition="right"
icon="chevron-right"
height={25}

View File

@@ -26,15 +26,12 @@ import {
ToastManager
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
import { eCloseSheet } from "../../utils/events";
import DialogHeader from "../dialog/dialog-header";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { Notice } from "../ui/notice";
import Seperator from "../ui/seperator";
import { Dialog } from "../dialog";
import BackupService from "../../services/backup";
import { sleep } from "../../utils/time";
export const ChangePassword = () => {
const passwordInputRef = useRef();
@@ -67,9 +64,6 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
const result = await BackupService.run(false, "change-password-dialog");
if (!result) throw new Error("Failed to create backup");
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastManager.show({
@@ -79,8 +73,6 @@ export const ChangePassword = () => {
});
setLoading(false);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
setLoading(false);
ToastManager.show({
@@ -100,7 +92,6 @@ export const ChangePassword = () => {
padding: 12
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader
title="Change password"
paragraph="Enter your old and new passwords"
@@ -138,14 +129,7 @@ export const ChangePassword = () => {
/>
<Notice
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
type="alert"
/>
<View style={{ height: 10 }} />
<Notice
text={`Once your password is changed, please make sure to save the new account recovery key.`}
text="Changing password is a non-undoable process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
type="alert"
/>

View File

@@ -113,7 +113,7 @@ export const ForgotPassword = () => {
}}
>
<IconButton
style={{
customStyle={{
width: null,
height: null
}}

View File

@@ -59,6 +59,22 @@ const Auth = ({ navigation, route }) => {
/>
)}
{/* {initialAuthMode.current === AuthMode.welcomeSignup ? null : (
<IconButton
name="arrow-left"
onPress={() => {
hideAuth();
}}
color={colors.primary.paragraph}
customStyle={{
position: 'absolute',
zIndex: 999,
left: 12,
top: Platform.OS === 'ios' ? 12 + insets.top : insets.top
}}
/>
)} */}
<Toast context="local" />
</View>
);

View File

@@ -38,7 +38,6 @@ import { useSettingStore } from "../../stores/use-setting-store";
import { eUserLoggedIn } from "../../utils/events";
import { useUserStore } from "../../stores/use-user-store";
import Sync from "../../services/sync";
import { Notice } from "../ui/notice";
const LoginSteps = {
emailAuth: 1,
@@ -67,7 +66,7 @@ export const Login = ({ changeMode }) => {
Progress.present();
setTimeout(() => {
if (!useUserStore.getState().syncing) {
Sync.run("global", false, "full");
Sync.run("global", false, true);
}
}, 5000);
});
@@ -104,7 +103,7 @@ export const Login = ({ changeMode }) => {
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
borderBottomWidth: 0.8,
borderBottomWidth: 1,
marginBottom: 12,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
@@ -223,7 +222,7 @@ export const Login = ({ changeMode }) => {
textDecorationLine: "underline"
}}
fontSize={SIZE.xs}
type="plain"
type="gray"
/>
</>
)}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { Modal, View } from "react-native";
import { db } from "../../common/database";
import { MMKV } from "../../common/database/mmkv";
import BiometricService from "../../services/biometrics";
@@ -37,7 +37,6 @@ import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import { presentDialog } from "../dialog/functions";
import SheetProvider from "../sheet-provider";
import { Toast } from "../toast";
@@ -47,6 +46,7 @@ import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import BaseDialog from "../dialog/base-dialog";
function getObfuscatedEmail(email) {
if (!email) return "";
@@ -69,9 +69,6 @@ export const SessionExpired = () => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
);
@@ -87,9 +84,6 @@ export const SessionExpired = () => {
MMKV.clearStore();
clearAllStores();
setVisible(false);
useUserStore.setState({
disableAppLockRequests: false
});
} catch (e) {
ToastManager.show({
heading: e.message,
@@ -112,7 +106,7 @@ export const SessionExpired = () => {
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, "full", async (complete) => {
Sync.run("global", false, true, async (complete) => {
if (!complete) {
let user = await db.user.getUser();
if (!user) return;
@@ -133,9 +127,6 @@ export const SessionExpired = () => {
email.current = user.email;
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [email]);
@@ -148,15 +139,9 @@ export const SessionExpired = () => {
animated={false}
centered={false}
onShow={async () => {
useUserStore.setState({
disableAppLockRequests: true
});
await sleep(300);
passwordInputRef.current?.focus();
setFocused(true);
useUserStore.setState({
disableAppLockRequests: true
});
}}
enableSheetKeyboardHandler={true}
visible={true}
@@ -181,7 +166,7 @@ export const SessionExpired = () => {
}}
>
<IconButton
style={{
customStyle={{
width: 60,
height: 60
}}

View File

@@ -111,7 +111,7 @@ export const Signup = ({ changeMode, trial }) => {
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 0.8,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,

View File

@@ -32,7 +32,7 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -151,7 +151,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
}}
>
<IconButton
style={{
customStyle={{
width: 70,
height: 70
}}
@@ -183,7 +183,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
type={seconds ? "plain" : "transparent"}
type={seconds ? "gray" : "transparent"}
title={
sending
? ""
@@ -247,7 +247,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
<Button
title={secondaryMethodsText[currentMethod.method]}
type="plain"
type="gray"
onPress={onRequestSecondaryMethod}
height={30}
/>
@@ -255,7 +255,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
) : (
<>
{getMethods().map((item) => (
<Pressable
<PressableButton
key={item.title}
onPress={() => {
setCurrentMethod({
@@ -263,7 +263,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
isPrimary: false
});
}}
style={{
customStyle={{
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 0,
@@ -275,8 +275,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
}}
>
<IconButton
type="secondaryAccented"
style={{
type="grayAccent"
customStyle={{
width: 40,
height: 40,
marginRight: 10
@@ -292,7 +292,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
>
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
</View>
</Pressable>
</PressableButton>
))}
</>
)}

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import { useRoute } from "@react-navigation/native";
import React, { useCallback, useEffect } from "react";
import { Keyboard, View } from "react-native";
import Animated, {
@@ -35,7 +34,7 @@ import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation";
import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
interface FloatingButtonProps {
title: string;
@@ -55,7 +54,6 @@ const FloatingButton = ({
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const translate = useSharedValue(0);
const route = useRoute();
const animatedStyle = useAnimatedStyle(() => {
return {
@@ -118,11 +116,11 @@ const FloatingButton = ({
animatedStyle
]}
>
<Pressable
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={color}
style={{
customStyle={{
...getElevationStyle(5),
borderRadius: 100
}}
@@ -142,18 +140,12 @@ const FloatingButton = ({
}}
>
<Icon
name={
route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
name={title === "Clear all trash" ? "delete" : "plus"}
color={colors.primary.accentForeground}
size={SIZE.xxl}
/>
</View>
</Pressable>
</PressableButton>
</Animated.View>
);
};

View File

@@ -17,49 +17,28 @@ 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 React, { PropsWithChildren, useEffect, useRef, useState } from "react";
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
import React, { PropsWithChildren } from "react";
import { KeyboardAvoidingView, Platform } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useKeyboard from "../../hooks/use-keyboard";
import { useThemeColors } from "@notesnook/theme";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
export const Container = ({ children }: PropsWithChildren) => {
const { colors } = useThemeColors();
const floating = useIsFloatingKeyboard();
const insets = useGlobalSafeAreaInsets();
const keyboard = useKeyboard();
const [height, setHeight] = useState(0);
const windowHeightRef = useRef(Dimensions.get("window").height);
const { height: windowHeight } = useWindowDimensions();
const timerRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (windowHeight !== windowHeightRef.current) {
setHeight(0);
windowHeightRef.current = windowHeight;
}
}, [windowHeight]);
return (
<View
<KeyboardAvoidingView
behavior="padding"
enabled={Platform.OS === "ios" && !floating}
style={{
flex: 1,
overflow: "hidden",
paddingTop: Platform.OS === "android" ? 0 : insets.top,
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom,
height: height || "100%",
width: "100%",
backgroundColor: colors.primary.background
}}
onLayout={(event) => {
const height = event.nativeEvent.layout.height;
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
if (!keyboard.keyboardShown) {
setHeight(height);
}
}, 500);
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
}}
>
{children}
</View>
</KeyboardAvoidingView>
);
};

View File

@@ -44,7 +44,7 @@ export default function DelayLayout({
}: IDelayLayoutProps) {
const { colors } = useThemeColors();
const loading = useDelayLayout(
!props.delay || props.delay < 300 ? 0 : props.delay
!props.delay || props.delay < 300 ? 300 : props.delay
);
const Placeholder = placeholder[props.type || "default"];

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { useNoteStore } from "../../stores/use-notes-store";
import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
@@ -36,14 +37,13 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import { AppLockPassword } from "../dialogs/applock-password";
const DialogProvider = () => {
const { colors } = useThemeColors();
const loading = useNoteStore((state) => state.loading);
return (
<>
<AppLockPassword />
<LoadingDialog />
<Dialog context="global" />
<PremiumDialog colors={colors} />
@@ -57,7 +57,7 @@ const DialogProvider = () => {
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<Expiring />
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />

View File

@@ -86,9 +86,7 @@ const BaseDialog = ({
}, []);
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.canLockAppInBackground()) {
if (SettingsService.get().appLockMode === "background") {
if (appState === "background") {
setIntervalVisible(false);
if (useUserStore.getState().appLocked) {

View File

@@ -25,7 +25,6 @@ import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { getColorLinearShade } from "../../utils/colors";
const DialogButtons = ({
onPressPositive,
@@ -36,7 +35,7 @@ const DialogButtons = ({
doneText,
positiveType
}) => {
const { colors, isDark } = useThemeColors();
const { colors } = useThemeColors();
return (
<View
@@ -45,15 +44,9 @@ const DialogButtons = ({
{
backgroundColor: colors.secondary.background,
height: 60,
borderBottomRightRadius: 5,
borderBottomLeftRadius: 5,
paddingHorizontal: 12,
borderTopWidth: 0.7,
borderTopColor: getColorLinearShade(
colors.secondary.background,
0.05,
isDark
)
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10,
paddingHorizontal: 12
}
]}
>
@@ -87,7 +80,7 @@ const DialogButtons = ({
onPress={onPressNegative}
fontSize={SIZE.md}
testID={notesnook.ids.default.dialog.no}
type="plain"
type="gray"
bold
title={negativeTitle}
/>

View File

@@ -18,21 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { View, ViewProps } from "react-native";
import { View } from "react-native";
import { DDS } from "../../services/device-detection";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../utils/elevation";
import { getContainerBorder } from "../../utils/colors";
const DialogContainer = ({
width,
height,
style,
...restProps
}: ViewProps & {
width?: any;
height?: any;
}) => {
const DialogContainer = ({ width, height, ...restProps }) => {
const { colors } = useThemeColors();
return (
@@ -44,8 +35,7 @@ const DialogContainer = ({
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12,
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
paddingTop: 12
}}
/>
);

View File

@@ -22,7 +22,7 @@ import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { PressableProps } from "../ui/pressable";
import { PressableButtonProps } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -34,7 +34,7 @@ type DialogHeaderProps = {
onPress?: () => void;
loading?: boolean;
title?: string;
type?: PressableProps["type"];
type?: PressableButtonProps["type"];
icon?: string;
};
paragraphColor?: string;
@@ -86,9 +86,7 @@ const DialogHeader = ({
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
<Text style={{ color: colors.primary.accent }}>{titlePart}</Text>
) : null}
</Heading>
@@ -103,8 +101,8 @@ const DialogHeader = ({
fontSize={13}
title={button.title}
icon={button.icon}
type={button.type || "secondary"}
height={30}
type={button.type || "grayBg"}
height={25}
/>
) : null}
</View>

View File

@@ -29,8 +29,8 @@ type DialogInfo = {
onClose?: () => void;
positiveType?:
| "transparent"
| "plain"
| "secondary"
| "gray"
| "grayBg"
| "accent"
| "inverted"
| "shade"
@@ -41,9 +41,7 @@ type DialogInfo = {
input: boolean;
inputPlaceholder: string;
defaultValue: string;
// eslint-disable-next-line @typescript-eslint/ban-types
context: "global" | "local" | (string & {});
secureTextEntry?: boolean;
context: "global" | "local";
};
export function presentDialog(data: Partial<DialogInfo>): void {

View File

@@ -36,15 +36,11 @@ import DialogButtons from "./dialog-buttons";
import DialogHeader from "./dialog-header";
import { useCallback } from "react";
import { Button } from "../ui/button";
import { getContainerBorder } from "../../utils/colors";
export const Dialog = ({ context = "global" }) => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [checked, setChecked] = useState(false);
const values = useRef({
inputValue: undefined
});
const [inputValue, setInputValue] = useState(null);
const inputRef = useRef();
const [dialogInfo, setDialogInfo] = useState({
title: "",
@@ -66,12 +62,21 @@ export const Dialog = ({ context = "global" }) => {
}
});
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [show]);
const onPressPositive = async () => {
if (dialogInfo.positivePress) {
inputRef.current?.blur();
let result = await dialogInfo.positivePress(
values.current.inputValue || dialogInfo.defaultValue,
checked
inputValue || dialogInfo.defaultValue
);
if (result === false) {
return;
@@ -86,26 +91,14 @@ export const Dialog = ({ context = "global" }) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
setDialogInfo(data);
setChecked(false);
values.current.inputValue = data.defaultValue;
setVisible(true);
setInputValue(data.defaultValue);
},
[context]
);
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [show]);
const hide = () => {
setChecked(false);
values.current.inputValue = undefined;
setInputValue(null);
setVisible(false);
};
@@ -113,6 +106,7 @@ export const Dialog = ({ context = "global" }) => {
if (dialogInfo.onClose) {
await dialogInfo.onClose();
}
hide();
};
@@ -122,8 +116,7 @@ export const Dialog = ({ context = "global" }) => {
maxHeight: 450,
borderRadius: 5,
backgroundColor: colors.primary.background,
paddingTop: 12,
...getContainerBorder(colors.primary.border, 0.5)
paddingTop: 12
};
return visible ? (
@@ -166,7 +159,7 @@ export const Dialog = ({ context = "global" }) => {
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.inputValue = value;
setInputValue(value);
}}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
@@ -183,10 +176,10 @@ export const Dialog = ({ context = "global" }) => {
<>
<Button
onPress={() => {
setChecked(!checked);
setInputValue(!inputValue);
}}
icon={
checked
inputValue
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
@@ -194,10 +187,9 @@ export const Dialog = ({ context = "global" }) => {
justifyContent: "flex-start"
}}
height={35}
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type || "selected" : "plain"}
type={inputValue ? dialogInfo.check.type : "gray"}
/>
</>
) : null}

View File

@@ -1,360 +0,0 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import {
clearAppLockVerificationCipher,
setAppLockVerificationCipher,
validateAppLockPassword
} from "../../../common/database/encryption";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseAppLocKPasswordDailog,
eOpenAppLockPasswordDialog
} from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
export const AppLockPassword = () => {
const { colors } = useThemeColors();
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
const [keyboardType, setKeyboardType] = useState<"pin" | "password">(
useSettingStore.getState().settings.applockKeyboardType === "default"
? "password"
: "pin"
);
const [visible, setVisible] = useState(false);
const currentPasswordInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const values = useRef<{
currentPassword?: string;
password?: string;
confirmPassword?: string;
}>({});
const [secureTextEntry, setSecureTextEntry] = useState(true);
useEffect(() => {
const subs = [
eSubscribeEvent(
eOpenAppLockPasswordDialog,
(mode: "create" | "change" | "remove") => {
setMode(mode);
setVisible(true);
}
),
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
values.current = {};
setVisible(false);
})
];
return () => {
subs.forEach((sub) => sub?.unsubscribe());
};
}, []);
const close = () => {
values.current = {};
setVisible(false);
};
return !visible ? null : (
<BaseDialog
onShow={async () => {
await sleep(100);
if (mode !== "change") {
passwordInputRef.current?.focus();
} else {
currentPasswordInputRef.current?.focus();
}
}}
statusBarTranslucent={false}
onRequestClose={close}
visible={visible}
>
<View
style={{
...getElevationStyle(10),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
}}
>
<DialogHeader
title={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
: `Set up app lock ${keyboardType}`
}
paragraph={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
: `Set up a custom app lock ${keyboardType} to unlock the app`
}
icon="shield"
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12
}}
>
{mode === "change" ? (
<Input
fwdRef={currentPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.currentPassword = value;
}}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
defaultValue={values.current.currentPassword}
autoComplete="password"
returnKeyLabel="Next"
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
returnKeyType="next"
secureTextEntry={secureTextEntry}
placeholder={`Current ${keyboardType}`}
/>
) : null}
<Input
fwdRef={passwordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.password = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.password}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
autoComplete="password"
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry={secureTextEntry}
buttonLeft={
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={SIZE.lg}
/>
}
placeholder={
mode === "change"
? `New ${keyboardType}`
: `${keyboardType === "pin" ? "Pin" : "Password"}`
}
/>
{mode !== "remove" ? (
<Input
fwdRef={confirmPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.confirmPassword = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.confirmPassword}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
customValidator={() => values.current.password || ""}
validationType="confirmPassword"
autoComplete="password"
returnKeyLabel="Done"
returnKeyType="done"
secureTextEntry={secureTextEntry}
placeholder={`Confirm ${keyboardType}`}
/>
) : null}
</View>
<DialogButtons
onPressNegative={close}
onPressPositive={async () => {
if (mode === "create") {
if (!values.current.password || !values.current.confirmPassword) {
ToastManager.error(
new Error("All inputs are required"),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
undefined,
"local"
);
return;
}
await setAppLockVerificationCipher(values.current.password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
} else if (mode === "change") {
if (
!values.current.currentPassword ||
!values.current.password ||
!values.current.confirmPassword
) {
ToastManager.error(
new Error("All inputs are required"),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.currentPassword
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
),
undefined,
"local"
);
return;
}
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(values.current.password);
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error("All inputs are required"),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.password
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
),
undefined,
"local"
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(SettingsService.get());
ToastManager.show({
message: "App lock disabled",
type: "success"
});
}
}
SettingsService.setProperty(
"applockKeyboardType",
keyboardType === "password" ? "default" : "numeric"
);
close();
}}
positiveTitle="Save"
negativeTitle="Cancel"
positiveType="transparent"
loading={false}
doneText=""
/>
</View>
<Toast context="local" />
</BaseDialog>
);
};
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
eSendEvent(eOpenAppLockPasswordDialog, mode);
};

View File

@@ -1,196 +0,0 @@
/*
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 React, { useState } from "react";
import {
Image,
Platform,
ScrollView,
TouchableOpacity,
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Notice } from "../../ui/notice";
import Paragraph from "../../ui/typography/paragraph";
export default function AttachImage({
response,
onAttach,
close
}: {
response: ImageType[];
onAttach: ({ compress }: { compress: boolean }) => void;
close: ((ctx?: string | undefined) => void) | undefined;
}) {
const { colors } = useThemeColors();
const [compress, setCompress] = useState(true);
return (
<View
style={{
alignItems: "center",
paddingHorizontal: 12
}}
>
<View
style={{
backgroundColor: colors.secondary.background,
marginBottom: 12,
height: 140,
width: "100%",
borderRadius: 10,
padding: 5,
paddingHorizontal: 12
}}
>
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
Attaching {response?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response?.map?.((item) => (
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
<Image
source={{
uri:
Platform.OS === "ios"
? item.sourceURL || item.path
: item.path
}}
style={{
width: 100,
height: 100,
borderRadius: 5,
backgroundColor: "black",
marginRight: 6
}}
resizeMode="cover"
/>
</TouchableOpacity>
))}
</ScrollView>
</View>
<TouchableOpacity
activeOpacity={1}
style={{
flexDirection: "row",
alignSelf: "center",
marginBottom: 12,
alignItems: "center",
width: "100%"
}}
onPress={() => {
setCompress(!compress);
}}
>
<IconButton
size={SIZE.lg}
name={compress ? "checkbox-marked" : "checkbox-blank-outline"}
color={compress ? colors.primary.accent : colors.primary.icon}
style={{
width: 25,
height: 25
}}
onPress={() => {
setCompress(!compress);
}}
/>
<Paragraph
style={{
flexShrink: 1,
marginLeft: 3
}}
size={SIZE.sm}
>
Compress (recommended)
</Paragraph>
</TouchableOpacity>
{!compress ? (
<Notice
type="alert"
text="Images uploaded without compression are slow to load and take more bandwidth. We recommend compressing images unless you need image in original quality."
size="small"
style={{
width: "100%",
marginBottom: 12
}}
/>
) : (
<Notice
type="information"
text="Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases."
size="small"
style={{
width: "100%",
marginBottom: 12
}}
/>
)}
<Button
title={`${
(response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`}
type="accent"
width="100%"
onPress={() => {
onAttach({
compress
});
}}
/>
</View>
);
}
AttachImage.present = (response: ImageType[], context?: string) => {
return new Promise<
| {
compress: boolean;
}
| undefined
>((resolve) => {
let resolved = false;
presentSheet({
context: context,
component: (ref, close, update) => (
<AttachImage
response={response}
close={close}
onAttach={(result) => {
resolved = true;
console.log("closing");
resolve(result);
close?.();
}}
/>
),
onClose: () => {
if (resolved) return;
resolve(undefined);
}
});
});
};

View File

@@ -1,199 +0,0 @@
/*
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 { Color } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import ColorPickerWheel from "react-native-wheel-color-picker";
import { db } from "../../../common/database";
import { useStoredRef } from "../../../hooks/use-stored-ref";
import { ToastManager } from "../../../services/event-manager";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
const HEX_COLOR_REGEX_ALPHA =
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
const ColorPicker = ({
visible,
setVisible,
onColorAdded
}: {
visible: boolean;
setVisible: (value: boolean) => void;
onColorAdded: (color: Color) => void;
}) => {
const [selectedColor, setSelectedColor] = useState<string>();
const { colors } = useThemeColors();
const inputRef = useRef<TextInput>(null);
const title = useRef<string>();
const colorRef = useStoredRef("color-ref", "#f0f0f0");
return (
<BaseDialog
visible={visible}
onRequestClose={() => {
title.current = undefined;
setVisible(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}}
statusBarTranslucent={false}
centered
>
<Toast context="color-picker" />
<DialogContainer
style={{
paddingTop: 0,
maxHeight: 600
}}
>
<View>
<View
style={{
width: "100%",
height: 250,
backgroundColor: colors.primary.background,
borderRadius: 10
}}
>
<ColorPickerWheel
onColorChangeComplete={(color) => {
if (color === selectedColor) return;
colorRef.current = color;
setSelectedColor(color);
inputRef.current?.setNativeProps({
placeholder: color,
text: color
});
}}
useNativeLayout={true}
autoResetSlider={true}
color={colorRef.current}
sliderSize={30}
thumbSize={25}
shadeWheelThumb
shadeSliderThumb={true}
swatchesOnly={false}
swatches={false}
useNativeDriver={true}
/>
</View>
<View
style={{
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingTop: 10,
columnGap: 10,
marginBottom: 10
}}
>
<Input
fwdRef={inputRef}
placeholder="#f0f0f0"
marginBottom={0}
onChangeText={(value) => {
if (HEX_COLOR_REGEX_ALPHA.test(value)) {
colorRef.current = value;
setSelectedColor(value);
}
}}
defaultValue={colorRef.current}
/>
<Pressable
type="accent"
accentColor={selectedColor || colors.secondary.background}
style={{
width: 45,
height: 45,
borderRadius: 100,
justifyContent: "center",
alignItems: "center"
}}
/>
</View>
<Input
marginBottom={10}
onChangeText={(value) => {
title.current = value;
}}
testID="color-title-input"
defaultValue={title.current}
placeholder={title.current || "Color title"}
/>
<Button
title="Add color"
style={{
marginBottom: 10
}}
onPress={async () => {
if (!selectedColor)
return ToastManager.error(
new Error("Select a color"),
undefined,
"color-picker"
);
if (!title.current)
return ToastManager.error(
new Error("Enter a title for the color")
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(`Color #${selectedColor} already exists`)
);
const id = await db.colors.add({
title: title.current,
colorCode: selectedColor
});
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);
const color = await db.colors.color(id);
if (color) {
onColorAdded?.(color);
}
}}
type={selectedColor ? "secondaryAccented" : "secondary"}
width="100%"
/>
</View>
</View>
</DialogContainer>
</BaseDialog>
);
};
export default ColorPicker;

View File

@@ -26,7 +26,7 @@ import React, {
useRef,
useState
} from "react";
import { ActivityIndicator, FlatList, ScrollView, View } from "react-native";
import { FlatList, ScrollView, View } from "react-native";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
@@ -41,7 +41,7 @@ import {
} from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import { Pressable } from "../../ui/pressable";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
const JumpToSectionDialog = () => {
@@ -52,8 +52,6 @@ const JumpToSectionDialog = () => {
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const currentScrollPosition = useRef(0);
const [loading, setLoading] = useState(false);
const [groups, setGroups] = useState<
{
index: number;
@@ -78,7 +76,6 @@ const JumpToSectionDialog = () => {
data: VirtualizedGrouping<Item>;
ref: RefObject<FlatList>;
}) => {
setLoading(true);
setData(data);
scrollRef.current = ref;
setVisible(true);
@@ -130,7 +127,6 @@ const JumpToSectionDialog = () => {
});
setCurrentIndex(index < 0 ? 0 : index);
setLoading(false);
});
}, [notes]);
@@ -156,64 +152,54 @@ const JumpToSectionDialog = () => {
paddingTop: 30
}}
>
{loading ? (
<ActivityIndicator
size={SIZE.lg}
color={colors.primary.accent}
<ScrollView
style={{
maxHeight: "100%"
}}
>
<View
style={{
marginBottom: 20
}}
/>
) : (
<ScrollView
style={{
maxHeight: "100%"
flexDirection: "row",
flexWrap: "wrap",
alignSelf: "center",
justifyContent: "center",
paddingBottom: 20
}}
>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
alignSelf: "center",
justifyContent: "center",
paddingBottom: 20
}}
>
{groups?.map((item, index) => {
return (
<Pressable
key={item.group.id}
onPress={() => onPress(item)}
type={currentIndex === index ? "accent" : "transparent"}
{groups?.map((item, index) => {
return (
<PressableButton
key={item.group.id}
onPress={() => onPress(item)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 25,
marginVertical: 10
}}
>
<Paragraph
size={SIZE.sm}
color={
currentIndex === index
? colors.selected.accent
: colors.primary.accent
}
style={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 30,
marginVertical: 10
textAlign: "center"
}}
>
<Paragraph
size={SIZE.sm}
color={
currentIndex === index
? colors.static.white
: colors.primary.paragraph
}
style={{
textAlign: "center"
}}
>
{item.group.title}
</Paragraph>
</Pressable>
);
})}
</View>
</ScrollView>
)}
{item.group.title}
</Paragraph>
</PressableButton>
);
})}
</View>
</ScrollView>
</View>
</BaseDialog>
);

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 React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import {
addOrientationListener,
@@ -85,7 +85,7 @@ const PDFPreview = () => {
return () => {
eUnSubscribeEvent("PDFPreview", open);
};
}, [open]);
}, []);
const onOrientationChange = (o) => {
if (o.includes("LANDSCAPE")) {
@@ -102,26 +102,23 @@ const PDFPreview = () => {
};
}, []);
const open = useCallback(
async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
},
[snapshot]
);
const open = async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.metadata.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
setPDFSource(null);
@@ -133,7 +130,7 @@ const PDFPreview = () => {
if (error?.message === "Password required or incorrect password.") {
await sleep(300);
presentDialog({
context: attachment?.hash,
context: attachment?.metadata?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
@@ -154,8 +151,8 @@ const PDFPreview = () => {
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.hash} />
<Dialog context={attachment?.hash} />
<SheetProvider context={attachment?.metadata?.hash} />
<Dialog context={attachment?.metadata?.hash} />
<View
style={{
@@ -211,7 +208,7 @@ const PDFPreview = () => {
color={colors.static.white}
name="arrow-left"
onPress={close}
style={{
customStyle={{
marginRight: 12
}}
size={SIZE.xxl}
@@ -264,7 +261,7 @@ const PDFPreview = () => {
color={colors.static.white}
name="download"
onPress={() => {
downloadAttachment(attachment.hash, false);
downloadAttachment(attachment.metadata.hash, false);
}}
/>
</View>

View File

@@ -23,6 +23,7 @@ import { InteractionManager, View } from "react-native";
import Share from "react-native-share";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { editorController } from "../../../screens/editor/tiptap/utils";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
@@ -34,11 +35,11 @@ import {
import Navigation from "../../../services/navigation";
import { getElevationStyle } from "../../../utils/elevation";
import {
eClearEditor,
eCloseActionSheet,
eCloseVaultDialog,
eOnLoadNote,
eOpenVaultDialog,
eUpdateNoteInEditor
eOpenVaultDialog
} from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { tabBarRef } from "../../../utils/global-refs";
@@ -80,8 +81,7 @@ export class VaultDialog extends Component {
description: null,
clearVault: false,
deleteVault: false,
deleteAll: false,
noteLocked: false
deleteAll: false
};
this.passInputRef = createRef();
@@ -99,7 +99,7 @@ export class VaultDialog extends Component {
? "Revoke Vault Fingerprint Unlock"
: this.state.changePassword
? "Change Vault Password"
: this.state.noteLocked
: this.state.note.locked
? this.state.deleteNote
? "Delete note"
: this.state.share
@@ -120,7 +120,7 @@ export class VaultDialog extends Component {
? "Setup a new password for the vault."
: this.state.permanant
? "Enter password to remove note from vault."
: this.state.noteLocked
: this.state.note.locked
? this.state.deleteNote
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.share
@@ -128,7 +128,7 @@ export class VaultDialog extends Component {
: this.state.copyNote
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.goToEditor
? "Unlock note to open it in editor."
? "Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault."
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
}
@@ -174,8 +174,7 @@ export class VaultDialog extends Component {
title: data.title,
description: data.description,
clearVault: data.clearVault,
deleteVault: data.deleteVault,
noteLocked: data.item && (await db.vaults.itemExists(data.item))
deleteVault: data.deleteVault
});
if (
@@ -241,6 +240,16 @@ export class VaultDialog extends Component {
});
return;
}
if (this.password && this.password.length < 3) {
ToastManager.show({
heading: "Password too short",
message: "Password must be longer than 3 characters.",
type: "error",
context: "local"
});
return;
}
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
@@ -305,7 +314,7 @@ export class VaultDialog extends Component {
});
return;
}
if (this.state.noteLocked) {
if (this.state.note.locked) {
await this._unlockNote();
} else {
db.vault
@@ -337,26 +346,7 @@ export class VaultDialog extends Component {
let verified = await db.user.verifyPassword(this.password);
if (!(await db.user.getUser())) verified = true;
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault, "note").get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(this.state.deleteAll);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
eSendEvent("vaultUpdated");
this.setState({
loading: false
@@ -383,22 +373,7 @@ export class VaultDialog extends Component {
loading: true
});
try {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault, "note").get();
const noteIds = relations.map((item) => item.toId);
await db.vault.clear(this.password);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
this.setState({
loading: false
});
@@ -427,10 +402,9 @@ export class VaultDialog extends Component {
return;
} else {
await db.vault.add(this.state.note.id);
console.log("update note event...");
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
this.close();
ToastManager.show({
message: "Note locked successfully",
@@ -535,7 +509,9 @@ export class VaultDialog extends Component {
}
if (this.state.note?.id) {
await db.vault.add(this.state.note.id);
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
this.setState({
loading: false
});
@@ -565,7 +541,6 @@ export class VaultDialog extends Component {
type: "success",
context: "global"
});
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
})
.catch((e) => {
@@ -891,10 +866,10 @@ export class VaultDialog extends Component {
title="Biometric unlocking"
iconColor={
this.state.biometricUnlock
? colors.selected.accent
? colors.selected.icon
: colors.primary.icon
}
type={this.state.biometricUnlock ? "transparent" : "plain"}
type={this.state.biometricUnlock ? "transparent" : "selected"}
/>
) : null}
</View>
@@ -917,7 +892,7 @@ export class VaultDialog extends Component {
? "Revoke"
: changePassword
? "Change"
: this.state.noteLocked
: note.locked
? deleteNote
? "Delete"
: share

View File

@@ -33,7 +33,6 @@ import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
import { useNavigation } from "@react-navigation/native";
type HeaderRightButton = {
title: string;
@@ -67,8 +66,6 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const navigation = useNavigation();
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true);

View File

@@ -54,7 +54,7 @@ export const LeftMenus = ({
return isTablet && !canGoBack ? null : (
<IconButton
testID={notesnook.ids.default.header.buttons.left}
style={{
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,

View File

@@ -23,6 +23,8 @@ import { Platform, StyleSheet, View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import Menu from "react-native-reanimated-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import {
HeaderRightButton,
RouteName
@@ -61,7 +63,7 @@ export const RightMenus = ({
testID="icon-search"
name="magnify"
color={colors.primary.paragraph}
style={styles.rightBtn}
customStyle={styles.rightBtn}
/>
) : null}
@@ -71,7 +73,7 @@ export const RightMenus = ({
testID={notesnook.ids.default.addBtn}
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
iconSize={SIZE.xl}
type="accent"
type="shade"
hitSlop={{
top: 10,
right: 10,
@@ -82,7 +84,7 @@ export const RightMenus = ({
marginLeft: 10,
width: 32,
height: 32,
borderRadius: 100,
borderRadius: 5,
paddingHorizontal: 0,
borderWidth: 1,
borderColor: colors.primary.accent
@@ -108,7 +110,7 @@ export const RightMenus = ({
}}
name="dots-vertical"
color={colors.primary.paragraph}
style={styles.rightBtn}
customStyle={styles.rightBtn}
/>
}
>
@@ -119,7 +121,7 @@ export const RightMenus = ({
justifyContent: "flex-start",
borderRadius: 0
}}
type="plain"
type="gray"
buttonType={{
text: contextMenuColors.primary.paragraph
}}

View File

@@ -49,8 +49,6 @@ export const Title = ({
const isTag = title.startsWith("#");
const onScroll = useCallback(
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== "Notebook") return;
if (data.route !== renderedInRoute || data.id !== id) return;
if (data.y > 150) {
if (!visible) return;

View File

@@ -19,12 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium";
import dataurl from "@notesnook/core/dist/utils/dataurl";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { Platform, View } from "react-native";
import ImageViewer from "react-native-image-zoom-viewer";
import { db } from "../../common/database";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { cacheDir } from "../../common/filesystem/utils";
import {
@@ -34,13 +32,13 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
const [visible, setVisible] = useState(false);
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
const imageRef = useRef<ImageAttributes>();
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
@@ -50,12 +48,11 @@ const ImagePreview = () => {
}, []);
const open = async (image: ImageAttributes) => {
imageRef.current = image;
setVisible(true);
setLoading(true);
setTimeout(async () => {
let hash = image.hash;
if (!hash && image.src && dataurl.toObject(image.src)) {
if (!hash && dataurl.toObject(image.src)) {
const data = dataurl.toObject(image.src);
if (!data) return;
hash = await Sodium.hashFile({
@@ -63,9 +60,6 @@ const ImagePreview = () => {
type: "base64",
uri: ""
});
if (imageRef.current) {
imageRef.current.hash = hash;
}
}
if (!hash) return;
//@ts-ignore // FIX ME
@@ -86,13 +80,7 @@ const ImagePreview = () => {
return (
visible && (
<BaseDialog
background="black"
animation="slide"
visible={true}
onRequestClose={close}
transparent
>
<BaseDialog animation="slide" visible={true} onRequestClose={close}>
<View
style={{
width: "100%",
@@ -113,21 +101,6 @@ const ImagePreview = () => {
color={colors.primary.accent}
borderColor="transparent"
/>
<IconButton
onPress={() => {
if (imageRef.current?.hash) {
db.fs().cancel(imageRef.current?.hash);
}
close();
}}
style={{
position: "absolute",
top: Platform.OS === "android" ? 35 : 0,
right: 12
}}
color={colors.static.white}
name="close"
/>
</View>
) : (
<ImageViewer
@@ -144,12 +117,13 @@ const ImagePreview = () => {
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 50,
paddingHorizontal: 24,
height: 80,
marginTop: 0,
paddingHorizontal: 12,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
marginTop: Platform.OS === "android" ? 30 : 0
paddingTop: Platform.OS === "android" ? 30 : 0
}}
>
<IconButton
@@ -163,7 +137,7 @@ const ImagePreview = () => {
)}
imageUrls={[
{
url: image as string
url: image
}
]}
/>

View File

@@ -36,10 +36,6 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
import { eOpenLoginDialog } from "../../utils/events";
import { AuthMode } from "../auth";
import { eSendEvent } from "../../services/event-manager";
const Intro = ({ navigation }) => {
const { colors } = useThemeColors();
@@ -197,15 +193,9 @@ const Intro = ({ navigation }) => {
<Button
width={250}
onPress={async () => {
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
setTimeout(() => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
}, 1000);
navigation.navigate("AppLock", {
welcome: true
});
}}
style={{
paddingHorizontal: 24,

View File

@@ -108,7 +108,7 @@ export const WelcomeNotice = () => {
eSendEvent(eCloseLoading);
}}
iconSize={20}
type="plain"
type="gray"
iconPosition="right"
icon="chevron-right"
height={25}

View File

@@ -19,6 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { View } from "react-native";
import React from "react";
export const Footer = ({ height = 150 }) => {
return <View style={{ height: height }} />;
export const Footer = () => {
return <View style={{ height: 150 }} />;
};

View File

@@ -95,7 +95,7 @@ export const NotebookHeader = ({
style={{
flexShrink: 1
}}
size={SIZE.lg}
size={SIZE.xxl}
>
{notebook.title}
</Heading>
@@ -109,12 +109,12 @@ export const NotebookHeader = ({
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
onPress={onPinNotebook}
tooltipText={"Create shortcut in side menu"}
style={{
customStyle={{
marginRight: 15,
width: 40,
height: 40
}}
type="transparent"
type={isPinnedToMenu ? "grayBg" : "grayBg"}
color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg}
/>
@@ -123,9 +123,9 @@ export const NotebookHeader = ({
onPress={onEditNotebook}
tooltipText="Edit this notebook"
name="pencil"
type="transparent"
type="grayBg"
color={colors.primary.icon}
style={{
customStyle={{
width: 40,
height: 40
}}
@@ -134,7 +134,7 @@ export const NotebookHeader = ({
</View>
{notebook.description ? (
<Paragraph size={SIZE.sm} color={colors.primary.paragraph}>
<Paragraph size={SIZE.md} color={colors.primary.paragraph}>
{notebook.description}
</Paragraph>
) : null}
@@ -148,6 +148,10 @@ export const NotebookHeader = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{/* {notebook.topics.length === 1
? "1 topic"
: `${notebook.topics.length} topics`}
,{" "} */}
{notebook && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1

Some files were not shown because too many files have changed in this diff Show More