Compare commits

..

85 Commits

Author SHA1 Message Date
Ammar Ahmed
e4f2786e55 mobile: stop bg fetch events when toggled off 2023-12-05 08:47:17 +05:00
Ammar Ahmed
c6612a8e5f mobile: enable experimental background sync 2023-12-04 15:40:40 +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
611 changed files with 20102 additions and 205794 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,126 +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 signed aab to Github
uses: actions/upload-artifact@v2
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}

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,18 +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:
@@ -147,9 +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 signed aab to Github
uses: actions/upload-artifact@v2
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}

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
@@ -123,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
@@ -154,7 +137,7 @@ 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
@@ -180,7 +163,6 @@ jobs:
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
steps:
@@ -199,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
@@ -239,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 AzureCodeSigning
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -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
@@ -275,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,104 +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@v2
with:
name: Notesnook.ipa
path: Notesnook.ipa

View File

@@ -1,11 +1,10 @@
name: Publish @notesnook/web Beta
on:
on:
workflow_dispatch:
push:
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
@@ -23,7 +22,6 @@ on:
- synchronize
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"

View File

@@ -1,32 +0,0 @@
name: Publish @notesnook/web v3 Beta
on:
workflow_dispatch:
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: Build
run: npm run build:web
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-v3-beta ./apps/web/build/

3
.gitignore vendored
View File

@@ -9,5 +9,4 @@ nx-cloud.env
.env.local
.nyc_output
site
node_modules.backup
.nx/cache
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

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.6-beta",
"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.4.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.4",
"icojs": "^0.17.1",
"sodium-native": "^4.0.6",
"typed-emitter": "^2.1.0",
"yargs": "^17.6.2",
"zod": "^3.21.4"
@@ -27,11 +25,12 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "^28.2.1",
"electron-builder": "^24.9.1",
"esbuild": "^0.20.0",
"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.6.1"
"undici": "^5.23.0"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -41,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"
},
@@ -55,8 +54,8 @@
"productName": "Notesnook",
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
"artifactName": "notesnook_${os}_${arch}.${ext}",
"npmRebuild": false,
"asar": false,
"generateUpdatesFilesForAllChannels": true,
"files": [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
@@ -65,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",
@@ -103,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": [
@@ -145,10 +135,6 @@
]
}
],
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {
@@ -219,8 +205,7 @@
{
"provider": "github",
"repo": "notesnook",
"owner": "streetwriters",
"channel": "beta"
"owner": "streetwriters"
}
]
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import fs, { readFile, writeFile } from "fs/promises";
import fs from "fs/promises";
import { existsSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
@@ -28,7 +28,19 @@ import { fileURLToPath } from "url";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
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 });
@@ -37,15 +49,6 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(`cd ${webAppPath} && npm run build:desktop`);
}
// temporary until there's support for prebuilt binaries for windows ARM & linux ARM
await patchBetterSQLite3();
if (os.platform() === "win32")
await exec(
"npx prebuildify --arch=arm64 --strip",
path.join(__dirname, "..", "node_modules", "sodium-native")
);
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
@@ -59,6 +62,42 @@ if (args.variant === "mas") {
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(`npx electron-builder --dir --x64`);
if (process.platform === "win32") {
@@ -70,28 +109,9 @@ if (args.run) {
}
}
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.4.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,8 +58,6 @@ process.on("SIGINT", async (s) => {
async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npx electron-rebuild");
}
await exec(`npm run bundle`);
@@ -64,6 +68,13 @@ async function onChange(first) {
return;
}
if (first) {
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
recursive: true,
force: true
});
}
if (first) {
await spawnAndWaitUntil(
["npx", "nx", "start:desktop", "@notesnook/web"],
@@ -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-AzureCodeSigning -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,7 +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";
const t = initTRPC.create();
@@ -32,8 +31,7 @@ export const router = t.router({
integration: osIntegrationRouter,
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter,
safeStorage: safeStorageRouter
bridge: bridgeRouter
});
export const api = router.createCaller({});

View File

@@ -31,7 +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";
const t = initTRPC.create();
@@ -127,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 }) => {

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

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

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

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

@@ -18,13 +18,13 @@ 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 "react-native-gesture-handler";
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";
@@ -32,37 +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);
// How app lock works
// 1. User goes to settings and setup app lock with a Pin/Password.
// 2. The Pin/Password is used to encrypt a random value or user's encryption key.
// 3. The encrypted value is stored in MMKV
// 4. When the app launches, the same value is decrypted with user provided key, if it works, we launch the app otherwise it remains locked.
// 5. If Biometrics are enabled, the app lock pin/password is stored in keychain. the value can be accessed if fingerprint auth works ONLY.
// 6. User can manually enter the pin if biometrics fails.
// 7. There is no way to enter the app if user forgets the PIN. The only way is to reset app data and start fresh again.
// How to handle app lock for existing users...
// 1.
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();

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,232 +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(
{
password: appLockPassword
},
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);
}
}
@@ -303,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);
}
}
@@ -317,9 +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);
}
@@ -332,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

@@ -34,8 +34,7 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
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
*/
@@ -89,8 +88,8 @@ export async function downloadAttachments(
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);
@@ -114,16 +113,15 @@ 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
});
@@ -183,9 +181,7 @@ export default async function downloadAttachment(
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
groupId: undefined
}
) {
await createCacheDir();
@@ -209,41 +205,37 @@ export default async function downloadAttachment(
try {
console.log(
"starting download attachment",
attachment.hash,
attachment.metadata.hash,
options.groupId
);
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
return;
}
if (options.base64 || options.text) {
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(
@@ -262,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) {
@@ -281,7 +273,7 @@ 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} />
});
}
@@ -291,13 +283,10 @@ export default async function downloadAttachment(
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

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

View File

@@ -17,12 +17,13 @@ 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 { 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();
@@ -34,7 +35,9 @@ export async function readEncrypted(filename, key, cipherData) {
return false;
}
const attachment = await db.attachments.attachment(filename);
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,
@@ -43,9 +46,20 @@ 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"
);
console.log("file decrypted...", attachment?.mimeType);
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) {
@@ -176,10 +190,10 @@ 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

View File

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

@@ -103,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}
@@ -149,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

@@ -20,22 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useRef } from "react";
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,65 +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 { 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 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;
@@ -120,30 +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
? 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;
@@ -159,128 +91,115 @@ const AppLockedOverlay = () => {
useUserStore.getState().disableAppLockRequests
)
return;
if (
appLocked &&
appState === "active" &&
!useSettingStore.getState().requestBiometrics
) {
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}
@@ -288,12 +207,12 @@ const AppLockedOverlay = () => {
icon={"fingerprint"}
type="transparent"
/>
) : null}
</View>
</View>
</View>
</View>
</View>
) : null;
)
);
};
export default AppLockedOverlay;

View File

@@ -50,19 +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";
const Actions = ({
attachment,
close,
setAttachments,
fwdRef
}: {
attachment: Attachment;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close: () => void;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
@@ -80,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);
@@ -114,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);
@@ -134,7 +130,6 @@ const Actions = ({
});
}
setAttachments();
setLoading({
name: undefined
});
@@ -157,7 +152,6 @@ const Actions = ({
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
}
},
@@ -170,9 +164,8 @@ const Actions = ({
name: "Delete",
onPress: async () => {
await db.attachments.remove(attachment.hash, false);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
close();
eSendEvent(eCloseSheet, contextId);
},
icon: "delete-outline"
}
@@ -291,7 +284,7 @@ const Actions = ({
</Heading>
{notes.map((item) => (
<Pressable
<PressableButton
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
await sleep(150);
@@ -299,7 +292,7 @@ const Actions = ({
await sleep(300);
openNote(item, (item as any).type === "trash");
}}
style={{
customStyle={{
paddingVertical: 12,
alignItems: "flex-start",
@@ -308,7 +301,7 @@ const Actions = ({
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
</Pressable>
</PressableButton>
))}
</>
</View>
@@ -319,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
}}
@@ -327,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,
@@ -357,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

@@ -18,22 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useDBItem } from "../../hooks/use-db-item";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
@@ -46,7 +46,7 @@ export const AttachmentItem = ({
hideWhenNotDownloading,
context
}: {
id: string | number;
id: string;
attachments?: VirtualizedGrouping<Attachment>;
encryption?: boolean;
setAttachments: (attachments: any) => void;
@@ -54,7 +54,7 @@ export const AttachmentItem = ({
hideWhenNotDownloading?: boolean;
context?: string;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments);
const [attachment] = useDBItem(id, "attachment", attachments?.item);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
@@ -67,8 +67,9 @@ export const AttachmentItem = ({
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
return (hideWhenNotDownloading &&
(!currentProgress || !(currentProgress as any).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 as any)?.type
? "(" + (currentProgress as any).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,
@@ -89,21 +89,29 @@ const DownloadAttachments = ({
groupId.current = undefined;
};
const successResults = () => {
const results = [];
for (let [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 (let [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"
}`;
@@ -166,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}
@@ -186,7 +192,7 @@ const DownloadAttachments = ({
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.placeholders : undefined}
data={downloading ? attachments.ids : undefined}
ListEmptyComponent={
<View
style={{
@@ -201,11 +207,11 @@ const DownloadAttachments = ({
</Paragraph>
</View>
}
keyExtractor={(index) => "attachment" + index}
renderItem={({ index }) => {
keyExtractor={(item) => item as string}
renderItem={({ item }) => {
return (
<AttachmentItem
id={index}
id={item as string}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
@@ -245,7 +251,7 @@ const DownloadAttachments = ({
onPress={() => {
close?.();
}}
type="secondary"
type="grayBg"
title="No"
/>
<Button

View File

@@ -61,16 +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"
});
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]);
@@ -79,32 +73,28 @@ 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);
searchTimer.current = setTimeout(async () => {
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
if (results.length === 0) return;
let results = await db.lookup.attachments(
attachmentSearchValue.current as string
);
setAttachments(results);
}, 300);
};
const renderItem = ({ index }: { item: boolean; index: number }) => (
const renderItem = ({ item }: { item: string }) => (
<AttachmentItem
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
}}
setAttachments={setAttachments}
attachments={attachments}
id={index}
id={item}
context="attachments-list"
/>
);
@@ -112,15 +102,15 @@ 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 (let id of attachments.ids) {
const attachment = await attachments.item(id as string);
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
let result = await filesystem.checkAttachment(attachment.hash);
if (result.failed) {
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
await db.attachments.markAsFailed(attachment.id, undefined);
await db.attachments.markAsFailed(id as string, undefined);
}
}
refresh();
@@ -176,10 +166,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
: db.attachments.documents;
}
return await items.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
});
return await items.sorted(DEFAULT_SORTING);
};
return (
@@ -218,7 +205,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
) : (
<IconButton
name="check-all"
style={{
customStyle={{
height: 40,
width: 40,
marginRight: 10
@@ -231,7 +218,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
<IconButton
name="download"
style={{
customStyle={{
height: 40,
width: 40
}}
@@ -274,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={{
@@ -321,7 +306,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
/>
}
estimatedItemSize={50}
data={attachments?.placeholders}
data={attachments?.ids as string[]}
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

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

@@ -103,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,
@@ -222,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 "";
@@ -166,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

@@ -34,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;
@@ -116,11 +116,11 @@ const FloatingButton = ({
animatedStyle
]}
>
<Pressable
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={color}
style={{
customStyle={{
...getElevationStyle(5),
borderRadius: 100
}}
@@ -145,7 +145,7 @@ const FloatingButton = ({
size={SIZE.xxl}
/>
</View>
</Pressable>
</PressableButton>
</Animated.View>
);
};

View File

@@ -17,42 +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 useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
export const Container = ({ children }: PropsWithChildren) => {
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();
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%"
}}
onLayout={(event) => {
if (!keyboard.keyboardShown) {
setHeight(event.nativeEvent.layout.height);
}
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
@@ -47,13 +46,7 @@ const DialogButtons = ({
height: 60,
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10,
paddingHorizontal: 12,
borderTopWidth: 0.7,
borderTopColor: getColorLinearShade(
colors.secondary.background,
0.05,
isDark
)
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,7 +101,7 @@ const DialogHeader = ({
fontSize={13}
title={button.title}
icon={button.icon}
type={button.type || "secondary"}
type={button.type || "grayBg"}
height={25}
/>
) : null}

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

@@ -62,6 +62,16 @@ 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();
@@ -87,16 +97,6 @@ export const Dialog = ({ context = "global" }) => {
[context]
);
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [show]);
const hide = () => {
setInputValue(null);
setVisible(false);
@@ -189,7 +189,7 @@ export const Dialog = ({ context = "global" }) => {
height={35}
width="100%"
title={dialogInfo.check.info}
type={inputValue ? dialogInfo.check.type : "plain"}
type={inputValue ? dialogInfo.check.type : "gray"}
/>
</>
) : null}

View File

@@ -1,358 +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 BiometicService 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`
),
"local"
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometicService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
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,181 +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, ScrollView, TouchableOpacity, View } from "react-native";
import { ImagePickerResponse } from "react-native-image-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: ImagePickerResponse;
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.assets?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response.assets?.map((item) => (
<TouchableOpacity key={item.fileName} activeOpacity={0.9}>
<Image
source={{
uri: item.uri
}}
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.assets?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`}
type="accent"
width="100%"
onPress={() => {
onAttach({
compress
});
}}
/>
</View>
);
}
AttachImage.present = (response: ImagePickerResponse) => {
return new Promise((resolve) => {
let resolved = false;
presentSheet({
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,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 { 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: selectedColor,
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;
}}
defaultValue={title.current}
placeholder={title.current || "Color title"}
/>
<Button
title="Add color"
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 = () => {
@@ -51,20 +51,19 @@ const JumpToSectionDialog = () => {
const notes = data;
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;
group: GroupHeader;
}[]
>();
const offsets = useRef<number[]>([]);
const timeout = useRef<NodeJS.Timeout>();
const onPress = (item: { index: number; group: GroupHeader }) => {
const onPress = (item: GroupHeader) => {
const index = notes?.ids?.findIndex((i) => {
if (typeof i === "object") {
return i.title === item.title && i.type === "header";
} else {
false;
}
});
scrollRef.current?.current?.scrollToIndex({
index: item.index,
index: index as number,
animated: true
});
close();
@@ -78,7 +77,6 @@ const JumpToSectionDialog = () => {
data: VirtualizedGrouping<Item>;
ref: RefObject<FlatList>;
}) => {
setLoading(true);
setData(data);
scrollRef.current = ref;
setVisible(true);
@@ -99,45 +97,55 @@ const JumpToSectionDialog = () => {
}, [open]);
const onScroll = (data: { x: number; y: number }) => {
currentScrollPosition.current = data.y;
const y = data.y;
if (timeout) {
clearTimeout(timeout.current);
timeout.current = undefined;
}
timeout.current = setTimeout(() => {
setCurrentIndex(
offsets.current?.findIndex(
(o, i) => o <= y && offsets.current[i + 1] > y
) || 0
);
}, 200);
};
const close = () => {
setVisible(false);
};
const loadGroupsAndOffsets = useCallback(() => {
notes?.groups?.().then((groups) => {
setGroups(groups);
offsets.current = [];
groups.map((item, index) => {
const loadOffsets = useCallback(() => {
notes?.ids
.filter((i) => typeof i === "object" && i.type === "header")
.map((item, index) => {
if (typeof item === "string") return;
let offset = 35 * index;
let groupIndex = item.index;
let ind = notes.ids.findIndex(
(i) =>
typeof i === "object" &&
i.title === item.title &&
i.type === "header"
);
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
groupIndex = groupIndex + 1;
groupIndex = groupIndex - (index + 1);
offset = offset + groupIndex * 100 + msgOffset;
ind = ind + 1;
ind = ind - (index + 1);
offset = offset + ind * 100 + msgOffset;
offsets.current.push(offset);
});
const index = offsets.current?.findIndex((o, i) => {
return (
o <= currentScrollPosition.current + 100 &&
offsets.current[i + 1] - 100 > currentScrollPosition.current
);
});
setCurrentIndex(index < 0 ? 0 : index);
setLoading(false);
});
}, [notes]);
useEffect(() => {
loadOffsets();
}, [loadOffsets, notes]);
return !visible ? null : (
<BaseDialog
onShow={() => {
loadGroupsAndOffsets();
loadOffsets();
}}
onRequestClose={close}
visible={true}
@@ -156,42 +164,35 @@ 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}
{notes?.ids
.filter((i) => typeof i === "object" && i.type === "header")
.map((item, index) => {
return typeof item === "object" && item.title ? (
<PressableButton
key={item.title}
onPress={() => onPress(item)}
type={currentIndex === index ? "accent" : "transparent"}
style={{
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 30,
height: 25,
marginVertical: 10
}}
>
@@ -199,21 +200,20 @@ const JumpToSectionDialog = () => {
size={SIZE.sm}
color={
currentIndex === index
? colors.static.white
: colors.primary.paragraph
? colors.selected.accent
: colors.primary.accent
}
style={{
textAlign: "center"
}}
>
{item.group.title}
{item.title}
</Paragraph>
</Pressable>
);
</PressableButton>
) : null;
})}
</View>
</ScrollView>
)}
</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

@@ -81,8 +81,7 @@ export class VaultDialog extends Component {
description: null,
clearVault: false,
deleteVault: false,
deleteAll: false,
noteLocked: false
deleteAll: false
};
this.passInputRef = createRef();
@@ -100,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
@@ -121,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
@@ -175,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 (
@@ -242,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) {
@@ -306,7 +314,7 @@ export class VaultDialog extends Component {
});
return;
}
if (this.state.noteLocked) {
if (this.state.note.locked) {
await this._unlockNote();
} else {
db.vault
@@ -858,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>
@@ -884,7 +892,7 @@ export class VaultDialog extends Component {
? "Revoke"
: changePassword
? "Change"
: this.state.noteLocked
: note.locked
? deleteNote
? "Delete"
: share

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

@@ -52,7 +52,7 @@ const ImagePreview = () => {
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({

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

@@ -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={isPinnedToMenu ? "secondary" : "secondary"}
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="secondary"
type="grayBg"
color={colors.primary.icon}
style={{
customStyle={{
width: 40,
height: 40
}}

View File

@@ -25,7 +25,7 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab
import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { RouteName } from "../../../stores/use-navigation-store";
import { ColorValues, getContainerBorder } from "../../../utils/colors";
import { ColorValues } from "../../../utils/colors";
import { GROUP } from "../../../utils/constants";
import { SIZE } from "../../../utils/size";
import Sort from "../../sheets/sort";
@@ -80,8 +80,7 @@ export const SectionHeader = React.memo<
backgroundColor: colors.secondary.background,
alignSelf: "center",
borderRadius: 5,
marginVertical: 5,
...getContainerBorder(colors.secondary.background, 0.8)
marginVertical: 5
}}
>
<TouchableOpacity
@@ -146,12 +145,12 @@ export const SectionHeader = React.memo<
? 10
: 0
}}
type="plain"
type="gray"
iconPosition="right"
/>
<IconButton
style={{
customStyle={{
width: 25,
height: 25
}}

View File

@@ -37,7 +37,10 @@ import { useEditorStore } from "../../../stores/use-editor-store";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE } from "../../../utils/size";
import {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -45,7 +48,6 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
@@ -58,7 +60,6 @@ type NoteItemProps = {
date: number;
isTrash?: boolean;
noOpen?: boolean;
locked?: boolean;
};
const NoteItem = ({
@@ -70,18 +71,18 @@ const NoteItem = ({
reminder,
tags,
attachmentsCount,
locked,
noOpen = false
noOpen
}: NoteItemProps) => {
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const _update = useRelationStore((state) => state.updater);
const primaryColors = isEditingNote ? colors.selected : colors.primary;
const primaryColors =
currentEditingNote === item.id ? colors.selected : colors.primary;
return (
<>
@@ -117,8 +118,8 @@ const NoteItem = ({
tooltipText={item.title}
key={item.id}
height={25}
icon="book-outline"
type="secondary"
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
textStyle={{
@@ -127,6 +128,8 @@ const NoteItem = ({
style={{
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingHorizontal: 6,
marginBottom: 5
}}
@@ -153,10 +156,10 @@ const NoteItem = ({
<Paragraph
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={SIZE.sm}
style={{
paddingRight: 10
flexWrap: "wrap"
}}
size={SIZE.sm}
>
{item.title}
</Paragraph>
@@ -164,10 +167,10 @@ const NoteItem = ({
<Heading
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={SIZE.md}
style={{
paddingRight: 10
flexWrap: "wrap"
}}
size={SIZE.md}
>
{item.title}
</Heading>
@@ -254,7 +257,7 @@ const NoteItem = ({
/>
) : null}
{locked ? (
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
@@ -285,7 +288,7 @@ const NoteItem = ({
title={"#" + item.title}
key={item.id}
height={23}
type="plain"
type="gray"
textStyle={{
textDecorationLine: "underline"
}}
@@ -353,7 +356,7 @@ const NoteItem = ({
/>
) : null}
{locked ? (
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
@@ -395,7 +398,7 @@ const NoteItem = ({
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && Properties.present(item)}
style={{
customStyle={{
justifyContent: "center",
height: 35,
width: 35,

View File

@@ -33,10 +33,12 @@ import { useEditorStore } from "../../../stores/use-editor-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import type {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
export const openNote = async (
item: Note,
@@ -49,20 +51,15 @@ export const openNote = async (
if (!isTrash) {
note = (await db.notes.note(item.id)) as Note;
}
if (useSelectionStore.getState().selectionMode === item.type) {
const {
selectedItemsList,
selectionMode,
clearSelection,
setSelectedItem
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(note.id);
} else {
clearSelection();
}
const { selectedItemsList, selectionMode, clearSelection, setSelectedItem } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(note.id);
return;
} else {
clearSelection();
}
if (note.conflicted) {
@@ -70,7 +67,7 @@ export const openNote = async (
return;
}
if (await db.vaults.itemExists(note)) {
if (note.locked) {
openVault({
item: note,
novault: true,
@@ -91,9 +88,7 @@ export const openNote = async (
)
});
} else {
if (note?.readonly) {
useEditorStore.getState().setReadonly(note?.readonly);
}
useEditorStore.getState().setReadonly(note?.readonly);
eSendEvent(eOnLoadNote, {
item: note
});
@@ -113,7 +108,6 @@ type NoteWrapperProps = {
attachmentsCount: number;
date: number;
isRenderedInActionSheet: boolean;
locked?: boolean;
};
export const NoteWrapper = React.memo<
@@ -162,7 +156,6 @@ export const NoteWrapper = React.memo<
if (prev.item?.dateModified !== next.item?.dateModified) {
return false;
}
if (prev.item?.id !== next.item?.id) return false;
return true;
}

View File

@@ -198,7 +198,7 @@ export const NotebookItem = ({
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => Properties.present(item)}
style={{
customStyle={{
justifyContent: "center",
height: 35,
width: 35,

View File

@@ -57,7 +57,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
},
onClose: async () => {
await db.trash.delete(item.id);
useTrashStore.getState().refresh();
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Permanently deleted items",
@@ -103,7 +103,6 @@ export const NotebookWrapper = React.memo(
if (prev.totalNotes !== next.totalNotes) return false;
if (prev.date !== next.date) return false;
if (prev.item?.dateModified !== next.item?.dateModified) return false;
if (prev.item?.id !== next.item?.id) return false;
return true;
}

View File

@@ -63,9 +63,8 @@ const ReminderItem = React.memo(
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
<View
style={{
opacity: item.disabled ? 0.5 : 1,
maxWidth: "80%",
flexGrow: 1
flexShrink: 1,
opacity: item.disabled ? 0.5 : 1
}}
>
<Heading
@@ -176,7 +175,7 @@ const ReminderItem = React.memo(
name="dots-horizontal"
size={SIZE.xl}
onPress={() => Properties.present(item, [], isSheet)}
style={{
customStyle={{
justifyContent: "center",
height: 35,
width: 35,

View File

@@ -26,13 +26,13 @@ import { Item } from "@notesnook/core";
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors();
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
);
const [selected] = useIsSelected(item);
return isEditingNote || selected ? (
return currentEditingNote === item.id || selected ? (
<View
style={{
position: "absolute",
@@ -40,11 +40,12 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
height: "150%",
backgroundColor: colors.selected.background,
borderLeftWidth: 5,
borderLeftColor: isEditingNote
? item.color
? colors.static[item.color]
: colors.selected.accent
: "transparent"
borderLeftColor:
currentEditingNote === item.id
? color
? color
: colors.selected.accent
: "transparent"
}}
collapsable={false}
/>

View File

@@ -21,7 +21,7 @@ import { useThemeColors } from "@notesnook/theme";
import React, { PropsWithChildren, useRef } from "react";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { Pressable } from "../../ui/pressable";
import { PressableButton } from "../../ui/pressable";
import { Filler } from "./back-fill";
import { SelectionIcon } from "./selection";
import { Item, TrashItem } from "@notesnook/core";
@@ -60,7 +60,7 @@ const SelectionWrapper = ({
};
return (
<Pressable
<PressableButton
customColor={isSheet ? colors.primary.hover : "transparent"}
testID={testID}
onLongPress={onLongPress}
@@ -68,7 +68,7 @@ const SelectionWrapper = ({
customSelectedColor={colors.primary.hover}
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={1}
style={{
customStyle={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
@@ -83,7 +83,7 @@ const SelectionWrapper = ({
{item.type === "note" ? <Filler item={item} color={color} /> : null}
<SelectionIcon item={item} />
{children}
</Pressable>
</PressableButton>
);
};

View File

@@ -105,7 +105,7 @@ const TagItem = React.memo(
Properties.present(item);
}}
testID={notesnook.ids.tag.menu}
style={{
customStyle={{
justifyContent: "center",
height: 35,
width: 35,

View File

@@ -22,9 +22,9 @@ import { Dimensions, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useMessageStore } from "../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { getContainerBorder, hexToRGBA } from "../../utils/colors";
import { hexToRGBA } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Card = ({ color }: { color?: string }) => {
@@ -41,10 +41,10 @@ export const Card = ({ color }: { color?: string }) => {
width: "95%"
}}
>
<Pressable
<PressableButton
onPress={messageBoardState.onPress}
type="plain"
style={{
type="gray"
customStyle={{
paddingVertical: 12,
flexDirection: "row",
alignItems: "center",
@@ -64,18 +64,12 @@ export const Card = ({ color }: { color?: string }) => {
width: 40 * fontScale,
backgroundColor:
messageBoardState.type === "error"
? hexToRGBA(colors.error.accent, 0.15)
? hexToRGBA(colors.static.red, 0.15)
: hexToRGBA(color, 0.15),
height: 40 * fontScale,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
...getContainerBorder(
messageBoardState.type === "error"
? colors.error.accent
: color || colors.primary.accent,
0.4
)
justifyContent: "center"
}}
>
<Icon
@@ -128,7 +122,7 @@ export const Card = ({ color }: { color?: string }) => {
/>
</View>
)}
</Pressable>
</PressableButton>
</View>
);
};

View File

@@ -98,13 +98,13 @@ export const Empty = React.memo(function Empty({
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="accent"
type="grayAccent"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
buttonType={{
text: "white"
text: color || colors.primary.accent
}}
style={{
alignSelf: "flex-start",

View File

@@ -17,7 +17,13 @@ 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 { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
import {
GroupHeader,
GroupingKey,
Item,
VirtualizedGrouping,
isGroupHeader
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useRef } from "react";
@@ -33,10 +39,11 @@ import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync";
import { RouteName } from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { eScrollEvent } from "../../utils/events";
import { eOpenJumpToDialog, eScrollEvent } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header";
import { SectionHeader } from "../list-items/headers/section-header";
import { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
@@ -52,7 +59,6 @@ type ListProps = {
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
id?: string;
};
export default function List(props: ListProps) {
@@ -86,20 +92,35 @@ export default function List(props: ListProps) {
};
const renderItem = React.useCallback(
({ index }: { index: number }) => {
return (
<ListItemWrapper
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
scrollRef={scrollRef}
/>
);
({ item, index }: { item: string | GroupHeader; index: number }) => {
if (isGroupHeader(item)) {
return (
<SectionHeader
screen={props.renderedInRoute}
item={item}
index={index}
dataType={props.dataType}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: scrollRef,
data: props.data
});
}}
/>
);
} else {
return (
<ListItemWrapper
id={item}
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
group={groupType as GroupingKey}
/>
);
}
},
[
groupOptions,
@@ -117,18 +138,16 @@ export default function List(props: ListProps) {
if (!event) return;
eSendEvent(eScrollEvent, {
y: event.nativeEvent.contentOffset.y,
route: props.renderedInRoute,
id: props.id || props.renderedInRoute
screen: props.renderedInRoute
});
},
[props.renderedInRoute, props.id]
[props.renderedInRoute]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
route: props.renderedInRoute,
id: props.id || props.renderedInRoute
screen: props.renderedInRoute
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -155,30 +174,26 @@ export default function List(props: ListProps) {
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={props.data?.placeholders || []}
data={props.data?.ids || []}
renderItem={renderItem}
onScroll={onListScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
}}
getItemType={(item: number, index: number) => {
return props.data?.type(index);
}}
estimatedItemSize={isCompactModeEnabled ? 60 : 120}
getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")}
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
refreshControl={
props.isRenderedInActionSheet ? null : (
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
/>
)
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
/>
}
ListEmptyComponent={
props.placeholder ? (

View File

@@ -16,53 +16,42 @@ 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 from "react";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import {
NotebooksWithDateEdited,
TagsWithDateEdited,
isNoteResolvedData,
resolveItems
} from "@notesnook/common";
import {
Color,
GroupHeader,
GroupOptions,
GroupingKey,
Item,
ItemType,
Note,
Notebook,
VirtualizedGrouping,
Color,
Reminder,
Notebook,
Tag,
TrashItem,
VirtualizedGrouping
ItemType,
Note
} from "@notesnook/core";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { db } from "../../common/database";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
export type TagsWithDateEdited = WithDateEdited<Tag>;
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
id: string;
isSheet: boolean;
index: number;
renderedInRoute?: RouteName;
customAccentColor?: string;
dataType: string;
scrollRef: any;
groupOptions: GroupOptions;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { items, group, isSheet, index, groupOptions } = props;
const { id, items, group, isSheet, index } = props;
const [item, setItem] = useState<Item>();
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
@@ -70,212 +59,83 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
const color = useRef<Color>();
const totalNotes = useRef<number>(0);
const attachmentsCount = useRef(0);
const [groupHeader, setGroupHeader] = useState<GroupHeader>();
const previousIndex = useRef<number>();
const refreshTimeout = useRef<NodeJS.Timeout>();
const currentItemId = useRef<string>();
const locked = useRef(false);
const refreshItem = useCallback((resolvedItem: any) => {
if (!resolvedItem || !resolvedItem.data) {
tags.current = undefined;
notebooks.current = undefined;
reminder.current = undefined;
color.current = undefined;
attachmentsCount.current = 0;
totalNotes.current = 0;
locked.current = false;
}
if (resolvedItem && resolvedItem.item) {
const data = resolvedItem.data;
if (resolvedItem.item.type === "note" && isNoteResolvedData(data)) {
useEffect(() => {
(async function () {
const { item, data } = (await items?.item(id, resolveItems)) || {};
if (!item) return;
if (item.type === "note" && isNoteResolvedData(data)) {
tags.current = data.tags;
notebooks.current = data.notebooks;
reminder.current = data.reminder;
color.current = data.color;
attachmentsCount.current = data.attachments?.total || 0;
locked.current = data.locked || false;
} else if (
resolvedItem.item.type === "note" &&
!isNoteResolvedData(data)
) {
tags.current = undefined;
notebooks.current = undefined;
reminder.current = undefined;
color.current = undefined;
attachmentsCount.current = 0;
totalNotes.current = 0;
locked.current = false;
} else if (
resolvedItem.item.type === "notebook" &&
typeof data === "number"
) {
attachmentsCount.current = data.attachmentsCount;
} else if (item.type === "notebook" && typeof data === "number") {
totalNotes.current = data;
} else if (resolvedItem.item.type === "tag" && typeof data === "number") {
} else if (item.type === "tag" && typeof data === "number") {
totalNotes.current = data;
}
currentItemId.current = resolvedItem.item.id;
setItem(resolvedItem.item);
setGroupHeader(resolvedItem.group);
}
}, []);
if (previousIndex.current !== index) {
previousIndex.current = index;
const resolvedItem = items?.cacheItem(index);
refreshItem(resolvedItem);
}
useEffect(() => {
(async function () {
try {
clearTimeout(refreshTimeout.current);
const idx = index;
refreshTimeout.current = setTimeout(async () => {
if (idx !== previousIndex.current) return;
const resolvedItem = await items?.item(idx, resolveItems);
if (idx !== previousIndex.current) return;
refreshItem(resolvedItem);
}, 100);
} catch (e) {
console.log("Error", e);
}
setItem(item);
})();
}, [index, items, refreshItem]);
}, [id, items]);
if (!item)
return (
<View
style={{
height: 120,
width: "100%"
}}
/>
);
if (!item) return <View style={{ height: 100, width: "100%" }} />;
const type = ((item as TrashItem).itemType || item.type) as ItemType;
switch (type) {
case "note": {
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NoteWrapper
item={item as Note}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
locked={locked.current}
/>
</>
<NoteWrapper
item={item as Note}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
/>
);
}
case "notebook":
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
</>
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
);
case "reminder":
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<ReminderItem
item={item as Reminder}
index={index}
isSheet={isSheet}
/>
</>
<ReminderItem item={item as Reminder} index={index} isSheet={isSheet} />
);
case "tag":
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
</>
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
);
default:
return null;
}
}
function withDateEdited<
T extends { dateEdited: number } | { dateModified: number }
>(items: T[]): WithDateEdited<T> {
let latestDateEdited = 0;
items.forEach((item) => {
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
if (latestDateEdited < date) latestDateEdited = date;
});
return { dateEdited: latestDateEdited, items };
}
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
@@ -290,3 +150,120 @@ function getDate(item: Item, groupType?: GroupingKey): number {
) || 0
);
}
async function resolveItems(ids: string[], items: Record<string, Item>) {
const { type } = items[ids[0]];
if (type === "note") return resolveNotes(ids);
else if (type === "notebook") {
const data: Record<string, number> = {};
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
return data;
} else if (type === "tag") {
const data: Record<string, number> = {};
for (const id of ids)
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
return data;
}
return {};
}
type NoteResolvedData = {
notebooks?: NotebooksWithDateEdited;
reminder?: Reminder;
color?: Color;
tags?: TagsWithDateEdited;
attachmentsCount: number;
};
async function resolveNotes(ids: string[]) {
console.time("relations");
const relations = [
...(await db.relations
.to({ type: "note", ids }, ["notebook", "tag", "color"])
.get()),
...(await db.relations.from({ type: "note", ids }, "reminder").get())
];
console.timeEnd("relations");
const relationIds: {
notebooks: Set<string>;
colors: Set<string>;
tags: Set<string>;
reminders: Set<string>;
} = {
colors: new Set(),
notebooks: new Set(),
tags: new Set(),
reminders: new Set()
};
const grouped: Record<
string,
{
notebooks: string[];
color?: string;
tags: string[];
reminder?: string;
}
> = {};
for (const relation of relations) {
const noteId =
relation.toType === "relation" ? relation.fromId : relation.toId;
const data = grouped[noteId] || {
notebooks: [],
tags: []
};
if (relation.toType === "relation" && !data.reminder) {
data.reminder = relation.fromId;
relationIds.reminders.add(relation.fromId);
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
data.notebooks.push(relation.fromId);
relationIds.notebooks.add(relation.fromId);
} else if (relation.fromType === "tag" && data.tags.length < 3) {
data.tags.push(relation.fromId);
relationIds.tags.add(relation.fromId);
} else if (relation.fromType === "color" && !data.color) {
data.color = relation.fromId;
relationIds.colors.add(relation.fromId);
}
grouped[relation.toId] = data;
}
console.time("resolve");
const resolved = {
notebooks: await db.notebooks.all.records(
Array.from(relationIds.notebooks)
),
tags: await db.tags.all.records(Array.from(relationIds.tags)),
colors: await db.colors.all.records(Array.from(relationIds.colors)),
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
};
console.timeEnd("resolve");
const data: Record<string, NoteResolvedData> = {};
for (const noteId in grouped) {
const group = grouped[noteId];
data[noteId] = {
color: group.color ? resolved.colors[group.color] : undefined,
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
notebooks: withDateEdited(
group.notebooks.map((id) => resolved.notebooks[id])
),
attachmentsCount:
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
};
}
return data;
}
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
return (
typeof data === "object" &&
!!data &&
"notebooks" in data &&
"reminder" in data &&
"color" in data &&
"tags" in data
);
}

View File

@@ -1,195 +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 { FlatList, StyleSheet, View } from "react-native";
import {
DraxList,
DraxListProps,
DraxListRenderItemContent,
DraxProvider
} from "react-native-drax";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
import { IconButton } from "../ui/icon-button";
interface ReorderableListProps<T extends { id: string }>
extends Omit<DraxListProps<T>, "renderItem" | "data" | "renderItemContent"> {
onListOrderChanged: (data: string[]) => void;
renderDraggableItem: DraxListRenderItemContent<T>;
data: T[];
itemOrder: string[];
hiddenItems: string[];
onHiddenItemsChanged?: (data: string[]) => void;
canHideItems?: boolean;
disableDefaultDrag?: boolean;
}
function ReorderableList<T extends { id: string }>({
renderDraggableItem,
data,
onListOrderChanged,
hiddenItems = [],
itemOrder = [],
onHiddenItemsChanged,
canHideItems = true,
disableDefaultDrag,
...restProps
}: ReorderableListProps<T>) {
const { colors } = useThemeColors();
const [itemOrderState, setItemsOrder] = useState(itemOrder);
const [hiddenItemsState, setHiddenItems] = useState(hiddenItems);
const dragging = useSideBarDraggingStore((state) => state.dragging);
const listRef = useRef<FlatList | null>(null);
if (dragging) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
useEffect(() => {
setItemsOrder(itemOrder);
setHiddenItems(hiddenItems);
}, [itemOrder, hiddenItems]);
const renderItemContent: DraxListRenderItemContent<T> = React.useCallback(
(info, props) => {
const isHidden = hiddenItemsState.indexOf(info?.item?.id) > -1;
return isHidden && !dragging ? null : (
<View
style={{
flexDirection: "row",
paddingHorizontal: 12
}}
>
<View
style={{
flexGrow: 1,
opacity: isHidden ? 0.4 : 1
}}
>
{renderDraggableItem(info, props)}
</View>
{dragging && canHideItems ? (
<IconButton
name={!isHidden ? "minus" : "plus"}
color={colors.primary.icon}
size={SIZE.lg}
top={0}
bottom={0}
onPress={() => {
const _hiddenItems = hiddenItemsState.slice();
const index = _hiddenItems.indexOf(info.item.id);
if (index === -1) {
_hiddenItems.push(info.item?.id);
} else {
_hiddenItems.splice(index, 1);
}
onHiddenItemsChanged?.(_hiddenItems);
setHiddenItems(_hiddenItems);
}}
/>
) : null}
</View>
);
},
[
colors.primary.icon,
dragging,
hiddenItemsState,
onHiddenItemsChanged,
renderDraggableItem,
canHideItems
]
);
function getOrderedItems() {
const items: T[] = [];
itemOrderState.forEach((id) => {
const item = data.find((i) => i.id === id);
if (!item) return;
items.push(item);
});
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
return items;
}
return (
<DraxProvider>
<View style={styles.container}>
<DraxList
{...restProps}
ref={listRef}
data={getOrderedItems()}
renderItemContent={renderItemContent}
itemStyles={{
hoverDragReleasedStyle: {
display: "none"
},
hoverDraggingWithoutReceiverStyle: {
opacity: 0.5
},
dragReleasedStyle: {
opacity: 1
},
hoverDraggingStyle: {
backgroundColor: colors.secondary.background
}
}}
longPressDelay={500}
onItemDragStart={() =>
useSideBarDraggingStore.setState({
dragging: true
})
}
itemsDraggable={disableDefaultDrag ? dragging : true}
lockItemDragsToMainAxis
onItemReorder={({ fromIndex, fromItem, toIndex, toItem }) => {
const newOrder = getOrderedItems().map((item) => item.id);
const element = newOrder.splice(fromIndex, 1)[0];
if (toIndex === 0) {
newOrder.unshift(element);
} else {
newOrder.splice(toIndex, 0, element);
}
setItemsOrder(newOrder);
onListOrderChanged?.(newOrder);
}}
onItemDragEnd={(e) => {
console.log(e.receiver?.receiveOffset);
}}
keyExtractor={(item) => (item as any).id}
/>
</View>
</DraxProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1
}
});
export default ReorderableList;

View File

@@ -17,11 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedDate } from "@notesnook/common";
import { useThemeColors } from "@notesnook/theme";
import KeepAwake from "@sayem314/react-native-keep-awake";
import React, { useEffect, useRef, useState } from "react";
import { SafeAreaView, Text, View } from "react-native";
import { Modal, SafeAreaView, Text, View } from "react-native";
import Animated from "react-native-reanimated";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
@@ -35,7 +33,7 @@ import {
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
@@ -47,6 +45,8 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { useSettingStore } from "../../stores/use-setting-store";
import { getFormattedDate } from "@notesnook/common";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -62,7 +62,7 @@ const MergeConflicts = () => {
const applyChanges = async () => {
let _content = keep;
let note = await db.notes.note(_content.noteId);
let note = db.notes.note(_content.noteId).data;
await db.notes.add({
id: note.id,
conflicted: false,
@@ -100,7 +100,7 @@ const MergeConflicts = () => {
};
const show = async (item) => {
let noteContent = await db.content.get(item.contentId);
let noteContent = await db.content.raw(item.contentId);
content.current = { ...noteContent };
if (__DEV__) {
if (!noteContent.conflicted) {
@@ -190,7 +190,7 @@ const MergeConflicts = () => {
setDialogVisible(true);
}}
title="Save a copy"
type="secondary"
type="grayBg"
height={30}
style={{
borderRadius: 100,
@@ -320,7 +320,7 @@ const MergeConflicts = () => {
readonly
editorId=":conflictPrimary"
onLoad={async () => {
const note = await db.notes.note(content.current?.noteId);
const note = db.notes.note(content.current?.noteId)?.data;
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictPrimary", {
@@ -357,7 +357,7 @@ const MergeConflicts = () => {
readonly
editorId=":conflictSecondary"
onLoad={async () => {
const note = await db.notes.note(content.current?.noteId);
const note = db.notes.note(content.current?.noteId)?.data;
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictSecondary", {

View File

@@ -0,0 +1,164 @@
/*
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, { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { presentSheet } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { PressableButton } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
import { getFormattedDate, getTimeAgo } from "@notesnook/common";
export default function NoteHistory({ note, fwdRef }) {
const [history, setHistory] = useState([]);
const [_loading, setLoading] = useState(true);
const { colors } = useThemeColors();
useEffect(() => {
(async () => {
setHistory([...(await db.noteHistory.get(note.id))]);
setLoading(false);
})();
}, [note.id]);
const preview = useCallback(async (item) => {
let content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
/>
),
context: "note_history"
});
}, []);
const getDate = (start, end) => {
let _start_date = getFormattedDate(start, "date");
let _end_date = getFormattedDate(end + 60000, "date");
let _start_time = getFormattedDate(start, "time");
let _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_end_time}`;
};
const renderItem = useCallback(
({ item }) => (
<PressableButton
type="grayBg"
onPress={() => preview(item)}
customStyle={{
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 12,
height: 45,
marginBottom: 10,
flexDirection: "row"
}}
>
<Paragraph>{getDate(item.dateCreated, item.dateModified)}</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{getTimeAgo(item.dateModified)}
</Paragraph>
</PressableButton>
),
[colors.secondary.paragraph, preview]
);
return (
<View>
<SheetProvider context="note_history" />
<DialogHeader
title="Note history"
paragraph="Revert back to an older version of this note"
padding={12}
/>
<Seperator />
<FlatList
onMomentumScrollEnd={() => {
fwdRef?.current?.handleChildScrollEnd();
}}
style={{
paddingHorizontal: 12
}}
nestedScrollEnabled
keyExtractor={(item) => item.id}
data={history}
ListFooterComponent={<View style={{ height: 250 }} />}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Icon name="history" size={60} color={colors.primary.icon} />
<Paragraph color={colors.secondary.paragraph}>
No note history found on this device.
</Paragraph>
</View>
}
renderItem={renderItem}
/>
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center"
}}
>
Note version history is local only.{" "}
<Text
onPress={() => {
openLinkInBrowser(
"https://docs.notesnook.com/versionhistory",
colors
);
}}
style={{
color: colors.primary.accent,
textDecorationLine: "underline"
}}
>
Learn how this works.
</Text>
</Paragraph>
</View>
);
}

View File

@@ -1,214 +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, { RefObject, useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { getFormattedDate, getTimeAgo } from "@notesnook/common";
import { HistorySession, Note, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useDBItem } from "../../hooks/use-db-item";
import { presentSheet } from "../../services/event-manager";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
const HistoryItem = ({
index,
items,
note
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
note?: Note;
}) => {
const [item] = useDBItem(index, "noteHistory", items);
const { colors } = useThemeColors();
const getDate = (start: number, end: number) => {
const _start_date = getFormattedDate(start, "date");
const _end_date = getFormattedDate(end + 60000, "date");
const _start_time = getFormattedDate(start, "time");
const _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_end_time}`;
};
const preview = useCallback(async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
/>
),
context: "note_history"
});
}, []);
return (
<Pressable
type="secondary"
onPress={() => {
if (!item) return;
preview(item);
}}
style={{
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 12,
height: 45,
marginBottom: 10,
flexDirection: "row"
}}
>
{!item ? null : (
<>
<Paragraph>{getDate(item.dateCreated, item.dateModified)}</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{getTimeAgo(item.dateModified)}
</Paragraph>
</>
)}
</Pressable>
);
};
export default function NoteHistory({
note
}: {
note: Note;
fwdRef: RefObject<ActionSheetRef>;
}) {
const [history, setHistory] = useState<VirtualizedGrouping<HistorySession>>();
const [_loading, setLoading] = useState(true);
const { colors } = useThemeColors();
useEffect(() => {
db.noteHistory
.get(note.id)
.sorted({
sortBy: "dateModified",
sortDirection: "desc"
})
.then((result) => {
setHistory(result);
setLoading(false);
})
.catch((e) => {
setLoading(false);
});
}, [note.id]);
const renderItem = useCallback(
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} />
),
[history]
);
return (
<View>
<SheetProvider context="note_history" />
<DialogHeader
title="Note history"
paragraph="Revert back to an older version of this note"
padding={12}
/>
<Seperator />
<View
style={{
paddingHorizontal: 12,
height: !history?.placeholders.length
? 300
: (history.placeholders.length + 1) * 55,
maxHeight: "100%"
}}
>
<FlashList
data={history?.placeholders}
estimatedItemSize={55}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 300,
gap: 10
}}
>
{_loading ? (
<ActivityIndicator
size={SIZE.xl}
color={colors.primary.accent}
/>
) : (
<>
<Icon name="history" size={50} color={colors.primary.icon} />
<Paragraph color={colors.secondary.paragraph}>
No note history found on this device.
</Paragraph>
</>
)}
</View>
}
renderItem={renderItem}
/>
</View>
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center"
}}
>
Note version history is local only.{" "}
<Text
onPress={() => {
openLinkInBrowser("https://docs.notesnook.com/versionhistory");
}}
style={{
color: colors.primary.accent,
textDecorationLine: "underline"
}}
>
Learn how this works.
</Text>
</Paragraph>
</View>
);
}

View File

@@ -18,7 +18,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 React from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import Editor from "../../screens/editor";
@@ -43,7 +43,6 @@ import Paragraph from "../ui/typography/paragraph";
export default function NotePreview({ session, content, note }) {
const { colors } = useThemeColors();
const editorId = ":noteHistory";
const [locked, setLocked] = useState(false);
async function restore() {
if (note && note.type === "trash") {
@@ -76,10 +75,6 @@ export default function NotePreview({ session, content, note }) {
});
}
useEffect(() => {
db.vaults.itemExists(note).then((locked) => setLocked(locked));
}, [note]);
const deleteNote = async () => {
presentDialog({
title: `Delete note permanently`,
@@ -89,7 +84,7 @@ export default function NotePreview({ session, content, note }) {
context: "local",
positivePress: async () => {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: "Permanently deleted items",
@@ -105,13 +100,13 @@ export default function NotePreview({ session, content, note }) {
return (
<View
style={{
height: locked || session?.locked ? null : 600,
height: note?.locked || session?.locked ? null : 600,
width: "100%"
}}
>
<Dialog context="local" />
<DialogHeader padding={12} title={note?.title || session?.session} />
{!session?.locked && !locked ? (
{!session?.locked && !note?.locked ? (
<View
style={{
flex: 1
@@ -123,7 +118,7 @@ export default function NotePreview({ session, content, note }) {
readonly
editorId={editorId}
onLoad={async () => {
const _note = note || (await db.notes.note(session?.noteId));
const _note = note || db.notes.note(session?.noteId)?.data;
eSendEvent(eOnLoadNote + editorId, {
item: {
..._note,

View File

@@ -98,7 +98,7 @@ export const Component = ({ close, promo }) => {
onPress={() => {
close();
}}
style={{
customStyle={{
position: "absolute",
right: DDS.isTab ? 30 : 15,
top: Platform.OS === "ios" ? 0 : 30,
@@ -219,7 +219,7 @@ export const Component = ({ close, promo }) => {
title={
promo ? promo.text : user ? "See all plans" : "Sign up for free"
}
type={userCanRequestTrial ? "secondaryAccented" : "accent"}
type={userCanRequestTrial ? "grayAccent" : "accent"}
width={250}
style={{
paddingHorizontal: 12,

View File

@@ -186,7 +186,7 @@ export const Expiring = () => {
{status.extend && (
<Button
type="plain"
type="gray"
title="Not sure yet? Extend trial for 7 days"
textStyle={{
textDecorationLine: "underline"

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Platform, View } from "react-native";
import { SIZE } from "../../utils/size";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import RNIap from "react-native-iap";
@@ -42,10 +42,10 @@ export const PricingItem = ({
compact?: boolean;
}) => {
return (
<Pressable
<PressableButton
onPress={onPress}
type="secondary"
style={{
type="grayBg"
customStyle={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
@@ -95,6 +95,6 @@ export const PricingItem = ({
: "/month"}
</Paragraph>
</View>
</Pressable>
</PressableButton>
);
};

View File

@@ -328,7 +328,7 @@ export const PricingPlans = ({
}
}}
title={"Try free for 14 days"}
type="secondaryAccented"
type="grayAccent"
width={250}
style={{
paddingHorizontal: 12,

View File

@@ -17,11 +17,11 @@ 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, Note } from "@notesnook/core/dist/types";
import { DefaultColors } from "@notesnook/core/dist/collections/colors";
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { FlashList } from "@shopify/flash-list";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
@@ -32,29 +32,87 @@ import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { refreshNotesPage } from "../../utils/events";
import { SIZE } from "../../utils/size";
import ColorPicker from "../dialogs/color-picker";
import { Button } from "../ui/button";
import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
export const ColorTags = ({ item }: { item: Note }) => {
const { colors } = useThemeColors();
const [isLinked, setIsLinked] = useState<boolean>();
const setColorNotes = useMenuStore((state) => state.setColorNotes);
useEffect(() => {
const checkIsLinked = async (color: Color) => {
const hasRelation = await db.relations.from(color, "note").has(note.id);
return hasRelation;
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const updater = useRelationStore((state) => state.updater);
const getColorInfo = async (colorCode: string) => {
const dbColor = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", colorCode)])
);
let isLinked = false;
if (dbColor) {
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
if (hasRelation) {
isLinked = true;
}
}
return {
linked: isLinked,
item: dbColor
};
};
checkIsLinked(item).then((info) => setIsLinked(info));
}, [item, note.id]);
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
const color = DefaultColors[name];
const [colorInfo, setColorInfo] = useState<{
linked: boolean;
item: Color | undefined;
}>();
const toggleColor = async () => {
await db.relations.to(note, "color").unlink();
useEffect(() => {
getColorInfo(color).then((info) => setColorInfo(info));
}, [color]);
if (!isLinked) {
await db.relations.add(item, note);
return (
<PressableButton
type="accent"
accentColor={color}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(name)}
key={color}
onPress={() => changeColor(name)}
customStyle={{
width: 30,
height: 30,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: isTablet ? 10 : undefined
}}
>
{colorInfo?.linked ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
const changeColor = async (color: string) => {
const colorInfo = await getColorInfo(DefaultColors[color]);
if (colorInfo.item) {
if (colorInfo.linked) {
await db.relations.unlink(colorInfo.item, item);
} else {
await db.relations.add(colorInfo.item, item);
}
} else {
const colorId = await db.colors.add({
title: color,
colorCode: DefaultColors[color]
});
const dbColor = await db.colors.color(colorId);
if (dbColor) {
await db.relations.add(dbColor as unknown as ItemReference, item);
}
}
useRelationStore.getState().update();
@@ -64,125 +122,20 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
};
return (
<Pressable
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(item.colorCode)}
key={item.id}
onPress={toggleColor}
onLongPress={(event) => {
NativeTooltip.show(event, item.title, NativeTooltip.POSITIONS.TOP);
}}
<View
style={{
width: 35,
height: 35,
borderRadius: 100,
justifyContent: "center",
flexDirection: "row",
flexWrap: "wrap",
flexGrow: isTablet ? undefined : 1,
paddingHorizontal: 12,
paddingRight: 0,
alignItems: "center",
marginRight: 5
justifyContent: isTablet ? "center" : "space-between"
}}
>
{isLinked ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</Pressable>
);
};
export const ColorTags = ({ item }: { item: Note }) => {
const { colors } = useThemeColors();
const colorNotes = useMenuStore((state) => state.colorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const updater = useRelationStore((state) => state.updater);
const [visible, setVisible] = useState(false);
const note = item;
const renderItem = useCallback(
({ item }: { item: Color }) => (
<ColorItem note={note} key={item.id} item={item} />
),
[note]
);
return (
<>
<ColorPicker
visible={visible}
setVisible={setVisible}
onColorAdded={async (color) => {
await db.relations.add(color, note);
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
}}
/>
<View
style={{
flexGrow: isTablet ? undefined : 1,
flexDirection: "row",
marginLeft: 5,
flexShrink: 2
}}
>
<FlashList
data={colorNotes}
estimatedItemSize={30}
horizontal
extraData={updater}
bounces={false}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
ListFooterComponent={
!colorNotes || !colorNotes.length ? (
<Button
onPress={async () => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
buttonType={{
text: colors.primary.accent
}}
title="Add color"
type="secondary"
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.xs}
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
) : (
<Pressable
style={{
width: 35,
height: 35,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: 5
}}
type="secondary"
onPress={() => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
>
<Icon
testID="icon-plus"
name="plus"
color={colors.primary.icon}
size={SIZE.lg}
/>
</Pressable>
)
}
/>
</View>
</>
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
return <ColorItem key={name} name={name} />;
})}
</View>
);
};

View File

@@ -78,7 +78,7 @@ export const DateMeta = ({ item }) => {
paddingVertical: 5,
marginTop: 5,
borderTopWidth: 1,
borderTopColor: colors.primary.border,
borderTopColor: colors.secondary.background,
paddingHorizontal: 12
}}
>

View File

@@ -26,8 +26,6 @@ import { presentSheet } from "../../services/event-manager";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
import { Pressable } from "../ui/pressable";
import { ReminderTime } from "../ui/reminder-time";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -73,7 +71,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%"
}}
nestedScrollEnabled
data={[0]}
keyExtractor={() => "properties-scroll-item"}
renderItem={() => (
@@ -83,38 +80,17 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
paddingHorizontal: 12,
marginTop: 5,
zIndex: 10,
marginBottom: 5
marginBottom: 6
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{item.type === "color" ? (
<Pressable
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
style={{
width: 30,
height: 30,
borderRadius: 100,
marginRight: 10
}}
/>
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
{item.title}
</Heading>
</View>
{item.title}
</Heading>
{item.type === "notebook" && item.description ? (
<Paragraph>{item.description}</Paragraph>
@@ -142,22 +118,15 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
<DateMeta item={item} />
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
{item.type === "note" ? (
<>
<Tags close={close} item={item} />
<Line bottom={0} />
</>
) : null}
{item.type === "note" ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
) : null}
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
<Items
item={item}
@@ -227,23 +196,25 @@ Properties.present = async (item, buttons = [], isSheet) => {
"add-shortcut",
"trash",
"default-notebook",
"add-notebook",
"move-notes"
"add-notebook"
]);
break;
// case "topic":
// props[0] = db.notebooks
// .notebook(item.notebookId)
// .topics.topic(item.id)._topic;
// props.push([
// "move-notes",
// "edit-topic",
// "add-shortcut",
// "trash",
// "default-topic"
// ]);
// break;
case "tag":
props[0] = await db.tags.tag(item.id);
props.push(["add-shortcut", "trash", "rename-tag"]);
break;
case "color":
props[0] = await db.colors.color(item.id);
props.push([
"trash",
"rename-color",
...(useSideBarDraggingStore.getState().dragging ? [] : ["reorder"])
]);
break;
case "reminder": {
props[0] = await db.reminders.reminder(item.id);
props.push(["edit-reminder", "trash", "disable-reminder"]);

View File

@@ -17,16 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Dimensions, ScrollView, View } from "react-native";
import { Dimensions, FlatList, ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useActions } from "../../hooks/use-actions";
import { DDS } from "../../services/device-detection";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Items = ({ item, buttons, close }) => {
@@ -54,10 +54,10 @@ export const Items = ({ item, buttons, close }) => {
marginBottom: 10
}}
>
<Pressable
<PressableButton
onPress={item.func}
type={item.on ? "shade" : "secondary"}
style={{
type={item.on ? "shade" : "grayBg"}
customStyle={{
height: columnItemWidth - 12,
width: columnItemWidth - 12,
borderRadius: 10,
@@ -80,7 +80,7 @@ export const Items = ({ item, buttons, close }) => {
: colors.secondary.icon
}
/>
</Pressable>
</PressableButton>
<Paragraph
size={SIZE.xs}
@@ -105,7 +105,7 @@ export const Items = ({ item, buttons, close }) => {
onPress={item.func}
title={item.title}
icon={item.icon}
type={item.on ? "shade" : "plain"}
type={item.on ? "shade" : "gray"}
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -119,21 +119,22 @@ export const Items = ({ item, buttons, close }) => {
const renderTopBarItem = (item, index) => {
const isLast = index === topBarItems.length;
return (
<Pressable
<PressableButton
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
activeOpacity={1}
style={{
customStyle={{
alignSelf: "flex-start",
marginRight: isLast ? 0 : 10,
paddingHorizontal: 0,
width: topBarItemWidth
}}
>
<View
<PressableButton
onPress={item.func}
style={{
type={item.on ? "shade" : "gray"}
customStyle={{
height: topBarItemWidth,
width: topBarItemWidth,
justifyContent: "center",
@@ -156,7 +157,7 @@ export const Items = ({ item, buttons, close }) => {
: colors.secondary.icon
}
/>
</View>
</PressableButton>
<Paragraph
textBreakStrategy="simple"
@@ -165,7 +166,7 @@ export const Items = ({ item, buttons, close }) => {
>
{item.title}
</Paragraph>
</Pressable>
</PressableButton>
);
};
@@ -178,6 +179,10 @@ export const Items = ({ item, buttons, close }) => {
"lock-unlock"
];
if (!shouldShrink) {
topBarItemsList.push("publish");
}
const topBarItems = data
.filter((item) => topBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
@@ -197,12 +202,6 @@ export const Items = ({ item, buttons, close }) => {
"trash"
];
if (!shouldShrink) {
topBarItemsList.push("publish");
} else {
bottomBarItemsList.splice(4, 0, "publish");
}
const bottomGridItems = data
.filter((item) => bottomBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
@@ -211,13 +210,8 @@ export const Items = ({ item, buttons, close }) => {
: -1
);
let topBarItemWidth =
const topBarItemWidth =
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
topBarItemWidth;
if (topBarItemWidth < 55) {
topBarItemWidth = 55;
}
return item.type === "note" ? (
<>
@@ -225,24 +219,30 @@ export const Items = ({ item, buttons, close }) => {
horizontal
style={{
paddingHorizontal: 12,
marginTop: 6
paddingVertical: 12
}}
>
{topBarItems.map(renderTopBarItem)}
</ScrollView>
<View
<FlatList
data={bottomGridItems}
keyExtractor={(item) => item.title}
key={columnItemsCount + "key"}
numColumns={columnItemsCount}
disableVirtualization={true}
style={{
flexDirection: "row",
flexWrap: "wrap",
alignContent: "flex-start",
marginTop: item.type !== "note" ? 10 : 0,
paddingTop: 10,
marginLeft: 6
}}
>
{bottomGridItems.map((item) => _renderRowItem({ item }))}
</View>
contentContainerStyle={{
alignSelf: "center",
width: buttons.length < 5 ? "100%" : null,
paddingLeft: buttons.length < 5 ? 10 : 0
}}
renderItem={_renderRowItem}
/>
</>
) : (
<View data={data}>{data.map(renderColumnItem)}</View>

View File

@@ -28,7 +28,7 @@ import { eClearEditor } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
@@ -48,15 +48,15 @@ export default function Notebooks({ note, close, full }) {
};
const renderItem = (item) => (
<Pressable
<PressableButton
key={item.id}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
type={full ? "transparent" : "secondary"}
style={{
type={full ? "transparent" : "grayBg"}
customStyle={{
justifyContent: "flex-start",
paddingHorizontal: 12,
flexDirection: "row",
@@ -85,7 +85,7 @@ export default function Notebooks({ note, close, full }) {
>
{item.title}
</Heading>
</Pressable>
</PressableButton>
);
return noteNotebooks.length === 0 ? null : (
@@ -110,7 +110,7 @@ export default function Notebooks({ note, close, full }) {
paddingHorizontal: 0,
backgroundColor: "transparent"
}}
type="plain"
type="gray"
textStyle={{
textDecorationLine: "underline"
}}

View File

@@ -102,7 +102,7 @@ export const Synced = ({ item, close }) => {
title="Learn more"
fontSize={SIZE.xs}
height={30}
type="secondaryAccented"
type="grayAccent"
/>
</View>
) : null;

View File

@@ -35,6 +35,8 @@ export const Tags = ({ item, close }) => {
<View
style={{
marginTop: 5,
paddingTop: 6,
paddingBottom: 6,
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
@@ -51,15 +53,16 @@ export const Tags = ({ item, close }) => {
buttonType={{
text: colors.primary.accent
}}
title="Add tag"
type="secondary"
title="Add tags"
type="grayBg"
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.sm - 1}
fontSize={SIZE.xs}
style={{
height: 35,
borderRadius: 100
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
<ColorTags item={item} />
@@ -103,7 +106,7 @@ const TagItem = ({ tag, close }) => {
<Button
onPress={onPress}
title={"#" + tag}
type="secondary"
type="grayBg"
height={20}
fontSize={SIZE.xs}
style={style}

View File

@@ -19,14 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import React, { useCallback, useEffect } from "react";
import {
BackHandler,
NativeEventSubscription,
Platform,
View
} from "react-native";
import Animated, { FadeInUp } from "react-native-reanimated";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
@@ -44,6 +43,7 @@ import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import Animated, { FadeInUp } from "react-native-reanimated";
export const SelectionHeader = React.memo(
({
@@ -65,7 +65,8 @@ export const SelectionHeader = React.memo(
const clearSelection = useSelectionStore((state) => state.clearSelection);
const insets = useGlobalSafeAreaInsets();
const allSelected =
items?.placeholders?.length === selectedItemsList.length;
items?.ids?.filter((id) => typeof id === "string").length ===
selectedItemsList.length;
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
useEffect(() => {
@@ -155,7 +156,7 @@ export const SelectionHeader = React.memo(
}}
>
<IconButton
style={{
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,
@@ -163,7 +164,7 @@ export const SelectionHeader = React.memo(
borderRadius: 100,
marginRight: 10
}}
type="secondary"
type="grayBg"
onPress={() => {
clearSelection();
}}
@@ -199,11 +200,19 @@ export const SelectionHeader = React.memo(
onPress={async () => {
useSelectionStore
.getState()
.setAll(allSelected ? [] : [...((await items?.ids()) || [])]);
.setAll(
allSelected
? []
: [
...((items?.ids.filter(
(id) => typeof id !== "object"
) as string[]) || [])
]
);
}}
tooltipText="Select all"
tooltipPosition={4}
style={{
customStyle={{
marginLeft: 10
}}
color={
@@ -220,7 +229,7 @@ export const SelectionHeader = React.memo(
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
style={{
customStyle={{
marginLeft: 10
}}
color={colors.primary.icon}
@@ -236,7 +245,7 @@ export const SelectionHeader = React.memo(
}}
tooltipText="Export"
tooltipPosition={4}
style={{
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
@@ -248,7 +257,7 @@ export const SelectionHeader = React.memo(
onPress={async () => {
MoveNoteSheet.present();
}}
style={{
customStyle={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
@@ -271,7 +280,7 @@ export const SelectionHeader = React.memo(
clearSelection();
}
}}
style={{
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from Notebook`}
@@ -286,7 +295,7 @@ export const SelectionHeader = React.memo(
{focusedRouteId === "Favorites" ? (
<IconButton
onPress={addToFavorite}
style={{
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
@@ -299,17 +308,14 @@ export const SelectionHeader = React.memo(
{type === "trash" ? null : (
<IconButton
style={{
customStyle={{
marginLeft: 10
}}
onPress={() => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
);
}}
tooltipText="Move to trash"
tooltipPosition={1}
@@ -322,7 +328,7 @@ export const SelectionHeader = React.memo(
{type === "trash" ? (
<>
<IconButton
style={{
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
@@ -334,7 +340,7 @@ export const SelectionHeader = React.memo(
/>
<IconButton
style={{
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}

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