Compare commits

..

1 Commits

Author SHA1 Message Date
ammarahm-ed
23d75c2e74 mobile: fix reminders not working on android 2023-03-02 13:21:46 +05:00
2185 changed files with 334765 additions and 465249 deletions

View File

@@ -14,16 +14,12 @@ const SCOPES = [
"mobile",
"web",
"vericrypt",
"desktop",
"crypto",
"editor",
"logger",
"theme",
"server",
"core",
"fs",
"ui",
"clipper",
"config",
"ci",
@@ -31,10 +27,7 @@ const SCOPES = [
"docs",
"refactor",
"misc",
"common",
"global",
"docs",
"themebuilder"
"global"
];
module.exports = {

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

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
* text=auto eol=lf

View File

@@ -1,26 +0,0 @@
name: "Setup node with cache action"
description: "Setups node with cache"
runs:
using: "composite"
steps:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: "npm"
cache-dependency-path: |
package-lock.json
apps/mobile/package-lock.json
apps/desktop/package-lock.json
apps/web/package-lock.json
extensions/web-clipper/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/sodium/package-lock.json
packages/clipper/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json

4
.github/dco.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
allowRemediationCommits:
individual: true
require:
members: false

View File

@@ -6,149 +6,59 @@ 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
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- uses: actions/setup-node@master
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "11"
architecture: "x64"
java-version: '11'
architecture: 'x64'
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
npm ci
- 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
keyPassword: ${{ secrets.KEY_PASSWORD }}
- 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}}
whatsNewDirectory: apps/mobile/native/android/releasenotes/

View File

@@ -1,4 +1,4 @@
name: Publish @notesnook/android
name: Publish @notesnook/mobile
on: workflow_dispatch
@@ -6,92 +6,62 @@ 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
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- uses: actions/setup-node@master
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "11"
architecture: "x64"
java-version: '11'
architecture: 'x64'
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
npm ci
- 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"
keyPassword: ${{ secrets.KEY_PASSWORD }}
- 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
@@ -104,9 +74,7 @@ jobs:
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"
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
- name: Rename apk files
run: |
@@ -122,18 +90,6 @@ jobs:
with:
path: apps/mobile
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -146,10 +102,4 @@ jobs:
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}}
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk

View File

@@ -8,7 +8,7 @@ on:
paths:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
- ".github/workflows/core.tests.yml"
pull_request:
types:
- "ready_for_review"
@@ -24,12 +24,25 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Prepare environment
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=core
npm ci --prefer-offline --no-audit
- name: Add environment variables
run: |
echo "USER_EMAIL=${{ secrets.USER_EMAIL }}" >> $GITHUB_ENV

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:
@@ -43,12 +28,26 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |
@@ -58,14 +57,14 @@ 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/**/*
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
@@ -82,24 +81,16 @@ 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
uses: actions/checkout@v3
- 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/
@@ -107,17 +98,16 @@ jobs:
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
run: |
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
echo ::set-output name=bundle_version::$(cat package.json | jq -r .build.mac.bundleVersion)
- 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 }}
@@ -125,20 +115,26 @@ jobs:
private-key-raw: ${{ secrets.api_key }}
platform: "MAC_OS"
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
path: ./apps/web/desktop/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
npm i
npm i dmg-license
working-directory: ./apps/web/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
- name: Build app for Mac App Store
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
@@ -146,9 +142,9 @@ jobs:
CSC_LINK: ${{ secrets.mac_certs }}
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
working-directory: ./apps/desktop
npm run build:mas
npx electron-builder --mac mas --universal -p never
working-directory: ./apps/web/desktop
- name: Build zip and dmg
env:
@@ -157,42 +153,38 @@ 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
npm run build
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
working-directory: ./apps/web/desktop
- name: Deploy to Testflight
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
env:
API_KEY_ID: ${{ secrets.api_key_id }}
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
run: |
package=$(find ./output/mas-universal/notesnook*.pkg)
run: |
package=$(find ./dist/mas-universal/notesnook*.pkg)
echo "Uploading ${package} using altool..."
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Snapcraft Auth
if: inputs.publish-snap
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
@@ -200,91 +192,84 @@ jobs:
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v1
if: inputs.publish-snap
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
path: ./apps/web/desktop/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
run: npm i
working-directory: ./apps/web/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run build
working-directory: ./apps/web/desktop
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --linux snap:x64 -p never
working-directory: ./apps/desktop
npx electron-builder --linux snap -p never
working-directory: ./apps/web/desktop
- name: Build AppImage
- name: Build AppImage deb and rpm
env:
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 deb rpm -p always
else
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
npx electron-builder --linux AppImage deb rpm -p never
fi
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
- name: Publish on Snapcraft
if: inputs.publish-snap
run: |
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
working-directory: ./apps/desktop
snapcraft upload --release=stable ./dist/notesnook_linux_amd64.snap
working-directory: ./apps/web/desktop
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
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
path: ./apps/web/desktop/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
run: npm i
working-directory: ./apps/web/desktop
- name: Build
run: npx nx run release --project @notesnook/desktop
- name: Build Electron wrapper
run: npm run build
working-directory: ./apps/web/desktop
- name: Publish
- name: Build app
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
npx electron-builder --win --publish always
} else {
yarn electron-builder --win --publish never
npx electron-builder --win --publish never
}
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop

View File

@@ -25,19 +25,30 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=editor
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Build editor
run: npx nx build @notesnook/editor
- name: Run all @notesnook/editor tests
run: npx nx test @notesnook/editor

View File

@@ -1,35 +0,0 @@
name: Publish Notesnook Help
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "docs/help/**"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install docgen
run: cargo install --git https://github.com/thecodrr/docgen
- name: Build site
run: docgen build --release
working-directory: docs/help
- name: Setup environment
run: |
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Publish on Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name notesnook-help ./docs/help/site/ --branch main

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

@@ -11,4 +11,4 @@ jobs:
steps:
- name: Run ats-diagnostics
run: nscurl --ats-diagnostics https://payments.streetwriters.co
run: nscurl --ats-diagnostics https://payments.streetwriters.co

View File

@@ -1,31 +0,0 @@
name: Publish @notesnook/theme-builder
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=themebuilder
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate build
run: npm run build:theme-builder
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-theme-builder ./apps/theme-builder/build/

View File

@@ -1,31 +0,0 @@
name: Publish @notesnook/vericrypt
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=vericrypt
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate build
run: npm run build:vericrypt
- name: Publish on Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=vericrypt ./apps/vericrypt/build/

View File

@@ -11,12 +11,26 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |
@@ -24,8 +38,8 @@ jobs:
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate build
- name: Generate desktop build
run: npm run build:web
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/
- name: Publish on Cloudflare Pages
run: npx --yes wrangler pages publish --project-name=notesnook-app ./apps/web/build/

View File

@@ -25,12 +25,26 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |
@@ -62,12 +76,26 @@ jobs:
path: ./apps/web/build
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm install -D @playwright/test otplib dayjs @zip.js/zip.js
npm i --ignore-scripts --prefer-offline --no-audit
npm install -D @playwright/test otplib
- name: Validate build extraction
run: ls -ld ./apps/web/build/index.html

5
.gitignore vendored
View File

@@ -7,7 +7,4 @@ nx-cloud.env
.idea
.eslintcache
.env.local
.nyc_output
site
node_modules.backup
.nx
.nyc_output

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

@@ -5,6 +5,5 @@ module.exports = {
printWidth: 80,
useTabs: false,
tabWidth: 2,
bracketSpacing: true,
endOfLine: "lf"
bracketSpacing: true
};

72
.vscode/notesnook.code-workspace vendored Normal file
View File

@@ -0,0 +1,72 @@
{
"folders": [
{
"name": "✨ notesnook",
"path": ".."
},
{
"name": "🚀 @notesnook/mobile",
"path": "../apps/mobile"
},
{
"name": "🚀 @notesnook/web",
"path": "../apps/web"
},
{
"name": "🚀 @notesnook/desktop",
"path": "../apps/web/desktop"
},
{
"name": "🚀 @notesnook/web-clipper",
"path": "../extensions/web-clipper"
},
{
"name": "📦 @notesnook/clipper",
"path": "../packages/clipper"
},
{
"name": "📦 @notesnook/core",
"path": "../packages/core"
},
{
"name": "📦 @notesnook/crypto",
"path": "../packages/crypto"
},
{
"name": "📦 @notesnook/crypto-worker",
"path": "../packages/crypto-worker"
},
{
"name": "📦 @notesnook/editor",
"path": "../packages/editor"
},
{
"name": "📦 @notesnook/editor-mobile",
"path": "../packages/editor-mobile"
},
{
"name": "📦 @notesnook/logger",
"path": "../packages/logger"
},
{
"name": "📦 @notesnook/streamable-fs",
"path": "../packages/streamable-fs"
},
{
"name": "📦 @notesnook/theme",
"path": "../packages/theme"
}
],
"settings": {
"typescript.tsdk": "node_modules/typescript/lib",
"javascript.format.enable": true,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modificationsIfAvailable",
"eslint.packageManager": "npm",
"eslint.run": "onSave",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.execArgv": ["--cache"]
}
}

15
.vscode/settings.json vendored
View File

@@ -1,15 +0,0 @@
{
"files.eol": "\n",
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"eslint.run": "onSave",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.options": {
"cache": true,
"cacheStrategy": "content"
},
"git.rebaseWhenSync": true
}

View File

@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
Ugh! Bugs!
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
@@ -30,7 +30,7 @@ Before you open a new feature request, please make sure it's not a duplicate. **
### Helping out in the issue tracker
New issues are always opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
There are always new issues getting opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
In addition to this, you can help out in the following ways:
@@ -62,7 +62,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
2. Run `npm run bootstrap` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
3. If youve fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`npm run test`).
5. Format your code with prettier (`npm run prettier`).
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
@@ -103,7 +103,7 @@ All commits must include valid scopes in the commit message. **Valid commit scop
**Packages:**
1. `crypto`: changes related to the cryptographic core
1. `crypto`: changes related to the cryptographic core (including `@notesnook/crypto-worker`)
2. `editor`: changes related to the editor (including `@notesnook/editor-mobile`)
3. `logger`: changes related to the logger
4. `theme`: changes related to the theme
@@ -131,11 +131,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnbs Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
## Git Branch Organization
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We dont use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
1. All tests must be passing at all times
2. There should be as few breaking changes as possible

View File

@@ -10,11 +10,11 @@
## Overview
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
## Developer guide
@@ -25,29 +25,25 @@ Notesnook is built using the following technologies:
1. JavaScript/Typescript — this repo is in a hybrid state. A lot of the newer code is being written in Typescript & the old code is slowly being ported over.
2. React — the whole front-end across all platforms is built using React.
3. React Native — For mobile apps we are using React Native
4. Electron — For desktop app
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
6. Nx — maintaining monorepos is hard but Nx makes it easier.
4. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
5. Lerna & Nx — maintaining monorepos is hard but Nx makes it easier.
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
### Monorepo structure
| Name | Path | Description |
| -------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
| `@notesnook/web` | [/apps/web](/apps/web) | Web client |
| `@notesnook/desktop` | [/apps/desktop](/apps/desktop) | Desktop client |
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
| `@notesnook/web-clipper` | [/extensions/web-clipper](/extensions/web-clipper) | Web clipper |
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
| `@notesnook/clipper` | [/packages/clipper](/packages/clipper) | Web clipper core handling everything related to actual page clipping |
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
| `@notesnook/sodium` | [/packages/sodium](/packages/sodium) | Wrapper around libsodium to support Node.js & Browser |
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
| Name | Path | Description |
| -------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
| `@notesnook/web` | [/apps/web](/apps/web) | Web/Desktop clients |
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
### Contributing guidelines

View File

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

View File

@@ -1,58 +0,0 @@
<p align="center">
<img style="align:center;" src="/resources/screenshots/web.jpg" alt="Notesnook desktop screenshot" width="600" />
</p>
<h1 align="center">Notesnook Desktop</h1>
<h3 align="center">The desktop app is built using Electron & Typescript.</h3>
<p align="center">
<a href="https://notesnook.com/downloads">Downloads</a> | <a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
</p>
## Getting started
## Build instructions
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
### Setting up the development environment
Requirements:
1. [Node.js](https://nodejs.org/en/download/)
2. [git](https://git-scm.com/downloads)
3. NPM (not yarn or pnpm)
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
Once you have completed the setup, the first step is to `clone` the monorepo:
```bash
git clone https://github.com/streetwriters/notesnook.git
# change directory
cd notesnook
```
Once you are inside the `./notesnook` directory, run the preparation step:
```bash
# this might take a while to complete
npm install
```
Now you can finally start the desktop app:
```bash
npm run start:desktop
```
## Developer guide
### The tech stack
We try to keep the stack as lean as possible:
1. Electron
2. tRPC: for cross-communication between the web & desktop parts
3. zod: for runtime typechecking
4. yargs: for CLI argument parsing

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,58 +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/>.
*/
/* eslint-disable no-var */
import { BrowserWindow } from "electron";
import {
type FormData as FormDataType,
type Headers as HeadersType,
type Request as RequestType,
type Response as ResponseType
} from "undici";
declare global {
var window: BrowserWindow | null;
var RELEASE: boolean;
var MAC_APP_STORE: boolean;
// Re-export undici fetch function and various classes to global scope.
// These are classes and functions expected to be at global scope according to Node.js v18 API
// documentation.
// See: https://nodejs.org/dist/latest-v18.x/docs/api/globals.html
// eslint-disable-next-line no-var
export var {
FormData,
Headers,
Request,
Response,
fetch
}: typeof import("undici");
type FormData = FormDataType;
type Headers = HeadersType;
type Request = RequestType;
type Response = ResponseType;
}
// NOTE: the import in the global block above needs to be a var for this to work properly.
globalThis.fetch = fetch;
globalThis.FormData = FormData;
globalThis.Headers = Headers;
globalThis.Request = Request;
globalThis.Response = Response;

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +0,0 @@
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
index 379cf3b..2644e5d 100644
--- a/node_modules/electron-trpc/dist/main.mjs
+++ b/node_modules/electron-trpc/dist/main.mjs
@@ -221,9 +221,16 @@ class G {
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
}
detachWindow(r) {
+
y(this, c, i(this, c).filter((n) => n !== r));
- for (const [n, t] of i(this, u).entries())
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ for (const [n, t] of i(this, u).entries()) {
+ try {
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ } catch(e) {
+ console.error(e);
+ // ignore
+ }
+ }
}
}
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {

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

@@ -1,103 +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 path from "path";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
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 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, "..", "..", "..")
);
}
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
if (os.platform() === "win32")
await exec(
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
path.join(__dirname, "..", "node_modules", "sodium-native")
);
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
});
if (args.variant === "mas") {
await exec(`npm run bundle:mas`);
} else {
await exec(`npm run bundle`);
}
await exec(`npx tsc`);
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`);
} else {
await exec(`./output/linux-unpacked/Notesnook`);
}
}
async function exec(cmd, cwd) {
return childProcess.execSync(cmd, {
env: { ...process.env, NOTESNOOK_STAGING: true },
stdio: "inherit",
cwd: cwd || process.cwd()
});
}
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

@@ -1,188 +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 path from "path";
import fs from "fs/promises";
import chokidar from "chokidar";
import { execSync, spawn } from "child_process";
import { fileURLToPath } from "url";
import treekill from "tree-kill";
import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
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...");
const watcher = chokidar.watch("src/", { ignoreInitial: true });
watcher.on("all", () => {
onChange(false);
});
process.on("SIGINT", async (s) => {
await cleanup();
});
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`);
execAsync(`npx`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
return;
}
if (first) {
await spawnAndWaitUntil(
["npx", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
}
if (!first) {
console.log("Restarting...", RESTARTABLE_PROCESSES.length);
await killProcesses(RESTARTABLE_PROCESSES);
}
execAsync(
"npx",
["electron", path.join("build", "electron.js")],
true,
cleanup
);
}
function spawnAndWaitUntil(cmd, cwd, predicate) {
return new Promise((resolve) => {
console.log(">", ...cmd);
const s = spawn(cmd[0], cmd.slice(1), {
cwd,
env: ENV,
shell: false
});
RUNNING_PROCESSES.push(s);
s.stderr.pipe(process.stderr);
s.stdout.on("data", (data) => {
process.stdout.write(data);
if (predicate(data)) resolve(undefined); //
});
});
}
async function exec(cmd, cwd) {
try {
console.log(">", cmd);
return execSync(cmd, {
env: ENV,
stdio: "inherit",
shell: false,
cwd: cwd || process.cwd()
});
} catch {
//ignore
}
}
function execAsync(cmd, args, restartable, onExit) {
try {
console.log(">", cmd, ...args);
const proc = spawn(cmd, args, {
stdio: "inherit",
env: ENV,
shell: false
});
const array = restartable ? RESTARTABLE_PROCESSES : RUNNING_PROCESSES;
array.push(proc);
proc.on("exit", (code, signal) => {
console.log(cmd, ...args, "closed with code", code);
if (code === 0 && !signal) {
array.splice(array.indexOf(proc), 1);
onExit && onExit();
}
});
} catch {
//ignore
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function killProcesses(processes) {
for (const process of processes.slice()) {
processes.splice(processes.indexOf(process), 1);
await new Promise((resolve, reject) =>
treekill(process.pid, (err) => (err ? reject(err) : resolve()))
);
}
}
async function cleanup() {
console.log("Cleaning up");
await killProcesses(RESTARTABLE_PROCESSES);
await killProcesses(RUNNING_PROCESSES);
await sleep(1000);
await fs.rm("./build/", { force: true, recursive: true });
process.exit();
}
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 hashSum = crypto.createHash("sha256");
hashSum.update(bundle);
const hex = hashSum.digest("hex");
if (!lastBundleHash) {
lastBundleHash = hex;
return false;
}
if (hex === lastBundleHash) return true;
lastBundleHash = hex;
return false;
}

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

@@ -1,58 +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";
import { EventEmitter } from "events";
import TypedEventEmitter from "typed-emitter";
export type AppEvents = {
onCreateItem(name: "note" | "notebook" | "reminder"): void;
};
const emitter = new EventEmitter();
const typedEmitter = emitter as TypedEventEmitter<AppEvents>;
const t = initTRPC.create();
export const bridgeRouter = t.router({
onCreateItem: createSubscription("onCreateItem")
});
export const bridge: AppEvents = new Proxy({} as AppEvents, {
get(_t, name) {
if (typeof name === "symbol") return;
return (...args: unknown[]) => {
emitter.emit(name, ...args);
};
}
});
function createSubscription<TName extends keyof AppEvents>(eventName: TName) {
return t.procedure.subscription(() => {
return observable<Parameters<AppEvents[TName]>[0]>((emit) => {
const listener: AppEvents[TName] = (...args: any[]) => {
emit.next(args[0]);
};
typedEmitter.addListener(eventName, listener);
return () => {
typedEmitter.removeListener(eventName, listener);
};
});
});
}

View File

@@ -1,39 +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 zlib from "node:zlib";
import utils from "node:util";
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
const gzipAsync = utils.promisify(zlib.gzip);
const gunzipAsync = utils.promisify(zlib.gunzip);
export const compressionRouter = t.router({
gzip: t.procedure
.input(z.object({ data: z.string(), level: z.number() }))
.query(async ({ input }) => {
const { data, level } = input;
return (await gzipAsync(data, { level })).toString("base64");
}),
gunzip: t.procedure.input(z.string()).query(async ({ input }) => {
return (await gunzipAsync(Buffer.from(input, "base64"))).toString("utf-8");
})
});

View File

@@ -1,45 +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 { compressionRouter } from "./compression";
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();
export const router = t.router({
compress: compressionRouter,
integration: osIntegrationRouter,
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter,
safeStorage: safeStorageRouter,
window: windowRouter
});
export const api = router.createCaller({});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof router;

View File

@@ -1,221 +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 { z } from "zod";
import { dialog, nativeTheme, Notification, shell } from "electron";
import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
import { getTheme, setTheme, Theme } from "../utils/theme";
import { mkdirSync, writeFileSync } from "fs";
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";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
const t = initTRPC.create();
const NotificationOptions = z.object({
title: z.string().optional(),
body: z.string().optional(),
silent: z.boolean().optional(),
timeoutType: z.union([z.literal("default"), z.literal("never")]).optional(),
urgency: z
.union([z.literal("normal"), z.literal("critical"), z.literal("low")])
.optional(),
tag: z.string()
});
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;
}),
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure
.input(z.string().optional())
.mutation(({ input: proxyRules }) => {
globalThis.window?.webContents.session.setProxy({ proxyRules });
config.proxyRules = proxyRules || "";
}),
privacyMode: t.procedure.query(() => config.privacyMode),
setPrivacyMode: t.procedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input: { enabled } }) => {
if (!globalThis.window || !["win32", "darwin"].includes(process.platform))
return;
globalThis.window.setContentProtection(enabled);
if (process.platform === "win32") {
globalThis.window.setThumbnailClip(
enabled
? { x: 0, y: 0, width: 1, height: 1 }
: { x: 0, y: 0, width: 0, height: 0 }
);
}
config.privacyMode = enabled;
}),
desktopIntegration: t.procedure.query(() => config.desktopSettings),
setDesktopIntegration: t.procedure
.input(DesktopIntegration)
.mutation(({ input: settings }) => {
if (settings.autoStart) {
AutoLaunch.enable(!!settings.startMinimized);
} else {
AutoLaunch.disable();
}
config.desktopSettings = settings;
setupDesktopIntegration(settings);
}),
selectDirectory: t.procedure
.input(
z.object({
title: z.string().optional(),
buttonLabel: z.string().optional(),
defaultPath: z.string().optional()
})
)
.query(async ({ input }) => {
if (!globalThis.window) return undefined;
const { title, buttonLabel, defaultPath } = input;
const result = await dialog.showOpenDialog(globalThis.window, {
title,
buttonLabel,
properties: ["openDirectory"],
defaultPath: defaultPath && resolvePath(defaultPath)
});
if (result.canceled) return undefined;
return result.filePaths[0];
}),
saveFile: t.procedure
.input(z.object({ data: z.string(), filePath: z.string() }))
.query(({ input }) => {
const { data, filePath } = input;
if (!data || !filePath) return;
const resolvedPath = resolvePath(filePath);
mkdirSync(dirname(resolvedPath), { recursive: true });
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 }) => {
const notification = new Notification({
...input,
icon: AssetManager.appIcon({
size: 64,
format: process.platform === "win32" ? "ico" : "png"
})
});
notification.show();
if (input.urgency === "critical") {
shell.beep();
}
return new Promise((resolve) => {
notification.once("close", () => resolve(undefined));
notification.once("click", () => resolve(input.tag));
});
}),
openPath: t.procedure
.input(z.object({ type: z.literal("path"), link: z.string() }))
.query(({ input }) => {
const { type, link } = input;
if (type === "path") return shell.openPath(resolvePath(link));
}),
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);
}
),
onThemeChanged: t.procedure.subscription(() =>
observable<"dark" | "light">((emit) => {
const 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

@@ -1,85 +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";
import { CancellationToken, autoUpdater } from "electron-updater";
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
import { z } from "zod";
import { config } from "../utils/config";
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));
}),
check: t.procedure.query(async () => {
await autoUpdater.checkForUpdates().catch(console.error);
}),
toggleAutoUpdates: t.procedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input: { enabled } }) => {
config.automaticUpdates = enabled;
}),
onChecking: createSubscription("checking-for-update"),
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
"update-downloaded"
),
onDownloadProgress: createSubscription<"download-progress", Progress>(
"download-progress"
),
onNotAvailable: createSubscription<"update-not-available", UpdateInfo>(
"update-not-available"
),
onAvailable: createSubscription<"update-available", UpdateInfo>(
"update-available"
),
onError: createSubscription("error")
});
function createSubscription<
TName extends keyof AppUpdaterEvents,
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
>(eventName: TName) {
return t.procedure.subscription(() => {
return observable<TReturnType>((emit) => {
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

@@ -1,23 +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/>.
*/
export * from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";

View File

@@ -1,197 +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, BrowserWindow, nativeTheme, shell } from "electron";
import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
import { configureAutoUpdater } from "./utils/autoupdater";
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
import { setupMenu } from "./utils/menu";
import { WindowState } from "./utils/window-state";
import { setupJumplist } from "./utils/jumplist";
import { setupTray } from "./utils/tray";
import { CLIOptions, parseArguments } from "./cli";
import { AssetManager } from "./utils/asset-manager";
import { createIPCHandler } from "electron-trpc/main";
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";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
console.log("Another instance is already running!");
app.exit();
}
if (process.platform == "win32" && process.env.PORTABLE_EXECUTABLE_DIR) {
console.log("Portable app: true");
const root = path.join(process.env.PORTABLE_EXECUTABLE_DIR, "Notesnook");
app.setPath("appData", path.join(root, "AppData"));
app.setPath("documents", path.join(root, "Documents"));
app.setPath("userData", path.join(root, "UserData"));
}
if (process.platform === "win32") {
app.setAppUserModelId(app.name);
}
process.on("uncaughtException", (error) => {
console.error("uncaughtException:", error);
});
process.on("unhandledRejection", (reason) => {
console.error("unhandledRejection:", reason);
});
app.commandLine.appendSwitch("lang", "en-US");
async function createWindow() {
const cliOptions = await parseArguments(process.argv);
setTheme(getTheme());
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
darkTheme: getTheme() === "dark",
backgroundColor: getBackgroundColor(),
opacity: 0,
autoHideMenuBar: false,
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,
contextIsolation: false,
spellcheck: config.isSpellCheckerEnabled,
preload: __dirname + "/preload.js"
}
});
createIPCHandler({ router, windows: [mainWindow] });
globalThis.window = mainWindow;
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
mainWindow.minimize();
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
mainWindow.setOpacity(1);
if (config.privacyMode) {
await api.integration.setPrivacyMode({ enabled: config.privacyMode });
}
await AssetManager.loadIcons();
setupDesktopIntegration(config.desktopSettings);
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
mainWindow.once("closed", () => {
globalThis.window = null;
});
setupMenu();
setupJumplist();
if (isDevelopment())
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: "deny" };
});
nativeTheme.on("updated", () => {
setupTray();
setupJumplist();
});
}
app.once("ready", async () => {
console.info("App ready. Opening window.");
if (config.customDns) enableCustomDns();
else disableCustomDns();
if (!isDevelopment()) registerProtocol();
await createWindow();
configureAutoUpdater();
});
app.once("window-all-closed", () => {
if (process.platform !== "darwin" || MAC_APP_STORE) {
app.quit();
}
});
app.on("second-instance", async (_ev, argv) => {
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();
});
app.on("activate", () => {
if (globalThis.window === null) {
createWindow();
}
});
function createURL(options: CLIOptions, path = "/") {
const url = new URL(isDevelopment() ? "http://localhost:3000" : PROTOCOL_URL);
url.pathname = path;
if (options.note === true) url.hash = "/notes/create/1";
else if (options.notebook === true) url.hash = "/notebooks/create";
else if (options.reminder === true) url.hash = "/reminders/create";
else if (typeof options.note === "string")
url.hash = `/notes/${options.note}/edit`;
else if (typeof options.notebook === "string")
url.hash = `/notebooks/${options.notebook}`;
return url;
}

View File

@@ -1,47 +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/>.
*/
/* eslint-disable no-var */
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
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 () => {
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
onMessage: (callback) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};
globalThis.electronTRPC = electronTRPC;
});
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -1,62 +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 { nativeTheme } from "electron";
import { JSONStorage } from "./json-storage";
import { z } from "zod";
export const DesktopIntegration = z.object({
autoStart: z.boolean().optional(),
startMinimized: z.boolean().optional(),
minimizeToSystemTray: z.boolean().optional(),
closeToSystemTray: z.boolean().optional()
});
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
export const config = {
desktopSettings: <DesktopIntegration>{
autoStart: false,
startMinimized: false,
minimizeToSystemTray: false,
closeToSystemTray: false
},
privacyMode: false,
isSpellCheckerEnabled: true,
zoomFactor: 1,
theme: nativeTheme.themeSource,
automaticUpdates: true,
proxyRules: "",
customDns: true,
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:
nativeTheme.themeSource === "dark" ? "#ffffff" : "#000000"
};
type ConfigKey = keyof typeof config;
for (const key in config) {
const defaultValue = config[<ConfigKey>key];
if (Object.hasOwn(config, key)) {
Object.defineProperty(config, key, {
get: () => JSONStorage.get(key, defaultValue),
set: (value) => JSONStorage.set(key, value)
});
}
}

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

@@ -1,78 +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 { net, protocol } from "electron";
import { isDevelopment } from "./index";
import { createReadStream } from "fs";
import { extname, normalize } from "path";
import { URL } from "url";
const BASE_PATH = isDevelopment() ? "../public" : "";
const HOSTNAME = `app.notesnook.com`;
const SCHEME = "https";
const extensionToMimeType: Record<string, string> = {
html: "text/html",
json: "application/json",
js: "application/javascript",
css: "text/css",
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpg",
ttf: "font/ttf",
woff: "font/woff",
woff2: "font/woff2"
};
function registerProtocol() {
protocol.handle(SCHEME, async (request) => {
const url = new URL(request.url);
if (shouldInterceptRequest(url)) {
console.info("Intercepting request:", request.url);
const loadIndex = !extname(url.pathname);
const filePath = normalize(
`${__dirname}${
loadIndex ? `${BASE_PATH}/index.html` : `${BASE_PATH}/${url.pathname}`
}`
);
if (!filePath) {
console.error("Local asset file not found at", filePath);
return new Response(undefined, {
status: 404,
statusText: "FILE_NOT_FOUND"
});
}
const fileExtension = extname(filePath).replace(".", "");
return new Response(createReadStream(filePath), {
headers: { "Content-Type": extensionToMimeType[fileExtension] }
});
} else {
return net.fetch(request, { bypassCustomProtocolHandlers: true });
}
});
console.info(`${SCHEME} protocol inteception "successful"`);
}
const bypassedRoutes: string[] = [];
function shouldInterceptRequest(url: URL) {
const shouldIntercept = url.hostname === HOSTNAME;
return shouldIntercept && !bypassedRoutes.includes(url.pathname);
}
const PROTOCOL_URL = `${SCHEME}://${HOSTNAME}/`;
export { registerProtocol, PROTOCOL_URL };

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist/",
"lib": ["ESNext"]
},
"include": ["src", "global.d.ts"]
}

View File

@@ -17,6 +17,15 @@ jobs:
with:
persist-credentials: false
- name: Setup npmrc
run: |
echo "registry=https://npm.pkg.github.com" > .npmrc
echo "@streetwriters:registry=https://npm.pkg.github.com" >> .npmrc
echo "//npm.pkg.github.com/:_authToken=${{secrets.PACKAGES_TOKEN}}" >> .npmrc
- name: Setup yarnrc
run: echo "\"@streetwriters:registry\" \"https://npm.pkg.github.com\"" > .yarnrc
- uses: actions/setup-node@master
with:
node-version: "16"

View File

@@ -17,6 +17,15 @@ jobs:
with:
persist-credentials: false
- name: Setup npmrc
run: |
echo "registry=https://npm.pkg.github.com" > .npmrc
echo "@streetwriters:registry=https://npm.pkg.github.com" >> .npmrc
echo "//npm.pkg.github.com/:_authToken=${{secrets.PACKAGES_TOKEN}}" >> .npmrc
- name: Setup yarnrc
run: echo "\"@streetwriters:registry\" \"https://npm.pkg.github.com\"" > .yarnrc
- uses: actions/setup-node@master
with:
node-version: "16"

View File

@@ -5,6 +5,7 @@ artifacts/
.DS_Store
native/android/app/src/main/assets/
*Issues.md
build_cache/
#
@@ -13,9 +14,7 @@ build_cache/
.yarnrc.yml
.yarn
*.log
.cxx/
*.keystore
!debug.keystore
# Xcode
#

View File

@@ -25,4 +25,4 @@
"request": "attach"
}
]
}
}

View File

@@ -19,9 +19,9 @@
## Build instructions
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
### Setting up the development environment
### Setting up development environment
Requirements:
@@ -57,7 +57,7 @@ npm install
### Running the app on Android
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
```bash
npm run start:android
@@ -78,11 +78,11 @@ npm run start:ios
## Developer guide
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
### The tech stack
We try to keep the stack as lean as possible:
We try to keep the stack as lean as possible
1. React Native
2. Typescript/Javascript
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible:
The app codebase is distributed over two primary directories. `native/` and `app/`.
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
- `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
- `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
- `hooks/`: Hooks for different app logic
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `screens`: Navigator screens.
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
- `utils`: General purpose stuff such as constant values, utility functions, etc.
- `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
- `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
- `utils`: General purpose stuff such as constant values, utility functions etc.
There are several other folders at the root:
@@ -115,11 +115,11 @@ There are several other folders at the root:
### Running the tests
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
### Android
To run the tests on Android, you will need to create an emulator device on your system:
To run the tests on android, you will need to create an emulator device on your system:
```
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
Once you have created an emulator device, build the Android apks:
Once you have created an emulator device, build the android apks
```
npm run build:android
```
Finally, run the tests:
Finally run the tests
```
npm run test:android
@@ -141,9 +141,9 @@ npm run test:android
### iOS
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
```
brew tap wix/brew
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
npm run build:ios
```
Finally, run the tests:
Finally run the tests:
```
npm run test:ios

View File

@@ -3,7 +3,6 @@
*/
import "react-native";
import { it } from "@jest/globals";
// Note: test renderer must be required after react-native.
import renderer from "react-test-renderer";
import Heading from "../app/components/ui/typography/heading";

98
apps/mobile/app/app.js Normal file
View File

@@ -0,0 +1,98 @@
/*
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-native-gesture-handler";
import React, { useEffect } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import Launcher from "./components/launcher";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useUserStore } from "./stores/use-user-store";
import { View } from "react-native";
import { useState } from "react";
import NetInfo from "@react-native-community/netinfo";
NetInfo.configure({
reachabilityUrl: "https://notesnook.com",
reachabilityTest: (response) => {
if (!response) return false;
return response?.status >= 200 && response?.status < 300;
}
});
SettingsService.init();
SettingsService.checkOrientation();
const App = () => {
useAppEvents();
const [init, setInit] = useState(false);
useEffect(() => {
let { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().setVerifyUser(true);
}
//BackgroundSync.start();
setInit(true);
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);
}, []);
return (
<View
style={{
height: "100%",
width: "100%"
}}
>
<View
style={{
position: "absolute",
width: "100%",
height: "100%",
zIndex: -1
}}
pointerEvents="none"
>
<SafeAreaProvider>
<GlobalSafeAreaProvider />
</SafeAreaProvider>
</View>
<GestureHandlerRootView
style={{
height: "100%",
width: "100%"
}}
>
<ApplicationHolder />
{init && <Launcher />}
</GestureHandlerRootView>
</View>
);
};
export default withErrorBoundry(App, "App");

View File

@@ -1,150 +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-native-gesture-handler";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { I18nManager, View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
const App = () => {
const init = useAppEvents();
useEffect(() => {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<View
style={{
height: "100%",
width: "100%"
}}
>
<View
style={{
position: "absolute",
width: "100%",
height: "100%",
zIndex: -1
}}
pointerEvents="none"
>
<SafeAreaProvider>
<GlobalSafeAreaProvider />
</SafeAreaProvider>
</View>
<GestureHandlerRootView
style={{
height: "100%",
width: "100%"
}}
>
<ApplicationHolder />
</GestureHandlerRootView>
<AppLockedOverlay />
</View>
);
};
let currTheme =
useThemeStore.getState().colorScheme === "dark"
? SettingsService.getProperty("darkTheme")
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: () => JSX.Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,
state.lightTheme
]);
useEffect(() => {
setTimeout(() => {
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (!currentTheme) return;
themeTrpcClient.updateTheme
.query({
version: currentTheme.version,
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
id: currentTheme.id
})
.then((theme) => {
if (theme) {
console.log(theme.version, "theme updated");
theme.colorScheme === "dark"
? useThemeStore.getState().setDarkTheme(theme)
: useThemeStore.getState().setLightTheme(theme);
}
})
.catch(console.log);
}, 1000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
currTheme = nextTheme;
}
return <Element />;
};
};
export default withTheme(withErrorBoundry(App, "App"));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -17,274 +17,56 @@ 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 { 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";
import Sodium from "@ammarahmed/react-native-sodium";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
},
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);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
console.error(e);
}
}
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
);
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 +84,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,28 +98,9 @@ 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);
}
export async function decryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
data = data.map((d) => {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
@@ -366,24 +129,3 @@ export async function encrypt(password, data) {
alg: getAlgorithm(7)
};
}
export async function encryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let results = await Sodium.encryptMulti(
password,
data.map((item) => ({
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}

View File

@@ -16,20 +16,37 @@ 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 Database from "@notesnook/core/api/index";
import { initalize, logger as dbLogger } from "@notesnook/core/logger";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import { MMKVLoader } from "react-native-mmkv-storage";
import filesystem from "../filesystem";
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 Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
import Storage, { KV } from "./storage";
import * as Gzip from "react-native-gzip";
database.host(
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
initalize(new KV(LoggerStorage), true);
export const DatabaseLogger = dbLogger;
/**
* @type {import("@notesnook/core/api/index").default}
*/
export var db = new Database(
Storage,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem,
{
compress: Gzip.deflate,
decompress: Gzip.inflate
}
);
db.host(
__DEV__
? {
API_HOST: "https://api.notesnook.com",
@@ -37,11 +54,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.8.101:5264",
// AUTH_HOST: "http://192.168.8.101:8264",
// SSE_HOST: "http://192.168.8.101:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.8.101:9264",
// ISSUES_HOST: "http://192.168.8.101:2624"
}
: {
API_HOST: "https://api.notesnook.com",
@@ -52,37 +69,13 @@ 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
}
});
export async function loadDatabase() {
// if (!DB) {
// let module = await import(/* webpackChunkName: "notes-core" */ 'notes-core/api/index');
// DB = module.default;
// }
// db = new DB(Storage, Platform.OS === 'ios' ? EventSource : AndroidEventSource, filesystem);
// if (DOMParser) {
// await DOMParser.prepare();
// }
}
export const db = database;
export const DatabaseLogger = dbLogger;

View File

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

View File

@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import { ProcessingModes, MMKVLoader } from "react-native-mmkv-storage";
import MMKVStorage, { ProcessingModes } from "react-native-mmkv-storage";
export const MMKV = new MMKVLoader()
export const MMKV = new MMKVStorage.Loader()
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();

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

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import {
decrypt,
deriveCryptoKey,
@@ -27,16 +27,11 @@ import {
getCryptoKey,
getRandomBytes,
hash,
removeCryptoKey,
decryptMulti,
encryptMulti
removeCryptoKey
} from "./encryption";
import { MMKV } from "./mmkv";
export class KV {
/**
* @type {typeof MMKV}
*/
storage = null;
constructor(storage) {
this.storage = storage;
@@ -58,7 +53,6 @@ export class KV {
key,
typeof data === "string" ? data : JSON.stringify(data)
);
return true;
}
@@ -66,37 +60,27 @@ export class KV {
if (keys.length <= 0) {
return [];
} else {
try {
let data = await this.storage.getMultipleItemsAsync(
keys.slice(),
"string"
);
return data.map(([key, value]) => {
let obj;
try {
obj = JSON.parse(value);
} catch (e) {
obj = value;
}
return [key, obj];
});
} catch (e) {
console.log(e);
}
let data = await this.storage.getMultipleItemsAsync(keys.slice());
return data.map(([key, value]) => {
let obj;
try {
obj = JSON.parse(value);
} catch (e) {
obj = value;
}
return [key, obj];
});
}
}
async remove(key) {
return this.storage.removeItem(key);
}
async removeMulti(keys) {
if (!keys) return true;
return this.storage.removeItems(keys);
return await this.storage.removeItem(key);
}
async clear() {
return this.storage.clearStore();
return await this.storage.clearStore();
}
async getAllKeys() {
@@ -112,10 +96,6 @@ export class KV {
);
return keys;
}
async writeMulti(items) {
return this.storage.setMultipleItemsAsync(items, "object");
}
}
const DefaultStorage = new KV(MMKV);
@@ -149,11 +129,8 @@ export default {
remove: (key) => DefaultStorage.remove(key),
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
encrypt,
decrypt,
decryptMulti,
getRandomBytes,
checkAndCreateDir,
requestPermission,
@@ -161,6 +138,5 @@ export default {
getCryptoKey,
removeCryptoKey,
hash,
generateCryptoKey,
encryptMulti
generateCryptoKey
};

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");
import RNFetchBlob from "rn-fetch-blob";
/**
* 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

@@ -17,288 +17,100 @@ 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 {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/dist/utils/filename";
import React from "react";
import { Platform } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { createCacheDir, exists } from "./io";
import { IOS_APPGROUPID } from "../../utils/constants";
import { cacheDir } from "./utils";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export async function downloadAllAttachments() {
const attachments = await db.attachments.all.ids();
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
await createCacheDir();
if (!attachments || !attachments.length) return;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
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;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
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}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
});
} catch (e) {
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Fail,
reason: e
});
ToastManager.error(e, "Error downloading attachment");
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
export default async function downloadAttachment(hash, global = true) {
let attachment = db.attachments.attachment(hash);
if (!attachment) {
DatabaseLogger.log("Attachment not found");
console.log("attachment not found");
return;
}
let folder = {};
if (!options.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
try {
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options.base64 || options.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
);
}
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
await db.fs.downloadFile(
attachment.metadata.hash,
attachment.metadata.hash
);
if (
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
)
return;
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,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType,
mime: attachment.metadata.type,
fileName: attachment.metadata.filename,
uri: folder.uri,
chunkSize: attachment.chunkSize
};
let fileUri = await Sodium.decryptFile(
key,
info,
options.cache ? "cache" : "file"
);
if (!options.silent) {
ToastManager.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
});
}
let fileUri = await Sodium.decryptFile(key, info, "file");
ToastEvent.show({
heading: "Download successful",
message: attachment.metadata.filename + " downloaded",
type: "success"
});
if (
attachment.dateUploaded &&
!isImage(attachment.mimeType) &&
!isDocument(attachment.mimeType)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
}
if (!options.silent) {
presentSheet({
title: "File downloaded",
paragraph: `${filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
return fileUri;
} catch (e) {
DatabaseLogger.error(e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios") {
fileUri = folder.uri + `/${attachment.metadata.filename}`;
}
console.log("saved file uri: ", fileUri);
presentSheet({
title: "File downloaded",
paragraph: `${attachment.metadata.filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.metadata.hash,
component: (
<ShareComponent
uri={fileUri}
name={attachment.metadata.filename}
padding={12}
/>
)
});
return fileUri;
} catch (e) {
console.log("download attachment error: ", e);
if (attachment.dateUploaded) {
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);
if (options.throwError) {
throw e;
}
useAttachmentStore.getState().remove(attachment.metadata.hash);
}
}

View File

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

View File

@@ -23,16 +23,14 @@ import {
deleteFile,
exists,
readEncrypted,
writeEncryptedBase64,
hashBase64
writeEncrypted
} from "./io";
import { uploadFile } from "./upload";
import { cancelable } from "./utils";
export default {
readEncrypted,
writeEncryptedBase64,
hashBase64,
writeEncrypted,
uploadFile: cancelable(uploadFile),
downloadFile: cancelable(downloadFile),
deleteFile,

View File

@@ -17,67 +17,64 @@ 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 RNFetchBlob from "react-native-blob-util";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
let exists = await RNFetchBlob.fs.exists(path);
if (!exists) {
return false;
}
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
let output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
hash: filename
},
cipherData.outputType === "base64" ? "base64" : "text"
cipherData.outputType === "base64"
? isPng || isJpeg
? "cache"
: "base64"
: "text"
);
DatabaseLogger.log("File decrypted...");
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
const dCachePath = `${cacheDir}/${output}`;
output = await compressToBase64(
`file://${dCachePath}`,
isPng ? "PNG" : "JPEG"
);
}
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e);
console.log(e);
return false;
}
}
export async function hashBase64(data) {
const hash = await Sodium.hashFile({
type: "base64",
data,
uri: ""
});
return {
hash: hash,
type: "xxh64"
};
}
export async function writeEncryptedBase64(data, key) {
await createCacheDir();
export async function writeEncrypted(filename, { data, type, key }) {
console.log("file input: ", { type, key });
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
console.log(filepath);
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
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"
@@ -85,7 +82,6 @@ export async function writeEncryptedBase64(data, key) {
}
export async function deleteFile(filename, data) {
await createCacheDir();
let delFilePath = cacheDir + `/${filename}`;
if (!data) {
if (!filename) return;
@@ -111,92 +107,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);
DatabaseLogger.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) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
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

@@ -17,65 +17,27 @@ 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 RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { db } from "../database";
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";
import { getUploadedFileSize } from "./download";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
let { url, headers } = data;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
console.log("uploading file: ", filename, headers);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
DatabaseLogger.log("Upload verification failed.");
return false;
}
const isUploaded = uploadedFileSize !== 0;
if (isUploaded) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
method: "PUT",
headers
});
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
const uploadUrl = await res.text();
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
if (!uploadUrl) throw new Error("Unable to resolve upload url");
let request = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
IOSBackgroundTask: true
})
.fetch(
"PUT",
@@ -83,21 +45,15 @@ export async function uploadFile(filename, data, cancelToken) {
{
"content-type": ""
},
RNFetchBlob.wrap(uploadFilePath)
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
)
.uploadProgress((sent, total) => {
useAttachmentStore
.getState()
.setProgress(sent, total, filename, 0, "upload");
DatabaseLogger.info(
`File upload progress: ${filename}, ${sent}/${total}`
);
console.log("uploading: ", sent, total);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
cancelToken.cancel = request.cancel;
let response = await request;
let status = response.info().status;
@@ -105,28 +61,17 @@ export async function uploadFile(filename, data, cancelToken) {
let result = status >= 200 && status < 300 && text.length === 0;
useAttachmentStore.getState().remove(filename);
if (result) {
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 (!attachment.metadata.type.startsWith("image/")) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
} else {
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
throw new Error(
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(response.info())}`
);
}
return result;
} catch (e) {
useAttachmentStore.getState().remove(filename);
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
DatabaseLogger.error(e);
console.log("upload file: ", e, url, headers);
return false;
}
}

View File

@@ -17,16 +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 * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
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()
@@ -69,25 +62,3 @@ export function cancelable(operation) {
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve, reject) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
if (e) {
reject(e);
return;
}
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -17,24 +17,22 @@ 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 { useThemeStore } from "../../stores/use-theme-store";
import { allowedOnPlatform, renderItem } from "./functions";
import { getContainerBorder } from "../../utils/colors";
export const Announcement = ({ color }) => {
const { colors } = useThemeColors();
const colors = useThemeStore((state) => state.colors);
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={{
backgroundColor: colors.primary.background,
backgroundColor: colors.bg,
width: "100%",
paddingHorizontal: 12,
paddingTop: 12,
@@ -46,27 +44,28 @@ export const Announcement = ({ color }) => {
width: "100%",
borderRadius: 10,
overflow: "hidden",
backgroundColor: colors.secondary.background,
paddingBottom: 12,
...getContainerBorder(colors.secondary.background)
backgroundColor: colors.nav,
paddingBottom: 12
}}
>
<View
style={{
width: "100%",
marginTop: 12
}}
>
{announcement?.body
.filter((item) => allowedOnPlatform(item.platforms))
.map((item, index) =>
<View>
<FlatList
style={{
width: "100%",
marginTop: 12
}}
data={announcement?.body.filter((item) =>
allowedOnPlatform(item.platforms)
)}
renderItem={({ item, index }) =>
renderItem({
item: item,
index: index,
color: colors[color],
inline: true
})
)}
}
/>
</View>
</View>
</View>

View File

@@ -22,7 +22,7 @@ import { Linking, View } from "react-native";
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { useThemeStore } from "../../stores/use-theme-store";
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
@@ -33,7 +33,7 @@ import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions";
export const Cta = ({ actions, style = {}, color, inline }) => {
const { colors } = useThemeColors();
const colors = useThemeStore((state) => state.colors);
let buttons =
actions.filter((item) => allowedOnPlatform(item.platforms)) || [];
@@ -92,7 +92,8 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
style={{
height: 30,
alignSelf: "flex-start",
paddingHorizontal: 0
paddingHorizontal: 0,
marginTop: -6
}}
/>
))}
@@ -103,13 +104,14 @@ 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}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
marginTop: -6,
marginLeft: 12
}}
textStyle={{
@@ -127,11 +129,9 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
title={item.title}
fontSize={SIZE.md}
buttonType={{
color: color ? color : colors.primary.accent,
text: color
? colors.static.white
: colors.primary.accentForeground,
selected: color ? color : colors.primary.accent,
color: color ? color : colors.accent,
text: colors.light,
selected: color ? color : colors.accent,
opacity: 1
}}
onPress={() => onPress(item)}
@@ -148,8 +148,8 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
<Button
key={item.title}
title={item.title}
fontSize={SIZE.xs}
type="plain"
fontSize={SIZE.xs + 1}
type="gray"
onPress={() => onPress(item)}
width={null}
height={30}

View File

@@ -25,7 +25,7 @@ import {
eUnSubscribeEvent
} from "../../services/event-manager";
import { useMessageStore } from "../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { useThemeStore } from "../../stores/use-theme-store";
import {
eCloseAnnouncementDialog,
eOpenAnnouncementDialog
@@ -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 colors = useThemeStore((state) => state.colors);
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(() => {
@@ -134,7 +73,7 @@ export const AnnouncementDialog = () => {
<View
style={{
width: DDS.isTab ? 600 : "100%",
backgroundColor: colors.primary.background,
backgroundColor: colors.bg,
maxHeight: DDS.isTab ? "90%" : "100%",
borderRadius: DDS.isTab ? 10 : 0,
overflow: "hidden",

View File

@@ -54,7 +54,7 @@ export const Title = ({ text, style = {}, inline }) => {
</Heading>
<Button
type="plain"
type="gray"
icon="close"
height={null}
onPress={() => {
@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
right: 0
}}
iconSize={24}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
borderRadius: 100,
paddingVertical: 0,

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,15 @@ 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 { useThemeStore } from "../../stores/use-theme-store";
import { formatBytes } from "../../utils";
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -51,43 +44,30 @@ import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
const Actions = ({
attachment,
close,
setAttachments,
fwdRef
}: {
attachment: Attachment;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const contextId = attachment.hash;
const [filename, setFilename] = useState(attachment.filename);
const Actions = ({ attachment, setAttachments, fwdRef }) => {
const colors = useThemeStore((state) => state.colors);
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"
@@ -96,7 +76,7 @@ const Actions = ({
name: "Reupload",
onPress: async () => {
if (!PremiumService.get()) {
ToastManager.show({
ToastEvent.show({
heading: "Upgrade to pro",
type: "error",
context: "local"
@@ -105,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"
@@ -118,29 +98,22 @@ 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({
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({
heading: "File check passed",
type: "success",
context: "local"
});
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
}
setAttachments();
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
setAttachments([...db.attachments.all]);
setLoading({
name: undefined
name: null
});
},
icon: "file-check"
@@ -149,20 +122,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"
@@ -173,40 +145,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
@@ -219,11 +185,10 @@ const Actions = ({
}}
>
<Dialog context={contextId} />
<SheetProvider context={contextId} />
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
borderBottomColor: colors.nav,
marginBottom: notes && notes.length > 0 ? 0 : 12
}}
>
@@ -240,37 +205,53 @@ const Actions = ({
style={{
flexDirection: "row",
marginBottom: 10,
paddingHorizontal: 12,
marginTop: 6,
gap: 10
paddingHorizontal: 12
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{attachment.mimeType}
<Paragraph
size={SIZE.xs + 1}
style={{
marginRight: 10
}}
color={colors.icon}
>
{attachment.metadata.type}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{formatBytes(attachment.size)}
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs + 1}
color={colors.icon}
>
{formatBytes(attachment.length)}
</Paragraph>
{notes.length ? (
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{notes.length} note
{notes.length > 1 ? "s" : ""}
{attachment.noteIds ? (
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs + 1}
color={colors.icon}
>
{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"
});
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.hash}
{attachment.metadata.hash}
</Paragraph>
</View>
@@ -281,7 +262,7 @@ const Actions = ({
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
borderBottomColor: colors.nav,
marginBottom: 12,
paddingVertical: 12
}}
@@ -297,15 +278,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",
@@ -313,8 +304,8 @@ const Actions = ({
}}
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
</Pressable>
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
</PressableButton>
))}
</>
</View>
@@ -324,16 +315,17 @@ const Actions = ({
<Button
key={item.name}
buttonType={{
text:
item.name === "Delete"
? colors.error.paragraph
: colors.primary.paragraph
text: item.on
? colors.accent
: item.name === "Delete" || item.name === "PermDelete"
? colors.errorText
: colors.pri
}}
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,
@@ -352,7 +344,7 @@ const Actions = ({
{failed ? (
<Notice
type="alert"
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
size="small"
/>
) : null}
@@ -361,20 +353,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,172 @@
/*
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 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 { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { formatBytes } from "../../utils";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
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 }) => {
const colors = useThemeStore((state) => state.colors);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const encryptionProgress = useAttachmentStore(
(state) => state.encryptionProgress
);
const onPress = () => {
Actions.present(attachment, setAttachments, attachment.metadata.hash);
};
return (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.nav
}}
type="grayBg"
>
<SheetProvider context={attachment.metadata.hash} />
<View
style={{
flexShrink: 1,
flexDirection: "row",
alignItems: "center"
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.icon} />
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.light}
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.pri}
>
{attachment.metadata.filename}
</Paragraph>
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
</View>
</View>
{currentProgress || encryptionProgress || encryption ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
style={{
justifyContent: "center",
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={
encryptionProgress
? encryptionProgress
: currentProgress?.value
? currentProgress?.value / 100
: 0
}
showsText
textStyle={{
fontSize: 10
}}
color={colors.accent}
formatText={(progress) => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton
onPress={onPress}
name="alert-circle-outline"
color={colors.errorText}
/>
) : 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

@@ -1,297 +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, { 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 { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
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 { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
value: 0,
statusText: "Download started... Please wait"
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef<string>();
const onDownload = async () => {
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 }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update?.({
disableClosing: false
} as PresentSheetOptions);
};
const cancel = async () => {
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);
setDownloading(false);
setResult(new Map());
groupId.current = undefined;
};
const failedResults = () => {
const results = [];
for (const value of result.values()) {
if (value.status === 0) results.push(value.attachment);
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments?.placeholders?.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments?.placeholders.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
}
return (
<View
style={{
alignItems: "center",
width: "100%",
paddingVertical: 12,
paddingHorizontal: 12
}}
>
<Heading>
{downloading
? "Downloading attachments"
: result?.size
? "Downloaded attachments"
: "Download attachments"}
</Heading>
{downloading ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{progress.statusText}
</Paragraph>
) : result?.size ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{getResultText()}
</Paragraph>
) : (
<Paragraph
style={{
textAlign: "center"
}}
>
Are you sure you want to download all attachments
{isNote ? " of this note?" : "?"}
</Paragraph>
)}
{downloading ? (
<View
style={{
width: 200,
marginTop: 10
}}
>
<ProgressBarComponent
height={5}
width={null}
animated={true}
useNativeDriver
progress={
progress.value
? progress.value / attachments.placeholders?.length
: 0
}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
) : null}
<FlatList
style={{
maxHeight: 300,
width: "100%",
minHeight: 60,
backgroundColor: colors.secondary.background,
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.placeholders : undefined}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 60
}}
>
<Paragraph color={colors.secondary.paragraph}>
No downloads in progress.
</Paragraph>
</View>
}
keyExtractor={(index) => "attachment_download" + index}
renderItem={({ index }) => {
return (
<AttachmentItem
id={index}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
/>
{result?.size ? (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={() => {
close?.();
}}
type="accent"
title="Done"
/>
) : !downloading ? (
<View
style={{
flexDirection: "row",
width: "100%",
marginTop: 20
}}
>
<Button
style={{
flex: 1,
borderRadius: 100,
marginRight: 5
}}
onPress={() => {
close?.();
}}
type="secondary"
title="No"
/>
<Button
style={{
flex: 1,
borderRadius: 100,
marginLeft: 5
}}
onPress={onDownload}
type="accent"
title="Yes"
/>
</View>
) : (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={cancel}
type="error"
title="Cancel"
/>
)}
</View>
);
};
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
presentSheet({
context: context,
component: (ref, close, update) => (
<DownloadAttachments
close={close}
attachments={attachments}
isNote={isNote}
update={update}
/>
)
});
};
export default DownloadAttachments;

View File

@@ -0,0 +1,210 @@
/*
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, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import {
eCloseAttachmentDialog,
eOpenAttachmentsDialog
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import DialogHeader from "../dialog/dialog-header";
import { Toast } from "../toast";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import SheetWrapper from "../ui/sheet";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
export const AttachmentDialog = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const actionSheetRef = useRef();
const [attachments, setAttachments] = useState([]);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent(eOpenAttachmentsDialog, open);
eSubscribeEvent(eCloseAttachmentDialog, close);
return () => {
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
eUnSubscribeEvent(eCloseAttachmentDialog, close);
};
}, [visible]);
const open = (data) => {
if (data?.id) {
setNote(data);
let _attachments = db.attachments.ofNote(data.id, "all");
setAttachments(_attachments);
} else {
setAttachments([...db.attachments.all]);
}
setVisible(true);
};
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
actionSheetRef.current?.hide();
setVisible(false);
};
const onChangeText = (text) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments([...db.attachments.all]);
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
db.attachments.all,
attachmentSearchValue.current
);
if (results.length === 0) return;
setAttachments(results);
}, 300);
};
const renderItem = ({ item }) => (
<AttachmentItem setAttachments={setAttachments} attachment={item} />
);
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onClose={async () => {
setVisible(false);
}}
>
<Toast context="local" />
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
}}
>
<DialogHeader
title={note ? "Attachments" : "Manage attachments"}
paragraph="Tap on an attachment to view properties"
button={{
title: "Check all",
type: "grayAccent",
loading: loading,
onPress: async () => {
setLoading(true);
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(
attachment.metadata.hash
);
if (result.failed) {
db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
db.attachments.markAsFailed(attachment.id, null);
}
setAttachments([...db.attachments.all]);
}
setLoading(false);
}
}}
/>
<Seperator />
{!note ? (
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
) : null}
<FlatList
nestedScrollEnabled
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon name="attachment" size={60} color={colors.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
data={attachments}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 10
}}
>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{" "}All attachments are end-to-end encrypted.
</Paragraph>
</View>
</SheetWrapper>
);
};

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 {
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 { 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 { 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";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: 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 [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]);
useEffect(() => {
refresh();
}, [note, refresh]);
const onChangeText = async (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments(await filterAttachments(currentFilter));
refresh();
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(async () => {
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
if (results.length === 0) return;
setAttachments(results);
}, 300);
};
const renderItem = ({ index }: { item: boolean; index: number }) => (
<AttachmentItem
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
}}
attachments={attachments}
id={index}
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);
if (result.failed) {
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
await db.attachments.markAsFailed(attachment.id, undefined);
}
}
refresh();
setLoading(false);
};
const attachmentTypes = [
{
title: "All",
filterBy: "all"
},
{
title: "Images",
filterBy: "images"
},
{
title: "Documents",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
}
];
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
switch (type) {
case "all":
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.all;
break;
case "images":
items = note
? db.attachments.ofNote(note.id, "images")
: db.attachments.images;
break;
case "video":
items = items = note
? db.attachments.ofNote(note.id, "videos")
: db.attachments.videos;
break;
case "audio":
items = note
? db.attachments.ofNote(note.id, "audio")
: db.attachments.audios;
break;
case "documents":
items = note
? db.attachments.ofNote(note.id, "documents")
: db.attachments.documents;
}
return await items.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
});
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
height: height * 0.85
}}
>
<SheetProvider context="attachments-list" />
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading>Attachments</Heading>
<View
style={{
flexDirection: "row"
}}
>
{loading ? (
<ActivityIndicator
style={{
height: 40,
width: 40,
marginRight: 10
}}
size={SIZE.lg}
/>
) : (
<IconButton
name="check-all"
style={{
height: 40,
width: 40,
marginRight: 10
}}
color={colors.primary.paragraph}
size={SIZE.lg}
onPress={onCheck}
/>
)}
<IconButton
name="download"
style={{
height: 40,
width: 40
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
!!note
);
}}
size={SIZE.lg}
/>
</View>
</View>
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
}}
/>
<View>
<ScrollView
style={{
width: "100%",
height: 50,
flexDirection: "row",
backgroundColor: colors.primary.background
}}
contentContainerStyle={{
minWidth: "100%",
height: 50
}}
horizontal
>
{attachmentTypes.map((item) => (
<Button
type={
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
}
key={item.title}
title={item.title}
style={{
borderRadius: 0,
borderBottomWidth: 1,
flexGrow: 1,
borderBottomColor:
currentFilter !== item.filterBy
? "transparent"
: colors.primary.accent
}}
onPress={async () => {
setCurrentFilter(item.filterBy);
setAttachments(await filterAttachments(item.filterBy));
}}
/>
))}
</ScrollView>
</View>
<FlashList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon name="attachment" size={60} color={colors.secondary.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
estimatedItemSize={50}
data={attachments?.placeholders}
renderItem={renderItem}
/>
<Paragraph
color={colors.secondary.paragraph}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 10
}}
>
<Icon
name="shield-key-outline"
size={SIZE.xs}
color={colors.primary.icon}
/>
{" "}All attachments are end-to-end encrypted.
</Paragraph>
</View>
);
};
AttachmentDialog.present = (note?: 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 { useThemeStore } from "../../stores/use-theme-store";
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
@@ -45,7 +43,7 @@ export const AuthMode = {
};
const AuthModal = () => {
const { colors } = useThemeColors();
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
const actionSheetRef = useRef();
@@ -69,9 +67,6 @@ const AuthModal = () => {
}
const close = () => {
useUserStore.setState({
disableAppLockRequests: false
});
actionSheetRef.current?.hide();
setCurrentAuthMode(AuthMode.login);
setVisible(false);
@@ -81,96 +76,73 @@ const AuthModal = () => {
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
onShow={() => {
useUserStore.setState({
disableAppLockRequests: true
});
}}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
useSafeArea={false}
bounce={false}
background={colors.primary.background}
background={colors.bg}
transparent={false}
animated={false}
centered={false}
enableSheetKeyboardHandler
>
<KeyboardAwareScrollView
style={{
width: "100%"
}}
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
/>
) : (
<Login
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
changeMode={(mode) => setCurrentAuthMode(mode)}
/>
)}
</KeyboardAwareScrollView>
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
/>
) : (
<Login
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
changeMode={(mode) => setCurrentAuthMode(mode)}
/>
)}
<View
style={{
position: "absolute",
paddingTop: Platform.OS === "android" ? 0 : insets.top,
top: 0,
top: Platform.OS === "ios" ? insets.top : 0,
zIndex: 999,
backgroundColor: colors.secondary.background,
width: "100%"
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
width: "100%",
height: 50,
justifyContent:
initialAuthMode.current !== AuthMode.welcomeSignup
? "space-between"
: "flex-end"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
width: "100%",
height: 50,
justifyContent:
initialAuthMode.current !== AuthMode.welcomeSignup
? "space-between"
: "flex-end"
}}
>
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
<IconButton
name="arrow-left"
onPress={() => {
hideAuth();
}}
color={colors.primary.paragraph}
/>
)}
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
<IconButton
name="arrow-left"
onPress={() => {
hideAuth();
}}
color={colors.pri}
/>
)}
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title="Skip"
onPress={() => {
hideAuth();
}}
iconSize={16}
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: 6
}}
/>
)}
</View>
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title="Skip for now"
onPress={() => {
hideAuth();
}}
iconSize={20}
type="gray"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: 6
}}
/>
)}
</View>
<Toast context="local" />

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,9 +22,9 @@ 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 { useThemeStore } from "../../stores/use-theme-store";
import DialogHeader from "../dialog/dialog-header";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
@@ -34,7 +34,7 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const ForgotPassword = () => {
const { colors } = useThemeColors("sheet");
const colors = useThemeStore((state) => state.colors);
const email = useRef();
const emailInputRef = useRef();
const [error, setError] = useState(false);
@@ -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,11 +113,11 @@ export const ForgotPassword = () => {
}}
>
<IconButton
style={{
customStyle={{
width: null,
height: null
}}
color={colors.primary.accent}
color={colors.accent}
name="email"
size={50}
/>
@@ -134,7 +134,7 @@ export const ForgotPassword = () => {
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
backgroundColor: colors.bg,
zIndex: 10,
width: "100%",
padding: 12

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