Compare commits

..

1 Commits

Author SHA1 Message Date
ammarahm-ed
74a0a117e6 editor: fix code-block ui 2023-09-11 12:35:04 +05:00
1252 changed files with 123048 additions and 347212 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

@@ -78,13 +78,7 @@ module.exports = {
semi: ["error", "always"],
"@typescript-eslint/no-empty-function": "off",
"react/prop-types": "off",
"header/header": ["error", "block", LICENSE, 1],
"@typescript-eslint/no-empty-interface": [
"error",
{
allowSingleExtends: true
}
]
"header/header": ["error", "block", LICENSE, 1]
},
settings: {
react: {

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
@@ -97,9 +80,6 @@ jobs:
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install setuptools
run: brew install python-setuptools
- name: Setup notarization
run: |
mkdir -p ~/private_keys/
@@ -117,7 +97,6 @@ jobs:
- name: Get App Store Version
id: appstore
uses: streetwriters/appstore-connect-app-version@develop
if: inputs.publish-apple
with:
app-id: ${{ steps.app_metadata.outputs.apple_app_id }}
key-id: ${{ secrets.api_key_id }}
@@ -126,7 +105,7 @@ jobs:
platform: "MAC_OS"
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -147,7 +126,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npx nx run release --project @notesnook/desktop -- --variant=mas
yarn electron-builder --mac mas --universal -p never
npx electron-builder --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
@@ -157,13 +136,13 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npx nx run release --project @notesnook/desktop
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
npx electron-builder --mac zip dmg --arm64 --x64 -p always
else
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
npx electron-builder --mac zip dmg --arm64 --x64 -p never
fi
working-directory: ./apps/desktop
@@ -183,7 +162,6 @@ jobs:
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
steps:
@@ -202,7 +180,7 @@ jobs:
if: inputs.publish-snap
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -219,7 +197,7 @@ jobs:
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --linux snap:x64 -p never
npx electron-builder --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
@@ -227,9 +205,9 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
else
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
@@ -242,27 +220,17 @@ jobs:
build-windows:
name: Build for Windows
needs: build
if: inputs.build-windows
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Install TrustedSigning
run: Install-Module -Name TrustedSigning -RequiredVersion 0.3.15 -Force -Repository PSGallery
shell: pwsh
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -278,9 +246,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

@@ -67,7 +67,7 @@ jobs:
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm install -D @playwright/test otplib dayjs @zip.js/zip.js
npm install -D @playwright/test otplib
- name: Validate build extraction
run: ls -ld ./apps/web/build/index.html

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

View File

@@ -1,3 +0,0 @@
owner: streetwriters
repo: notesnook
provider: github

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -27,15 +27,21 @@ import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sodiumNativePrebuildPath = path.join(
`node_modules`,
`@notesnook`,
`crypto`,
`node_modules`,
`@notesnook`,
`sodium`,
`node_modules`,
`sodium-native`,
`prebuilds`
);
const RUNNING_PROCESSES = [];
const RESTARTABLE_PROCESSES = [];
let lastBundleHash = null;
const ENV = {
...process.env,
NO_COLOR: "true",
FORCE_COLOR: "false",
COLOR: "0"
};
await onChange(true);
console.log("Watching...");
@@ -52,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-builder install-app-deps");
}
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-TrustedSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
console.debug("Signing", configuration.path, "using command", command);
psexec(command);
console.debug("Signed", configuration.path);
rmSync(FilesCatalog);
};
function createCatalog(path) {
const catalogPath = join(__dirname, "_catalog");
writeFileSync(catalogPath, relative(__dirname, path));
return catalogPath;
}
function psexec(cmd) {
return execSync(cmd, {
env: process.env,
stdio: "inherit",
shell: "pwsh"
});
}

View File

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

View File

@@ -29,9 +29,6 @@ import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
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();
@@ -47,22 +44,12 @@ const NotificationOptions = z.object({
});
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
globalThis.window?.webContents.setZoomFactor(factor);
config.zoomFactor = factor;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure
.input(z.string().optional())
.mutation(({ input: proxyRules }) => {
globalThis.window?.webContents.session.setProxy({ proxyRules });
config.proxyRules = proxyRules || "";
}),
privacyMode: t.procedure.query(() => config.privacyMode),
setPrivacyMode: t.procedure
.input(z.object({ enabled: z.boolean() }))
@@ -92,7 +79,6 @@ export const osIntegrationRouter = t.router({
AutoLaunch.disable();
}
config.desktopSettings = settings;
setupDesktopIntegration(settings);
}),
selectDirectory: t.procedure
@@ -131,17 +117,6 @@ export const osIntegrationRouter = t.router({
writeFileSync(resolvedPath, data);
}),
resolvePath: t.procedure
.input(z.object({ filePath: z.string() }))
.query(({ input }) => {
const { filePath } = input;
return resolvePath(filePath);
}),
deleteFile: t.procedure.input(z.string()).query(async ({ input }) => {
await rm(input);
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {
@@ -170,42 +145,16 @@ export const osIntegrationRouter = t.router({
}),
bringToFront: t.procedure.query(() => bringToFront()),
changeTheme: t.procedure
.input(
z.object({
theme: Theme,
windowControlsIconColor: z.string().optional(),
backgroundColor: z.string().optional()
})
)
.mutation(
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor;
if (process.platform === "win32")
globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor
});
}
if (backgroundColor) {
config.backgroundColor = backgroundColor;
}
setTheme(theme);
}
),
.input(Theme)
.mutation(({ input }) => setTheme(input)),
onThemeChanged: t.procedure.subscription(() =>
observable<"dark" | "light">((emit) => {
const updated = () => {
nativeTheme.on("updated", () => {
if (getTheme() === "system") {
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
}
};
nativeTheme.on("updated", updated);
return () => {
nativeTheme.off("updated", updated);
};
});
})
)
});

View File

@@ -1,44 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { safeStorage } from "electron";
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const safeStorageRouter = t.router({
isEncryptionAvailable: t.procedure.query(() => {
return (
!process.env.PORTABLE_EXECUTABLE_DIR &&
safeStorage.isEncryptionAvailable()
);
}),
/**
* Takes a string and returns an encrypted base64 string
*/
encryptString: t.procedure.input(z.string()).query(({ input }) => {
return safeStorage.encryptString(input).toString("base64");
}),
/**
* Takes an encrypted base64 string and returns a string
*/
decryptString: t.procedure.input(z.string()).query(({ input }) => {
return safeStorage.decryptString(Buffer.from(input, "base64"));
})
});

View File

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

View File

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

View File

@@ -1,83 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
const t = initTRPC.create();
export const windowRouter = t.router({
maximize: t.procedure.mutation(() => {
globalThis.window?.maximize();
}),
restore: t.procedure.mutation(() => {
globalThis.window?.restore();
}),
minimze: t.procedure.mutation(() => {
globalThis.window?.minimize();
}),
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
onWindowStateChanged: t.procedure.subscription(() => {
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
function listener() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: !!globalThis.window?.isFullScreen()
});
}
function enterFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: true
});
}
function leaveFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: false
});
}
globalThis.window?.addListener("maximize", listener);
globalThis.window?.addListener("minimize", listener);
globalThis.window?.addListener("unmaximize", listener);
globalThis.window?.addListener("restore", listener);
globalThis.window?.addListener("enter-full-screen", enterFullscreen);
globalThis.window?.addListener("leave-full-screen", leaveFullscreen);
globalThis.window?.addListener("leave-html-full-screen", leaveFullscreen);
globalThis.window?.addListener("enter-html-full-screen", enterFullscreen);
return () => {
globalThis.window?.removeListener("maximize", listener);
globalThis.window?.removeListener("minimize", listener);
globalThis.window?.removeListener("unmaximize", listener);
globalThis.window?.removeListener("restore", listener);
globalThis.window?.removeListener("enter-full-screen", enterFullscreen);
globalThis.window?.removeListener("leave-full-screen", leaveFullscreen);
globalThis.window?.removeListener(
"leave-html-full-screen",
leaveFullscreen
);
globalThis.window?.removeListener(
"enter-html-full-screen",
enterFullscreen
);
};
});
})
});

View File

@@ -27,23 +27,19 @@ export type CLIOptions = {
hidden: boolean;
};
export async function parseArguments(argv: string[]): Promise<CLIOptions> {
export async function parseArguments(): Promise<CLIOptions> {
const result: CLIOptions = {
note: false,
notebook: false,
reminder: false,
hidden: false
};
const { hidden } = await yargs(hideBin(argv))
const { hidden } = await yargs(hideBin(process.argv))
.boolean("hidden")
// have to account for this flag added on Windows when launching
// via Jumplist
.boolean("allow-file-access-from-files")
.command("new", "Create a new item", (yargs) => {
return yargs
.command("note", "Create a new note", {}, () => {
result.note = true;
console.log("HERE!");
})
.command("notebook", "Create a new notebook", {}, () => {
result.notebook = true;

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

@@ -24,6 +24,7 @@ import { configureAutoUpdater } from "./utils/autoupdater";
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
import { setupMenu } from "./utils/menu";
import { WindowState } from "./utils/window-state";
import { AutoLaunch } from "./utils/autolaunch";
import { setupJumplist } from "./utils/jumplist";
import { setupTray } from "./utils/tray";
import { CLIOptions, parseArguments } from "./cli";
@@ -33,8 +34,6 @@ import { router, api } from "./api";
import { config } from "./utils/config";
import path from "path";
import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -64,7 +63,7 @@ process.on("unhandledRejection", (reason) => {
app.commandLine.appendSwitch("lang", "en-US");
async function createWindow() {
const cliOptions = await parseArguments(process.argv);
const cliOptions = await parseArguments();
setTheme(getTheme());
const mainWindowState = new WindowState({});
@@ -77,24 +76,11 @@ async function createWindow() {
darkTheme: getTheme() === "dark",
backgroundColor: getBackgroundColor(),
opacity: 0,
autoHideMenuBar: false,
autoHideMenuBar: true,
icon: AssetManager.appIcon({
size: 512,
format: process.platform === "win32" ? "ico" : "png"
}),
titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
},
webPreferences: {
zoomFactor: config.zoomFactor,
nodeIntegration: true,
@@ -104,9 +90,7 @@ async function createWindow() {
}
});
createIPCHandler({ router, windows: [mainWindow] });
globalThis.window = mainWindow;
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
@@ -120,12 +104,12 @@ async function createWindow() {
}
await AssetManager.loadIcons();
setupDesktopIntegration(config.desktopSettings);
setupDesktopIntegration();
createIPCHandler({ router, windows: [mainWindow] });
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
mainWindow.once("closed", () => {
globalThis.window = null;
@@ -135,7 +119,7 @@ async function createWindow() {
setupJumplist();
if (isDevelopment())
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
@@ -151,15 +135,6 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
app.configureHostResolver({
secureDnsServers: [
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true,
secureDnsMode: "automatic"
});
if (!isDevelopment()) registerProtocol();
await createWindow();
configureAutoUpdater();
@@ -171,12 +146,8 @@ app.once("window-all-closed", () => {
}
});
app.on("second-instance", async (_ev, argv) => {
app.on("second-instance", () => {
if (!globalThis.window) return;
const cliOptions = await parseArguments(argv);
if (cliOptions.note) bridge.onCreateItem("note");
if (cliOptions.notebook) bridge.onCreateItem("notebook");
if (cliOptions.reminder) bridge.onCreateItem("reminder");
bringToFront();
});
@@ -200,3 +171,49 @@ function createURL(options: CLIOptions, path = "/") {
return url;
}
function setupDesktopIntegration() {
const desktopIntegration = config.desktopSettings;
if (
desktopIntegration.closeToSystemTray ||
desktopIntegration.minimizeToSystemTray
) {
setupTray();
}
// when close to system tray is enabled, it becomes nigh impossible
// to "quit" the app. This is necessary in order to fix that.
if (desktopIntegration.closeToSystemTray) {
app.on("before-quit", () => app.exit(0));
}
globalThis.window?.once("close", (e) => {
if (config.desktopSettings.closeToSystemTray) {
e.preventDefault();
if (process.platform == "darwin") {
// on macOS window cannot be minimized/hidden if it is already fullscreen
// so we just close it.
if (globalThis.window?.isFullScreen()) app.exit(0);
else app.hide();
} else {
globalThis.window?.minimize();
globalThis.window?.hide();
}
}
});
globalThis.window?.on("minimize", () => {
if (config.desktopSettings.minimizeToSystemTray) {
if (process.platform == "darwin") {
app.hide();
} else {
globalThis.window?.hide();
}
}
});
if (desktopIntegration.autoStart) {
AutoLaunch.enable(!!desktopIntegration.startMinimized);
}
}

View File

@@ -23,16 +23,15 @@ 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 () => {
console.log("HELLO!");
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
@@ -42,6 +41,11 @@ process.once("loaded", async () => {
globalThis.electronTRPC = electronTRPC;
});
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
globalThis.NativeNNCrypto =
process.platform === "win32" &&
process.arch !== "x64" &&
process.arch !== "ia32"
? undefined
: require("@notesnook/crypto").NNCrypto;
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -25,11 +25,7 @@ Type=Application
Version=${app.getVersion()}
Name=${app.getName()}
Comment=${app.getName()} startup script
Exec=${
process.env.APPIMAGE
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
: `${process.execPath}${hidden ? " --hidden" : ""}`
}
Exec=${process.execPath}${hidden ? " --hidden" : ""}
StartupNotify=false
Terminal=false`;
@@ -39,8 +35,6 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
"autostart"
);
const STARTUP_ARGS = ["--hidden"];
export class AutoLaunch {
static enable(hidden: boolean) {
if (process.platform === "linux") {
@@ -53,14 +47,10 @@ export class AutoLaunch {
LINUX_DESKTOP_ENTRY(hidden)
);
} else {
const loginItemSettings = app.getLoginItemSettings({
args: STARTUP_ARGS
});
if (loginItemSettings.openAtLogin) return;
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: hidden,
args: STARTUP_ARGS
args: ["--hidden"]
});
}
}

View File

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

View File

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

View File

@@ -1,74 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app } from "electron";
import { DesktopIntegration, config } from "./config";
import { setupTray, destroyTray } from "./tray";
import { AutoLaunch } from "./autolaunch";
export function setupDesktopIntegration(
desktopIntegration: DesktopIntegration
) {
if (
desktopIntegration.closeToSystemTray ||
desktopIntegration.minimizeToSystemTray
) {
setupTray();
} else {
destroyTray();
}
// when close to system tray is enabled, it becomes nigh impossible
// to "quit" the app. This is necessary in order to fix that.
app.on("before-quit", () =>
desktopIntegration.closeToSystemTray ? app.exit(0) : null
);
globalThis.window?.on("close", (e) => {
if (config.desktopSettings.closeToSystemTray) {
e.preventDefault();
if (process.platform == "darwin") {
// on macOS window cannot be minimized/hidden if it is already fullscreen
// so we just close it.
if (globalThis.window?.isFullScreen()) app.exit(0);
else app.hide();
} else {
try {
globalThis.window?.minimize();
globalThis.window?.hide();
} catch (error) {
console.error(error);
}
}
}
});
globalThis.window?.on("minimize", () => {
if (config.desktopSettings.minimizeToSystemTray) {
if (process.platform == "darwin") {
app.hide();
} else {
globalThis.window?.hide();
}
}
});
if (desktopIntegration.autoStart) {
AutoLaunch.enable(!!desktopIntegration.startMinimized);
}
}

View File

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

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { net, protocol } from "electron";
import { protocol, net } from "electron";
import { isDevelopment } from "./index";
import { createReadStream } from "fs";
import { extname, normalize } from "path";
@@ -62,7 +62,9 @@ function registerProtocol() {
headers: { "Content-Type": extensionToMimeType[fileExtension] }
});
} else {
return net.fetch(request, { bypassCustomProtocolHandlers: true });
return net.fetch(request, {
bypassCustomProtocolHandlers: true
});
}
});
console.info(`${SCHEME} protocol inteception "successful"`);

View File

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

View File

@@ -16,13 +16,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "react-native-gesture-handler";
import "@azure/core-asynciterator-polyfill";
import SettingsService from "./services/settings";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { I18nManager, View } from "react-native";
import { View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
@@ -30,26 +32,20 @@ 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";
import { themeTrpcClient } from "./screens/settings/theme-selector";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
SettingsService.checkOrientation();
const App = () => {
const init = useAppEvents();
useEffect(() => {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
let { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
@@ -101,10 +97,10 @@ const App = () => {
let currTheme =
useThemeStore.getState().colorScheme === "dark"
? SettingsService.getProperty("darkTheme")
: SettingsService.getProperty("lighTheme");
: SettingsService.getProperty("lightTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: () => JSX.Element) => {
export const withTheme = (Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

View File

@@ -16,18 +16,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger } from "@notesnook/core/dist/logger";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import "./logger";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
database.host(
__DEV__
@@ -37,11 +34,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 +49,15 @@ 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,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem,
{
compress: Gzip.deflate,
decompress: Gzip.inflate
}
);
export const db = database;
export const DatabaseLogger = dbLogger;

View File

@@ -17,13 +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 { MMKVLoader } from "react-native-mmkv-storage";
import { initialize } from "@notesnook/core/dist/logger";
import { initalize } from "@notesnook/core/dist/logger";
import { KV } from "./storage";
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
initialize(new KV(LoggerStorage));
initalize(new KV(LoggerStorage));
export { LoggerStorage };
export {};

View File

@@ -1,123 +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 type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
private connectionMutex = new ConnectionMutex();
private db: QuickSQLiteConnection;
constructor(private readonly config: Config) {
this.db = open({
name: config.dbName
});
}
async init(): Promise<void> {
this.connection = new RNSqliteConnection(this.db);
}
async acquireConnection(): Promise<DatabaseConnection> {
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.connectionMutex.lock();
return this.connection!;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("BEGIN TRANSACTION"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("COMMIT"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("ROLLBACK"));
}
async releaseConnection(): Promise<void> {
this.connectionMutex.unlock();
}
async destroy(): Promise<void> {
this.db.close();
}
}
class ConnectionMutex {
private promise?: Promise<void>;
private resolve?: () => void;
async lock(): Promise<void> {
while (this.promise) {
await this.promise;
}
this.promise = new Promise((resolve) => {
this.resolve = resolve;
});
}
unlock(): void {
const resolve = this.resolve;
this.promise = undefined;
this.resolve = undefined;
resolve?.();
}
}
class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(
compiledQuery: CompiledQuery<unknown>
): Promise<QueryResult<R>> {
const { parameters, sql, query } = compiledQuery;
const mode =
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);
if (mode === "query" || !result.insertId)
return {
rows: result.rows?._array || []
};
return {
insertId: BigInt(result.insertId),
numAffectedRows: BigInt(result.rowsAffected),
rows: mode === "raw" ? result.rows?._array || [] : []
};
}
}

View File

@@ -90,11 +90,6 @@ export class KV {
return this.storage.removeItem(key);
}
async removeMulti(keys) {
if (!keys) return true;
return this.storage.removeItems(keys);
}
async clear() {
return this.storage.clearStore();
}
@@ -150,7 +145,6 @@ export default {
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
encrypt,
decrypt,
decryptMulti,

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

@@ -29,13 +29,11 @@ import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { createCacheDir, exists } from "./io";
import { IOS_APPGROUPID } from "../../utils/constants";
export const FileDownloadStatus = {
Success: 1,
@@ -46,15 +44,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
*/
@@ -64,7 +62,6 @@ export async function downloadAttachments(
canceled,
groupId
) {
await createCacheDir();
if (!attachments || !attachments.length) return;
const result = new Map();
@@ -84,15 +81,13 @@ export async function downloadAttachments(
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
if (await RNFetchBlob.fs.exists(zipSourceFolder))
await RNFetchBlob.fs.unlink(zipSourceFolder);
}
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
let attachment = attachments[i];
const hash = attachment.metadata.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -116,20 +111,19 @@ export async function downloadAttachments(
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
});
} catch (e) {
result.set(hash, {
filename: attachment.filename,
filename: attachment.metadata.filename,
status: FileDownloadStatus.Fail,
reason: e
});
ToastManager.error(e, "Error downloading attachment");
console.log("Error downloading attachment", e);
}
}
if (canceled.current) {
@@ -166,7 +160,7 @@ export async function downloadAttachments(
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
console.log("Error zipping attachments", e);
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -185,16 +179,12 @@ export default async function downloadAttachment(
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
groupId: undefined
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
let attachment = db.attachments.attachment(hash);
if (!attachment) {
DatabaseLogger.log("Attachment not found");
console.log("attachment not found");
return;
}
@@ -209,47 +199,39 @@ export default async function downloadAttachment(
}
try {
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
console.log(
"starting download attachment",
attachment.metadata.hash,
options.groupId
);
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
)
return;
}
if (options.base64 || options.text) {
console.log(
"starting decrypt base64 file...",
options.base64,
options.text,
attachment.hash
);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
);
}
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
attachment.metadata.filename,
attachment.metadata.type
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
length: attachment.length,
alg: attachment.alg,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType,
mime: attachment.metadata.type,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
chunkSize: attachment.chunkSize
};
let fileUri = await Sodium.decryptFile(
key,
info,
@@ -257,7 +239,7 @@ export default async function downloadAttachment(
);
if (!options.silent) {
ToastManager.show({
ToastEvent.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
@@ -266,11 +248,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) {
@@ -285,7 +267,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} />
});
}
@@ -295,13 +277,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

@@ -20,23 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "@notesnook/core/dist/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) {
if (!data) return false;
console.log("Downloading", filename);
await createCacheDir();
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
console.log("Exists already", filename);
let exists = await RNFetchBlob.fs.exists(path);
if (exists) {
return true;
}
@@ -64,55 +60,44 @@ export async function downloadFile(filename, data, cancelToken) {
console.log("downloading: ", recieved, total);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
cancelToken.cancel = request.cancel;
let response = await request;
await fileCheck(response, totalSize);
let status = response.info().status;
useAttachmentStore.getState().remove(filename);
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled") {
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
}
ToastEvent.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastEvent.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("Download file error:", e, url, headers);
console.log("download file error: ", e, url, headers);
return false;
}
}
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.user.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
return 0;
}
const contentLength = parseInt(attachmentInfo.headers?.get("content-length"));
return isNaN(contentLength) ? 0 : contentLength;
}
export async function checkAttachment(hash) {
@@ -120,7 +105,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,25 +17,35 @@ 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 { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
import { compressToBase64 } from "./compress";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
console.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists =
(await RNFetchBlob.fs.exists(path)) ||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
if (!exists) {
return false;
}
const attachment = await db.attachments.attachment(filename);
console.log("decrypting....");
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
let output = await Sodium.decryptFile(
key,
{
@@ -43,14 +53,24 @@ 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);
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
const dCachePath = `${cacheDir}/${output}`;
output = await compressToBase64(
`file://${dCachePath}`,
isPng ? "PNG" : "JPEG"
);
}
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("readEncrypted", e);
console.log(e);
return false;
}
}
@@ -67,8 +87,7 @@ export async function hashBase64(data) {
};
}
export async function writeEncryptedBase64(data, key) {
await createCacheDir();
export async function writeEncryptedBase64({ data, key }) {
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
@@ -77,8 +96,6 @@ export async function writeEncryptedBase64(data, key) {
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
output.size = output.length;
delete output.length;
return {
...output,
alg: "xcha-stream"
@@ -86,7 +103,6 @@ export async function writeEncryptedBase64(data, key) {
}
export async function deleteFile(filename, data) {
await createCacheDir();
let delFilePath = cacheDir + `/${filename}`;
if (!data) {
if (!filename) return;
@@ -112,89 +128,19 @@ export async function deleteFile(filename, data) {
export async function clearFileStorage() {
try {
let files = await RNFetchBlob.fs.ls(cacheDir);
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
for (let file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
try {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`);
} catch (e) {
console.log(e);
}
}
} catch (e) {
console.log("clearFileStorage", e);
console.log(e);
}
}
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
console.log("Cache directory created");
}
}
export async function migrateFilesFromCache() {
try {
await createCacheDir();
const migratedFilesPath = cacheDir + "/.migrated_1";
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
if (migrated) {
console.log("Files migrated already");
return;
}
let files = await RNFetchBlob.fs.ls(cacheDir);
console.log("Files to migrate:", files.join(","));
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
.catch(console.log);
console.log("Moved", file);
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
console.log("migrateFilesFromCache", e);
}
}
const ABYTES = 17;
export async function exists(filename) {
let path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path
);
if (stat.size !== expectedFileSize) {
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log);
return false;
}
exists = true;
}
let exists = await RNFetchBlob.fs.exists(`${cacheDir}/${filename}`);
return exists;
}

View File

@@ -24,12 +24,11 @@ import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
let { url, headers } = data;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
@@ -73,10 +72,7 @@ export async function uploadFile(filename, data, cancelToken) {
);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
cancelToken.cancel = request.cancel;
let response = await request;
let status = response.info().status;
@@ -87,9 +83,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

@@ -21,12 +21,7 @@ import * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
export const cacheDir =
Platform.OS == "ios"
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
export function getRandomId(prefix) {
return Math.random()
@@ -71,12 +66,9 @@ export function cancelable(operation) {
}
export function copyFileAsync(source, dest) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
if (e) {
reject(e);
return;
}
console.log(e, r);
resolve();
});
});

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

@@ -34,69 +34,10 @@ import BaseDialog from "../dialog/base-dialog";
import { allowedOnPlatform, renderItem } from "./functions";
import { useCallback } from "react";
/**
* Test announcement
* {
id: "some-announcement",
type: "dialog",
body: [
{
type: "title",
text: "This is a title",
platforms: ["all"]
},
{
type: "description",
text: "Most of you are too busy to keep up to date with what's happening in Notesnook. That is unfortunate because Notesnook has come a looooong way.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "To solve this, we are launching the Notesnook Digest — a newsletter to help you stay updated about Notesnook development. And to keep things interesting I'll also sprinkle this newsletter with other interesting stuff like privacy tips & news, interesting books, things I am looking forward to etc.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "So be sure to subscribe. There won't be a proper schedule to this (yet) maybe once or twice a month. I promise no spam — only more awesomeness.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "— May privacy reign.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "callToActions",
actions: [
{
type: "promo",
title: "15% Off",
platforms: ["android"],
data: "com.streetwriters.notesnook.sub.yr.15"
}
],
platforms: ["all"]
}
]
}
*/
export const AnnouncementDialog = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [info, setInfo] = useState();
const [info, setInfo] = useState(null);
const remove = useMessageStore((state) => state.remove);
useEffect(() => {
@@ -110,9 +51,7 @@ export const AnnouncementDialog = () => {
const open = (data) => {
setInfo(data);
setImmediate(() => {
setVisible(true);
});
setVisible(true);
};
const close = useCallback(() => {

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

@@ -0,0 +1,215 @@
/*
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, useRef } from "react";
import { Platform, View } from "react-native";
import { enabled } from "react-native-privacy-snapshot";
import { db } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
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";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
const AppLockedOverlay = () => {
const { colors } = useThemeColors();
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();
const password = useRef();
const appState = useAppState();
const biometricUnlockAwaitingUserInput = useRef(false);
const onUnlockAppRequested = useCallback(async () => {
if (!(await BiometricService.isBiometryAvailable())) return;
if (Platform.OS === "android") {
const activityName = await NotesnookModule.getActivityName();
if (activityName !== "MainActivity") return;
}
useSettingStore.getState().setRequestBiometrics(true);
let unlocked = await BiometricService.validateUser(
"Unlock to access your notes",
""
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = null;
}
setTimeout(() => {
biometricUnlockAwaitingUserInput.current = false;
useSettingStore.getState().setRequestBiometrics(false);
}, 1);
}, [lockApp]);
const onSubmit = async () => {
if (!password.current) return;
try {
let unlocked = await db.user.verifyPassword(password.current);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = null;
}
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (
biometricUnlockAwaitingUserInput.current ||
useUserStore.getState().disableAppLockRequests
)
return;
if (appLocked && appState === "active") {
biometricUnlockAwaitingUserInput.current = true;
onUnlockAppRequested();
}
}, [appState, onUnlockAppRequested, appLocked]);
return (
appLocked && (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
}}
>
<View
style={{
flex: 1,
justifyContent: "center",
width:
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,
alignSelf: "center"
}}
>
<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={{
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
}}
>
{user ? (
<>
<Input
fwdRef={passwordInputRef}
secureTextEntry
placeholder="Enter account password"
onChangeText={(v) => (password.current = v)}
onSubmit={onSubmit}
/>
</>
) : null}
<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}
onPress={onUnlockAppRequested}
icon={"fingerprint"}
type="transparent"
/>
</View>
</View>
</View>
</View>
)
);
};
export default AppLockedOverlay;

View File

@@ -1,328 +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, { useCallback, useEffect, useRef } from "react";
import { AppStateStatus, 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 { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { NotesnookModule } from "../../utils/notesnook-module";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
const getUser = () => {
const user = MMKV.getString("user");
if (user) {
return JSON.parse(user);
}
};
const verifyUserPassword = async (password: string) => {
try {
await getDatabaseKey();
const key = await getCryptoKey();
const user = getUser();
const cipher = await encrypt(
{
key: key,
salt: user.salt
},
"notesnook"
);
const plainText = await decrypt({ password }, cipher);
return plainText === "notesnook";
} catch (e) {
DatabaseLogger.error(e as Error);
return false;
}
};
const AppLockedOverlay = () => {
const initialLaunchBiometricRequest = useRef(true);
const { colors } = useThemeColors();
const user = getUser();
const appLocked = useUserStore((state) => state.appLocked);
const lockApp = useUserStore((state) => state.lockApp);
const deviceMode = useSettingStore((state) => state.deviceMode);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>();
const appState = useAppState();
const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false);
const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType
);
const appLockHasPasswordSecurity = useSettingStore(
(state) => state.settings.appLockHasPasswordSecurity
);
const biometricsAuthEnabled = useSettingStore(
(state) =>
state.settings.biometricsAuthEnabled === true ||
(state.settings.biometricsAuthEnabled === undefined &&
!state.settings.appLockHasPasswordSecurity)
);
const onUnlockAppRequested = useCallback(async () => {
if (
!biometricsAuthEnabled ||
!(await BiometricService.isBiometryAvailable())
)
return;
biometricUnlockAwaitingUserInput.current = true;
if (Platform.OS === "android") {
const activityName = await NotesnookModule.getActivityName();
if (activityName !== "MainActivity") return;
}
useSettingStore.getState().setRequestBiometrics(true);
const unlocked = await BiometricService.validateUser(
"Unlock to access your notes",
""
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
}
biometricUnlockAwaitingUserInput.current = false;
setTimeout(() => {
if (biometricUnlockAwaitingUserInput.current) return;
useSettingStore.getState().setRequestBiometrics(false);
}, 500);
}, [biometricsAuthEnabled, lockApp]);
const onSubmit = async () => {
if (!password.current) return;
try {
const unlocked = appLockHasPasswordSecurity
? await validateAppLockPassword(password.current)
: await verifyUserPassword(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;
}
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (appState === "active") {
enabled(false);
}
const prevState = lastAppState.current;
lastAppState.current = appState;
if (
appState === "active" &&
(prevState === "background" || initialLaunchBiometricRequest.current)
) {
if (SettingsService.shouldLockAppOnEnterForeground()) {
DatabaseLogger.info("Locking app on entering foreground");
useUserStore.getState().lockApp(true);
}
if (
!(
biometricUnlockAwaitingUserInput.current ||
useUserStore.getState().disableAppLockRequests ||
!appLocked ||
useSettingStore.getState().requestBiometrics ||
useSettingStore.getState().appDidEnterBackgroundForAction
)
) {
initialLaunchBiometricRequest.current = false;
DatabaseLogger.info("Biometric unlock request");
onUnlockAppRequested();
}
enabled(false);
} else {
SettingsService.appEnteredBackground();
if (
SettingsService.get().privacyScreen ||
SettingsService.getProperty("appLockEnabled")
) {
enabled(true);
}
}
}, [appState, onUnlockAppRequested, appLocked]);
return appLocked ? (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
}}
>
<View
style={{
flex: 1,
justifyContent: "center",
width:
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,
alignSelf: "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
}}
>
{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
style={{
marginTop: user ? 25 : 25
}}
>
{user || appLockHasPasswordSecurity ? (
<>
<Button
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
height={45}
style={{
borderRadius: 150,
marginBottom: 10
}}
fontSize={SIZE.md}
/>
</>
) : null}
{biometricsAuthEnabled ? (
<Button
title="Unlock with Biometrics"
width={250}
onPress={onUnlockAppRequested}
icon={"fingerprint"}
type="transparent"
/>
) : null}
</View>
</View>
</View>
</View>
) : null;
};
export default AppLockedOverlay;

View File

@@ -17,13 +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 { formatBytes } from "@notesnook/common";
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { RefObject, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
@@ -31,18 +27,14 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import picker from "../../screens/editor/tiptap/picker";
import {
ToastManager,
eSendEvent,
presentSheet
presentSheet,
ToastEvent
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import {
eCloseAttachmentDialog,
eCloseSheet,
eDBItemUpdate,
eOnLoadNote
} from "../../utils/events";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -51,42 +43,31 @@ import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import { formatBytes } from "@notesnook/common";
const Actions = ({
attachment,
close,
setAttachments,
fwdRef
}: {
attachment: Attachment;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const Actions = ({ attachment, setAttachments, fwdRef }) => {
const { colors } = useThemeColors();
const contextId = attachment.hash;
const [filename, setFilename] = useState(attachment.filename);
const contextId = attachment.metadata.hash;
const [filename, setFilename] = useState(attachment.metadata.filename);
const [currentProgress] = useAttachmentProgress(attachment);
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState<{
name?: string;
}>({});
const [failed, setFailed] = useState(attachment.failed);
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState({
name: null
});
const actions = [
{
name: "Download",
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.hash);
useAttachmentStore.getState().remove(attachment.hash);
await db.fs.cancel(attachment.metadata.hash, "download");
useAttachmentStore.getState().remove(attachment.metadata.hash);
}
downloadAttachment(attachment.hash, false);
downloadAttachment(attachment.metadata.hash, false);
eSendEvent(eCloseSheet, contextId);
},
icon: "download"
@@ -95,7 +76,7 @@ const Actions = ({
name: "Reupload",
onPress: async () => {
if (!PremiumService.get()) {
ToastManager.show({
ToastEvent.show({
heading: "Upgrade to pro",
type: "error",
context: "local"
@@ -104,9 +85,9 @@ const Actions = ({
}
await picker.pick({
reupload: true,
hash: attachment.hash,
hash: attachment.metadata.hash,
context: contextId,
type: attachment.mimeType.startsWith("image") ? "image" : "file"
type: attachment.metadata.type
});
},
icon: "upload"
@@ -117,29 +98,28 @@ const Actions = ({
setLoading({
name: "Run file check"
});
const res = await filesystem.checkAttachment(attachment.hash);
let res = await filesystem.checkAttachment(attachment.metadata.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastManager.show({
ToastEvent.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(undefined);
db.attachments.markAsFailed(attachment.id);
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
}
setAttachments();
setAttachments([...db.attachments.all]);
setLoading({
name: undefined
name: null
});
},
icon: "file-check"
@@ -148,20 +128,19 @@ const Actions = ({
name: "Rename",
onPress: () => {
presentDialog({
context: contextId as any,
context: contextId,
input: true,
title: "Rename file",
paragraph: "Enter a new name for the file",
defaultValue: attachment.filename,
defaultValue: attachment.metadata.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.hash,
hash: attachment.metadata.hash,
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
setAttachments([...db.attachments.all]);
}
},
positiveText: "Rename"
@@ -172,40 +151,34 @@ const Actions = ({
{
name: "Delete",
onPress: async () => {
const relations = await db.relations.to(attachment, "note").get();
await db.attachments.remove(attachment.hash, false);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
relations
.map((relation) => relation.fromId)
.forEach(async (id) => {
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
const isFocused = useTabStore.getState().currentTab === tab;
if (isFocused) {
eSendEvent(eOnLoadNote, {
item: await db.notes.note(id),
forced: true
});
} else {
editorController.current.commands.setLoading(true, tab);
}
}
});
close?.();
await db.attachments.remove(attachment.metadata.hash, false);
setAttachments([...db.attachments.all]);
eSendEvent(eCloseSheet, contextId);
},
icon: "delete-outline"
}
];
const getNotes = useCallback(() => {
let allNotes = db.notes.all;
let attachmentNotes = attachment.noteIds?.map((id) => {
let index = allNotes?.findIndex((note) => id === note.id);
if (index !== -1) {
return allNotes[index];
} else {
return {
type: "notfound",
title: `Note with id ${id} does not exist.`,
id: id
};
}
});
return attachmentNotes;
}, [attachment.noteIds]);
useEffect(() => {
db.relations
.to(attachment, "note")
.selector.items()
.then((items) => {
setNotes(items);
});
}, [attachment]);
setNotes(getNotes());
}, [attachment, getNotes]);
return (
<ScrollView
@@ -221,7 +194,7 @@ const Actions = ({
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
borderBottomColor: colors.secondary.background,
marginBottom: notes && notes.length > 0 ? 0 : 12
}}
>
@@ -248,7 +221,7 @@ const Actions = ({
}}
color={colors.secondary.paragraph}
>
{attachment.type}
{attachment.metadata.type}
</Paragraph>
<Paragraph
style={{
@@ -257,10 +230,10 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{formatBytes(attachment.size)}
{formatBytes(attachment.length)}
</Paragraph>
{notes.length ? (
{attachment.noteIds ? (
<Paragraph
style={{
marginRight: 10
@@ -268,14 +241,14 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notes.length} note
{notes.length > 1 ? "s" : ""}
{attachment.noteIds.length} note
{attachment.noteIds.length > 1 ? "s" : ""}
</Paragraph>
) : null}
<Paragraph
onPress={() => {
Clipboard.setString(attachment.hash);
ToastManager.show({
Clipboard.setString(attachment.metadata.hash);
ToastEvent.show({
type: "success",
heading: "Attachment hash copied",
context: "local"
@@ -284,7 +257,7 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.hash}
{attachment.metadata.hash}
</Paragraph>
</View>
@@ -295,7 +268,7 @@ const Actions = ({
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
borderBottomColor: colors.secondary.background,
marginBottom: 12,
paddingVertical: 12
}}
@@ -311,15 +284,25 @@ const Actions = ({
</Heading>
{notes.map((item) => (
<Pressable
<PressableButton
onPress={async () => {
if (item.type === "notfound") {
ToastEvent.show({
heading: "Note not found",
message:
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
type: "error",
context: "local"
});
return;
}
eSendEvent(eCloseSheet, contextId);
await sleep(150);
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
openNote(item, (item as any).type === "trash");
openNote(item, item.type === "trash");
}}
style={{
customStyle={{
paddingVertical: 12,
alignItems: "flex-start",
@@ -328,7 +311,7 @@ const Actions = ({
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
</Pressable>
</PressableButton>
))}
</>
</View>
@@ -338,16 +321,17 @@ const Actions = ({
<Button
key={item.name}
buttonType={{
text:
item.name === "Delete"
? colors.error.paragraph
: colors.primary.paragraph
text: item.on
? colors.primary.accent
: item.name === "Delete" || item.name === "PermDelete"
? colors.error.paragraph
: colors.primary.paragraph
}}
onPress={item.onPress}
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type="plain"
type={item.on ? "shade" : "gray"}
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -375,20 +359,11 @@ const Actions = ({
);
};
Actions.present = (
attachment: Attachment,
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
context?: string
) => {
Actions.present = (attachment, set, context) => {
presentSheet({
context: context,
component: (ref, close) => (
<Actions
fwdRef={ref}
setAttachments={set}
close={close}
attachment={attachment}
/>
component: (ref) => (
<Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
)
});
};

View File

@@ -0,0 +1,171 @@
/*
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 { formatBytes } from "@notesnook/common";
import React 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 { 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";
function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
}) => {
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const onPress = () => {
if (!pressable) return;
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.secondary.background
}}
type="grayBg"
>
<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={{
position: "absolute"
}}
>
{getFileExtension(attachment.metadata.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.metadata.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{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.metadata.hash);
setCurrentProgress(null);
}}
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

@@ -1,191 +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 { formatBytes } from "@notesnook/common";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React 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 { 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";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
id,
attachments,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
}: {
id: string | number;
attachments?: VirtualizedGrouping<Attachment>;
encryption?: boolean;
setAttachments: (attachments: any) => void;
pressable?: boolean;
hideWhenNotDownloading?: boolean;
context?: string;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const onPress = () => {
if (!pressable || !attachment) return;
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.secondary.background,
minHeight: 45
}}
>
{!attachment ? null : (
<>
<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={{
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>
{!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}
</>
)}
</>
)}
</TouchableOpacity>
);
};

View File

@@ -21,10 +21,7 @@ import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import {
PresentSheetOptions,
presentSheet
} from "../../services/event-manager";
import { presentSheet } from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -32,19 +29,8 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeColors } from "@notesnook/theme";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
const DownloadAttachments = ({
close,
attachments,
isNote,
update
}: {
attachments: VirtualizedGrouping<Attachment>;
close?: ((ctx?: string | undefined) => void) | undefined;
isNote?: boolean;
update?: (props: PresentSheetOptions) => void;
}) => {
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
@@ -53,58 +39,63 @@ const DownloadAttachments = ({
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef<string>();
const groupId = useRef();
const onDownload = async () => {
update?.({
update({
disableClosing: true
} as PresentSheetOptions);
});
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
await attachments.ids(),
(progress: number, statusText: string) =>
setProgress({ value: progress, statusText }),
attachments,
(progress, statusText) => setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update?.({
update({
disableClosing: false
} as PresentSheetOptions);
});
};
const cancel = async () => {
update?.({
update({
disableClosing: false
} as PresentSheetOptions);
});
canceled.current = true;
if (!groupId.current) return;
console.log(groupId.current, "canceling groupId downloads");
await db.fs().cancel(groupId.current);
await db.fs.cancel(groupId.current);
setDownloading(false);
setResult(new Map());
groupId.current = undefined;
groupId.current = null;
};
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.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments?.placeholders.length
attachments.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
@@ -166,11 +157,7 @@ const DownloadAttachments = ({
width={null}
animated={true}
useNativeDriver
progress={
progress.value
? progress.value / attachments.placeholders?.length
: 0
}
progress={progress.value ? progress.value / attachments.length : 0}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
@@ -187,7 +174,7 @@ const DownloadAttachments = ({
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.placeholders : undefined}
data={downloading ? attachments : []}
ListEmptyComponent={
<View
style={{
@@ -202,15 +189,14 @@ const DownloadAttachments = ({
</Paragraph>
</View>
}
keyExtractor={(index) => "attachment_download" + index}
renderItem={({ index }) => {
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<AttachmentItem
id={index}
attachment={item}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
@@ -223,9 +209,7 @@ const DownloadAttachments = ({
borderRadius: 100,
marginTop: 20
}}
onPress={() => {
close?.();
}}
onPress={close}
type="accent"
title="Done"
/>
@@ -243,10 +227,8 @@ const DownloadAttachments = ({
borderRadius: 100,
marginRight: 5
}}
onPress={() => {
close?.();
}}
type="secondary"
onPress={close}
type="grayBg"
title="No"
/>
<Button
@@ -276,11 +258,7 @@ const DownloadAttachments = ({
);
};
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
DownloadAttachments.present = (context, attachments, isNote) => {
presentSheet({
context: context,
component: (ref, close, update) => (

View File

@@ -17,25 +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 {
Attachment,
Note,
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
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 filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
@@ -43,92 +34,79 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/dist/utils/filename";
import { useSettingStore } from "../../stores/use-setting-store";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: Note }) => {
export const AttachmentDialog = ({ note }) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const searchTimer = useRef<NodeJS.Timeout>();
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const refresh = React.useCallback(() => {
if (note) {
db.attachments
.ofNote(note.id, "all")
.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => {
setAttachments(attachments);
});
} else {
db.attachments.all
.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => setAttachments(attachments));
}
}, [note]);
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
useEffect(() => {
refresh();
}, [note, refresh]);
const onChangeText = async (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments(await filterAttachments(currentFilter));
refresh();
setAttachments([...attachments]);
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(async () => {
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
attachmentSearchValue.current
);
if (results.length === 0) return;
setAttachments(results);
}, 300);
};
const renderItem = ({ index }: { item: boolean; index: number }) => (
const renderItem = ({ item }) => (
<AttachmentItem
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
}}
attachments={attachments}
id={index}
setAttachments={setAttachments}
attachment={item}
context="attachments-list"
/>
);
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
for (let i = 0; i < attachments.placeholders.length; i++) {
const attachment = (await attachments.item(i))?.item;
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
await db.attachments.markAsFailed(attachment.hash, result.failed);
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
await db.attachments.markAsFailed(attachment.id, undefined);
await db.attachments.markAsFailed(attachment.id, null);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
refresh();
setLoading(false);
};
@@ -155,40 +133,31 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
}
];
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
const filterAttachments = (type) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
switch (type) {
case "all":
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.all;
break;
return attachments;
case "images":
items = note
? db.attachments.ofNote(note.id, "images")
: db.attachments.images;
break;
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
case "video":
items = items = note
? db.attachments.ofNote(note.id, "videos")
: db.attachments.videos;
break;
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
case "audio":
items = note
? db.attachments.ofNote(note.id, "audio")
: db.attachments.all;
break;
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
case "documents":
items = note
? db.attachments.ofNote(note.id, "documents")
: db.attachments.documents;
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
}
return await items.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
});
};
return (
@@ -227,7 +196,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
) : (
<IconButton
name="check-all"
style={{
customStyle={{
height: 40,
width: 40,
marginRight: 10
@@ -240,13 +209,12 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
<IconButton
name="download"
style={{
customStyle={{
height: 40,
width: 40
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
@@ -263,7 +231,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
onChangeText(attachmentSearchValue.current);
}}
/>
@@ -283,11 +251,12 @@ 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}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
style={{
borderRadius: 0,
borderBottomWidth: 1,
@@ -297,9 +266,9 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
? "transparent"
: colors.primary.accent
}}
onPress={async () => {
onPress={() => {
setCurrentFilter(item.filterBy);
setAttachments(await filterAttachments(item.filterBy));
setAttachments(filterAttachments(item.filterBy));
}}
/>
))}
@@ -330,7 +299,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
/>
}
estimatedItemSize={50}
data={attachments?.placeholders}
data={attachments}
renderItem={renderItem}
/>
@@ -353,7 +322,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
);
};
AttachmentDialog.present = (note?: Note) => {
AttachmentDialog.present = (note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});

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}
@@ -94,14 +85,11 @@ const AuthModal = () => {
background={colors.primary.background}
transparent={false}
animated={false}
centered={false}
enableSheetKeyboardHandler
>
<KeyboardAwareScrollView
style={{
width: "100%"
}}
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
{currentAuthMode !== AuthMode.login ? (
@@ -158,7 +146,7 @@ const AuthModal = () => {
hideAuth();
}}
iconSize={16}
type="plain"
type="gray"
iconPosition="right"
icon="chevron-right"
height={25}

View File

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

View File

@@ -22,7 +22,7 @@ import { View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { ToastEvent } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useThemeColors } from "@notesnook/theme";
import DialogHeader from "../dialog/dialog-header";
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastManager.show({
ToastEvent.show({
heading: "Account email is required.",
type: "error",
context: "local"
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastManager.show({
ToastEvent.show({
heading: "Check your email to reset password",
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
type: "success",
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
setSent(true);
} catch (e) {
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Recovery email not sent",
message: e.message,
type: "error",
@@ -113,7 +113,7 @@ export const ForgotPassword = () => {
}}
>
<IconButton
style={{
customStyle={{
width: null,
height: null
}}

View File

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

View File

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

View File

@@ -19,12 +19,12 @@ 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";
import {
ToastManager,
ToastEvent,
eSendEvent,
eSubscribeEvent
} from "../../services/event-manager";
@@ -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";
@@ -69,11 +68,7 @@ export const SessionExpired = () => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
}
);
const logout = async () => {
@@ -87,11 +82,8 @@ export const SessionExpired = () => {
MMKV.clearStore();
clearAllStores();
setVisible(false);
useUserStore.setState({
disableAppLockRequests: false
});
} catch (e) {
ToastManager.show({
ToastEvent.show({
heading: e.message,
type: "error",
context: "local"
@@ -108,9 +100,9 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.tokenManager.getToken();
let res = await db.user.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
if (db.user.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
if (!complete) {
@@ -133,34 +125,20 @@ export const SessionExpired = () => {
email.current = user.email;
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [email]);
return (
visible && (
<BaseDialog
transparent={false}
background={colors.primary.background}
bounce={false}
animated={false}
centered={false}
<Modal
onShow={async () => {
useUserStore.setState({
disableAppLockRequests: true
});
await sleep(300);
passwordInputRef.current?.focus();
setFocused(true);
useUserStore.setState({
disableAppLockRequests: true
});
}}
enableSheetKeyboardHandler={true}
visible={true}
>
<SheetProvider context="two_factor_verify" />
<View
style={{
width: focused ? "100%" : "99.9%",
@@ -181,7 +159,7 @@ export const SessionExpired = () => {
}}
>
<IconButton
style={{
customStyle={{
width: 60,
height: 60
}}
@@ -253,9 +231,7 @@ export const SessionExpired = () => {
</View>
<Toast context="local" />
<Dialog context="session_expiry" />
<SheetProvider context="two_factor_verify" />
</BaseDialog>
</Modal>
)
);
};

View File

@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { ToastEvent } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -52,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
const { width, height } = useWindowDimensions();
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
ToastEvent.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -85,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
}
} catch (e) {
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Signup failed",
message: e.message,
type: "error",
@@ -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

@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
@@ -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";
@@ -138,7 +138,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
setSending(false);
} catch (e) {
setSending(false);
ToastManager.error(e, "Error sending 2FA Code", "local");
ToastEvent.error(e, "Error sending 2FA Code", "local");
}
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
@@ -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

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useRef, useState } from "react";
import { db } from "../../common/database";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import { clearMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -33,7 +33,7 @@ export const LoginSteps = {
passwordAuth: 3
};
export const useLogin = (onFinishLogin, sessionExpired = false) => {
export const useLogin = (onFinishLogin) => {
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
(!password.current && step === LoginSteps.passwordAuth) ||
(!email.current && step === LoginSteps.emailAuth)
) {
ToastManager.show({
ToastEvent.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -69,7 +69,6 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
switch (step) {
case LoginSteps.emailAuth: {
const mfaInfo = await db.user.authenticateEmail(email.current);
console.log("email auth", mfaInfo);
if (mfaInfo) {
TwoFactorVerification.present(async (mfa, callback) => {
try {
@@ -102,12 +101,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
break;
}
case LoginSteps.passwordAuth: {
await db.user.authenticatePassword(
email.current,
password.current,
null,
sessionExpired
);
await db.user.authenticatePassword(email.current, password.current);
finishLogin();
break;
}
@@ -121,7 +115,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const finishWithError = async (e) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Login failed",
message: e.message,
type: "error",
@@ -135,7 +129,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastManager.show({
ToastEvent.show({
heading: "Login successful",
message: `Logged in as ${user.email}`,
type: "success",

View File

@@ -17,19 +17,24 @@ 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 { path } from "@notesnook-importer/core/dist/src/utils/path";
import React, { PropsWithChildren } from "react";
import { View } from "react-native";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
export function makeUniqueFilename(
filePath: string,
counters: Record<string, number>
) {
const matchablePath = filePath.toLowerCase();
const count = (counters[matchablePath] = (counters[matchablePath] || 0) + 1);
if (count === 1) return filePath;
export const ContainerHeader = (props: PropsWithChildren) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const ext = path.extname(filePath);
const basename = ext
? `${path.basename(filePath, ext)}-${count}${ext}`
: `${path.basename(filePath)}-${count}`;
return path.join(path.dirname(filePath), basename);
}
return !selectionMode ? (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
overflow: "hidden"
}}
>
{props.children}
</View>
) : null;
};

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import { useRoute } from "@react-navigation/native";
import React, { useCallback, useEffect } from "react";
import { Keyboard, View } from "react-native";
import Animated, {
@@ -35,13 +34,12 @@ 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;
interface FloatingButton {
title?: string;
onPress: () => void;
color?: string;
shouldShow?: boolean;
alwaysVisible?: boolean;
}
@@ -50,12 +48,11 @@ const FloatingButton = ({
onPress,
color,
alwaysVisible = false
}: FloatingButtonProps) => {
}: FloatingButton) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const translate = useSharedValue(0);
const route = useRoute();
const animatedStyle = useAnimatedStyle(() => {
return {
@@ -118,11 +115,11 @@ const FloatingButton = ({
animatedStyle
]}
>
<Pressable
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={color}
style={{
accentColor={colors.static[color as keyof typeof colors.static]}
customStyle={{
...getElevationStyle(5),
borderRadius: 100
}}
@@ -142,18 +139,12 @@ const FloatingButton = ({
}}
>
<Icon
name={
route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
name={title === "Clear all trash" ? "delete" : "plus"}
color={colors.primary.accentForeground}
size={SIZE.xxl}
/>
</View>
</Pressable>
</PressableButton>
</Animated.View>
);
};

View File

@@ -17,49 +17,41 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
import React, { PropsWithChildren } from "react";
import { KeyboardAvoidingView, Platform } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useKeyboard from "../../hooks/use-keyboard";
import { useThemeColors } from "@notesnook/theme";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { Header } from "../header";
import SelectionHeader from "../selection-header";
export const Container = ({ children }: PropsWithChildren) => {
const { colors } = useThemeColors();
const floating = useIsFloatingKeyboard();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const insets = useGlobalSafeAreaInsets();
const keyboard = useKeyboard();
const [height, setHeight] = useState(0);
const windowHeightRef = useRef(Dimensions.get("window").height);
const { height: windowHeight } = useWindowDimensions();
const timerRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (windowHeight !== windowHeightRef.current) {
setHeight(0);
windowHeightRef.current = windowHeight;
}
}, [windowHeight]);
return (
<View
<KeyboardAvoidingView
behavior="padding"
enabled={Platform.OS === "ios" && !floating}
style={{
flex: 1,
overflow: "hidden",
paddingTop: Platform.OS === "android" ? 0 : insets.top,
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom,
height: height || "100%",
width: "100%",
backgroundColor: colors.primary.background
}}
onLayout={(event) => {
const height = event.nativeEvent.layout.height;
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
if (!keyboard.keyboardShown) {
setHeight(height);
}
}, 500);
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
}}
>
{!introCompleted ? null : (
<>
<SelectionHeader />
<Header />
</>
)}
{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

@@ -17,15 +17,15 @@ 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 { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { AddTopicDialog } from "../dialogs/add-topic";
import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview";
@@ -36,16 +36,17 @@ 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";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => {
const { colors } = useThemeColors();
const loading = useNoteStore((state) => state.loading);
return (
<>
<AppLockPassword />
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -57,11 +58,10 @@ const DialogProvider = () => {
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<Expiring />
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
</>
);
};

View File

@@ -21,9 +21,11 @@ import { eSendEvent } from "../../services/event-manager";
import {
eCloseActionSheet,
eCloseAddNotebookDialog,
eCloseAddTopicDialog,
eCloseMoveNoteDialog,
eOpenActionSheet,
eOpenAddNotebookDialog,
eOpenAddTopicDialog,
eOpenMoveNoteDialog
} from "../../utils/events";
@@ -50,3 +52,9 @@ export const AddNotebookEvent = (notebook) => {
export const HideAddNotebookEvent = (notebook) => {
eSendEvent(eCloseAddNotebookDialog, notebook);
};
export const AddTopicEvent = (topic) => {
eSendEvent(eOpenAddTopicDialog, topic);
};
export const HideAddTopicEvent = (notebook) => {
eSendEvent(eCloseAddTopicDialog, notebook);
};

View File

@@ -17,9 +17,8 @@ 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 React, { useEffect } from "react";
import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
@@ -32,28 +31,6 @@ import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { BouncingView } from "../ui/transitions/bouncing-view";
import { ScopedThemeProvider } from "@notesnook/theme";
import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { useAppState } from "../../hooks/use-app-state";
export interface BaseDialogProps extends PropsWithChildren {
animation?: "fade" | "none" | "slide" | undefined;
visible: boolean;
onRequestClose?: () => void;
onShow?: () => void;
premium?: boolean;
statusBarTranslucent?: boolean;
transparent?: boolean;
centered?: boolean;
bottom?: boolean;
background?: string | ColorValue;
animated?: boolean;
bounce?: boolean;
closeOnTouch?: boolean;
useSafeArea?: boolean;
avoidKeyboardResize?: boolean;
enableSheetKeyboardHandler?: boolean;
}
const BaseDialog = ({
visible,
@@ -66,18 +43,14 @@ const BaseDialog = ({
transparent,
centered = true,
bottom = false,
background = null,
animated = true,
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false,
enableSheetKeyboardHandler,
background
}: BaseDialogProps) => {
avoidKeyboardResize = false
}) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
const lockEvents = useRef(false);
const [internalVisible, setIntervalVisible] = useState(true);
useEffect(() => {
return () => {
@@ -85,35 +58,14 @@ const BaseDialog = ({
};
}, []);
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
setIntervalVisible(false);
if (useUserStore.getState().appLocked) {
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
setIntervalVisible(true);
unsub();
setTimeout(() => {
lockEvents.current = false;
});
}
});
}
}
}
}, [appState]);
const Wrapper = useSafeArea ? SafeAreaView : View;
return (
<ScopedThemeProvider value="dialog">
<Modal
visible={visible && internalVisible}
visible={visible}
transparent={true}
animated
statusBarTranslucent={statusBarTranslucent}
supportedOrientations={[
"portrait",
@@ -123,17 +75,13 @@ const BaseDialog = ({
"landscape-right"
]}
onShow={() => {
if (lockEvents.current) return;
if (onShow) {
onShow();
if (!enableSheetKeyboardHandler) {
useSettingStore.getState().setSheetKeyboardHandler(false);
}
useSettingStore.getState().setSheetKeyboardHandler(false);
}
}}
animationType={animation}
onRequestClose={() => {
if (lockEvents.current) return;
if (!closeOnTouch) return null;
useSettingStore.getState().setSheetKeyboardHandler(true);
onRequestClose && onRequestClose();
@@ -159,7 +107,6 @@ const BaseDialog = ({
style={[
styles.backdrop,
{
alignItems: centered ? "center" : undefined,
justifyContent: centered
? "center"
: bottom
@@ -169,7 +116,7 @@ const BaseDialog = ({
]}
>
<TouchableOpacity
onPress={closeOnTouch ? onRequestClose : undefined}
onPress={closeOnTouch ? onRequestClose : null}
style={styles.overlayButton}
/>
{premium}
@@ -186,7 +133,8 @@ const styles = StyleSheet.create({
backdrop: {
width: "100%",
height: "100%",
justifyContent: "center"
justifyContent: "center",
alignItems: "center"
},
overlayButton: {
width: "100%",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,190 @@
/*
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, { createRef } from "react";
import { View } from "react-native";
import { useMenuStore } from "../../../stores/use-menu-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddTopicDialog,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogContainer from "../../dialog/dialog-container";
import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { Toast } from "../../toast";
import { useRelationStore } from "../../../stores/use-relation-store";
export class AddTopicDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
titleFocused: false,
loading: false
};
this.ref = createRef();
this.title;
this.titleRef = createRef();
this.notebook = null;
this.toEdit = null;
}
addNewTopic = async () => {
try {
if (!this.title || this.title?.trim() === "") {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return;
}
if (!this.toEdit) {
await db.notebooks.notebook(this.notebook.id).topics.add(this.title);
} else {
let topic = this.toEdit;
topic.title = this.title;
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
}
this.close();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
} catch (e) {
console.error(e);
}
};
componentDidMount() {
eSubscribeEvent(eOpenAddTopicDialog, this.open);
eSubscribeEvent(eCloseAddTopicDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
}
open = async ({ notebookId, toEdit }) => {
let id = notebookId;
if (id) {
this.notebook = await db.notebooks.notebook(id).data;
}
this.toEdit = toEdit;
if (this.toEdit) {
this.title = this.toEdit.title;
}
this.setState({
visible: true
});
};
close = () => {
this.title = null;
this.notebook = null;
this.toEdit = null;
this.setState({
visible: false
});
};
render() {
const { visible } = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
if (this.toEdit) {
this.titleRef.current?.setNativeProps({
text: this.toEdit.title
});
}
this.ref.current?.animateNextTransition();
await sleep(300);
this.titleRef.current?.focus();
}}
bounce={false}
statusBarTranslucent={false}
visible={true}
onRequestClose={this.close}
>
<DialogContainer>
<DialogHeader
icon="book-outline"
title={this.toEdit ? "Edit topic" : "New topic"}
paragraph={
this.toEdit
? "Edit title of the topic"
: "Add a new topic in " + this.notebook.title
}
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12,
zIndex: 10
}}
>
<Input
fwdRef={this.titleRef}
testID="input-title"
onChangeText={(value) => {
this.title = value;
}}
blurOnSubmit={false}
placeholder="Enter title"
onSubmit={() => this.addNewTopic()}
returnKeyLabel="Done"
returnKeyType="done"
/>
</View>
<DialogButtons
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
/>
</DialogContainer>
<Toast context="local" />
</BaseDialog>
);
}
}

View File

@@ -1,360 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import {
clearAppLockVerificationCipher,
setAppLockVerificationCipher,
validateAppLockPassword
} from "../../../common/database/encryption";
import 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`
),
undefined,
"local"
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometicService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(SettingsService.get());
ToastManager.show({
message: "App lock disabled",
type: "success"
});
}
}
SettingsService.setProperty(
"applockKeyboardType",
keyboardType === "password" ? "default" : "numeric"
);
close();
}}
positiveTitle="Save"
negativeTitle="Cancel"
positiveType="transparent"
loading={false}
doneText=""
/>
</View>
<Toast context="local" />
</BaseDialog>
);
};
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
eSendEvent(eOpenAppLockPasswordDialog, mode);
};

View File

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

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