Compare commits
3 Commits
v3.0.17
...
fix-github
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e68dd129e | ||
|
|
2350d0e41d | ||
|
|
fbd5c8e3cd |
@@ -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/
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -5,9 +5,9 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
|
||||
113
.github/workflows/android.publish.internal.yml
vendored
@@ -6,11 +6,6 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||
NDK_CCACHE: ccache
|
||||
USE_CCACHE: 1
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -29,128 +24,30 @@ jobs:
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Install CCache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||
with:
|
||||
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
max-size: 1500M
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ccache-
|
||||
|
||||
- name: Setup CCache
|
||||
run: |
|
||||
ln -s $(which ccache) /usr/local/bin/gcc
|
||||
ln -s $(which ccache) /usr/local/bin/g++
|
||||
ln -s $(which ccache) /usr/local/bin/cc
|
||||
ln -s $(which ccache) /usr/local/bin/c++
|
||||
ln -s $(which ccache) /usr/local/bin/clang
|
||||
ln -s $(which ccache) /usr/local/bin/clang++
|
||||
ccache --set-config=base_dir=$PWD
|
||||
ccache --set-config=inode_cache=true
|
||||
ccache --set-config=compiler_check=content
|
||||
ccache --set-config=depend_mode=true
|
||||
ccache --set-config=file_clone=true
|
||||
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Patch CPP Projects to use CCache
|
||||
run: |
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: CCache Stats after build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@master
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
|
||||
- name: Sign apk files
|
||||
id: sign_apk
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
|
||||
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
cd apps/mobile/native/android/app/build/outputs/apk/release/
|
||||
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
|
||||
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
|
||||
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
|
||||
mv app-x86_64-release-unsigned-signed.apk notesnook-x86_64.apk
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@master
|
||||
with:
|
||||
path: apps/mobile
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1.1.1
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: alpha
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: internal
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
- name: Create release draft on Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
|
||||
repository: streetwriters/notesnook
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/native/android/app/build/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
86
.github/workflows/android.publish.yml
vendored
@@ -6,11 +6,6 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||
NDK_CCACHE: ccache
|
||||
USE_CCACHE: 1
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -22,7 +17,7 @@ jobs:
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
java-version: "11"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
@@ -33,66 +28,6 @@ jobs:
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Install CCache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||
with:
|
||||
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
max-size: 1500M
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ccache-
|
||||
|
||||
- name: Setup CCache
|
||||
run: |
|
||||
ln -s $(which ccache) /usr/local/bin/gcc
|
||||
ln -s $(which ccache) /usr/local/bin/g++
|
||||
ln -s $(which ccache) /usr/local/bin/cc
|
||||
ln -s $(which ccache) /usr/local/bin/c++
|
||||
ln -s $(which ccache) /usr/local/bin/clang
|
||||
ln -s $(which ccache) /usr/local/bin/clang++
|
||||
ccache --set-config=base_dir=$PWD
|
||||
ccache --set-config=inode_cache=true
|
||||
ccache --set-config=compiler_check=content
|
||||
ccache --set-config=depend_mode=true
|
||||
ccache --set-config=file_clone=true
|
||||
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Patch CPP Projects to use CCache
|
||||
run: |
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: CCache Stats after build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@master
|
||||
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
|
||||
|
||||
@@ -122,17 +57,6 @@ jobs:
|
||||
with:
|
||||
path: apps/mobile
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1.1.1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
- name: Create release draft on Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
@@ -146,11 +70,3 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/native/android/app/build/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
32
.github/workflows/desktop.publish.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
@@ -82,13 +82,12 @@ jobs:
|
||||
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
||||
tag_name: v${{ steps.app_metadata.outputs.app_version }}
|
||||
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
if: inputs.build-mac
|
||||
runs-on: macos-14
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -97,9 +96,6 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install setuptools
|
||||
run: brew install python-setuptools
|
||||
|
||||
- name: Setup notarization
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
@@ -126,7 +122,7 @@ jobs:
|
||||
platform: "MAC_OS"
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -147,7 +143,7 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
yarn electron-builder --mac mas --universal -p never
|
||||
npx electron-builder --mac mas --universal -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build zip and dmg
|
||||
@@ -157,13 +153,13 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
else
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
@@ -202,7 +198,7 @@ jobs:
|
||||
if: inputs.publish-snap
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -219,7 +215,7 @@ jobs:
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
yarn electron-builder --linux snap:x64 -p never
|
||||
npx electron-builder --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
@@ -227,9 +223,9 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
else
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
@@ -254,15 +250,15 @@ jobs:
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install TrustedSigning
|
||||
run: Install-Module -Name TrustedSigning -RequiredVersion 0.3.15 -Force -Repository PSGallery
|
||||
- name: Install AzureCodeSigning
|
||||
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
|
||||
shell: pwsh
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
54
.github/workflows/ios.publish.yml
vendored
@@ -22,45 +22,11 @@ jobs:
|
||||
- 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
|
||||
uses: yukiarrr/ios-build-action@v1.11.2
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
@@ -86,22 +52,16 @@ jobs:
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||
|
||||
- name: CCache Stats After Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: "Upload app to TestFlight"
|
||||
- name: 'Upload app to TestFlight'
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
with:
|
||||
with:
|
||||
app-path: Notesnook.ipa
|
||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload Notesnook.ipa to Github
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Upload to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.zip
|
||||
path: |
|
||||
Notesnook.ipa
|
||||
apps/mobile/native/ios/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
name: Notesnook.ipa
|
||||
path: Notesnook.ipa
|
||||
|
||||
69
.github/workflows/web.beta.publish.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Publish @notesnook/web Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
- "apps/desktop/**"
|
||||
- "apps/vericrypt/**"
|
||||
- "servers/**"
|
||||
- "packages/clipper/**"
|
||||
- "extensions/web-clipper/**"
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- opened
|
||||
- synchronize
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
- "apps/desktop/**"
|
||||
- "apps/vericrypt/**"
|
||||
- "servers/**"
|
||||
- "packages/clipper/**"
|
||||
- "extensions/web-clipper/**"
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate beta build
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
|
||||
id: pages
|
||||
with:
|
||||
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
cloudflare-project-name: notesnook-beta-app
|
||||
directory: ./apps/web/build
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
||||
8
.github/workflows/web.tests.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: npm run build:test:web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm install -D @playwright/test otplib dayjs @zip.js/zip.js
|
||||
npm install -D @playwright/test otplib
|
||||
|
||||
- name: Validate build extraction
|
||||
run: ls -ld ./apps/web/build/index.html
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
working-directory: apps/web
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}
|
||||
|
||||
40
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Publish @notesnook/web v3 Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate beta build
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
|
||||
id: pages
|
||||
with:
|
||||
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
cloudflare-project-name: notesnook-v3-app
|
||||
directory: ./apps/web/build
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
||||
2
.gitignore
vendored
@@ -10,4 +10,4 @@ nx-cloud.env
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
.nx
|
||||
@@ -11,5 +11,4 @@ apps/mobile/native/
|
||||
apps/web/public/an.js
|
||||
|
||||
# editor
|
||||
packages/editor/styles/
|
||||
/.nx/cache
|
||||
packages/editor/styles/
|
||||
3
apps/desktop/.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
output
|
||||
dist
|
||||
_catalog
|
||||
dist
|
||||
@@ -40,35 +40,12 @@ Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app for development:
|
||||
Now you can finally start the desktop app:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
npm run staging -- --rebuild
|
||||
```
|
||||
|
||||
This will compile and run the app in production mode but it won't generate any packages. To create the final packages, you'll have to run the following commands:
|
||||
|
||||
```bash
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --mac dmg --arm64 --x64 -publish never
|
||||
|
||||
# For Linux (AppImage)
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
|
||||
# For Windows
|
||||
npx electron-builder --win --publish never
|
||||
```
|
||||
|
||||
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
|
||||
|
||||
## Developer guide
|
||||
|
||||
### The tech stack
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
owner: streetwriters
|
||||
repo: notesnook
|
||||
provider: github
|
||||
updaterCacheDirName: Notesnook
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 520 B |
|
Before Width: | Height: | Size: 543 B After Width: | Height: | Size: 917 B |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 763 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 36 KiB |
1980
apps/desktop/package-lock.json
generated
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.17",
|
||||
"version": "2.6.15",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -11,11 +11,11 @@
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"better-sqlite3-multiple-ciphers": "^9.5.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "^6.2.1",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
@@ -25,14 +25,12 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.23.0",
|
||||
"kysely": "^0.27.3",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"prebuildify": "^6.0.1",
|
||||
"electron": "25.9.8",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.14.1"
|
||||
"undici": "^5.23.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -42,7 +40,7 @@
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "node scripts/build.mjs",
|
||||
"build": "tsc",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
@@ -56,7 +54,7 @@
|
||||
"productName": "Notesnook",
|
||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"npmRebuild": false,
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!*.chunk.js.map",
|
||||
@@ -66,17 +64,9 @@
|
||||
"!build/banner.jpg",
|
||||
"!build/*.ico",
|
||||
"!build/*.png",
|
||||
"!node_modules${/*}",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/lib",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/package.json",
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/bindings",
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||
"node_modules/sodium-native/index.js",
|
||||
"node_modules/sodium-native/package.json"
|
||||
"!node_modules${/*}"
|
||||
],
|
||||
"afterSign": "electron-builder-notarize",
|
||||
"afterPack": "./scripts/removeLocales.js",
|
||||
"mac": {
|
||||
"bundleVersion": "240",
|
||||
@@ -104,8 +94,7 @@
|
||||
"entitlements": "assets/entitlements.mac.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||
"gatekeeperAssess": false,
|
||||
"icon": "assets/icons/app.icns",
|
||||
"notarize": true
|
||||
"icon": "assets/icons/app.icns"
|
||||
},
|
||||
"dmg": {
|
||||
"contents": [
|
||||
@@ -149,7 +138,7 @@
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./scripts/sign.js",
|
||||
"sign": "./sign.js",
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs, { readFile, writeFile } from "fs/promises";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import yargs from "yargs-parser";
|
||||
import os from "os";
|
||||
import * as childProcess from "child_process";
|
||||
@@ -28,76 +28,90 @@ import { fileURLToPath } from "url";
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
|
||||
);
|
||||
|
||||
const sodiumNativePrebuildPath = (arch) =>
|
||||
path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`,
|
||||
`${os.platform()}-${arch}`
|
||||
);
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
await exec(`cd ${webAppPath} && npm run build:desktop`);
|
||||
}
|
||||
|
||||
// temporary until there's support for prebuilt binaries for linux ARM
|
||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
|
||||
// if (os.platform() === "win32")
|
||||
// await exec(
|
||||
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
// path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
// );
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`yarn run bundle:mas`);
|
||||
await exec(`npm run bundle:mas`);
|
||||
} else {
|
||||
await exec(`yarn run bundle`);
|
||||
await exec(`npm run bundle`);
|
||||
}
|
||||
|
||||
await exec(`yarn tsc`);
|
||||
await exec(`npx tsc`);
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("x64"))) {
|
||||
console.log("copying sodium-native-x64");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("x64"),
|
||||
path.join("build", "prebuilds", `${process.platform}-x64`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("ia32"))) {
|
||||
console.log("copying sodium-native-ia32");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("ia32"),
|
||||
path.join("build", "prebuilds", `${process.platform}-ia32`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("arm64"))) {
|
||||
console.log("copying sodium-native-arm64");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("arm64"),
|
||||
path.join("build", "prebuilds", `${process.platform}-arm64`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (args.run) {
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
await exec(`./output/darwin-unpacked/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
}
|
||||
|
||||
async function exec(cmd, cwd) {
|
||||
async function exec(cmd) {
|
||||
return childProcess.execSync(cmd, {
|
||||
env: { ...process.env, NOTESNOOK_STAGING: true },
|
||||
stdio: "inherit",
|
||||
cwd: cwd || process.cwd()
|
||||
env: process.env,
|
||||
stdio: "inherit"
|
||||
});
|
||||
}
|
||||
|
||||
async function patchBetterSQLite3() {
|
||||
const jsonPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"better-sqlite3-multiple-ciphers",
|
||||
"package.json"
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
json.version = "9.5.1";
|
||||
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||
json.repository.url =
|
||||
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||
|
||||
await writeFile(jsonPath, JSON.stringify(json));
|
||||
}
|
||||
|
||||
@@ -27,15 +27,21 @@ import crypto from "crypto";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`
|
||||
);
|
||||
const RUNNING_PROCESSES = [];
|
||||
const RESTARTABLE_PROCESSES = [];
|
||||
let lastBundleHash = null;
|
||||
const ENV = {
|
||||
...process.env,
|
||||
NO_COLOR: "true",
|
||||
FORCE_COLOR: "false",
|
||||
COLOR: "0"
|
||||
};
|
||||
|
||||
await onChange(true);
|
||||
|
||||
console.log("Watching...");
|
||||
@@ -52,21 +58,26 @@ process.on("SIGINT", async (s) => {
|
||||
async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("yarn electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`yarn run bundle`);
|
||||
execAsync(`yarn`, [`tsc`]);
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
@@ -78,7 +89,7 @@ async function onChange(first) {
|
||||
}
|
||||
|
||||
execAsync(
|
||||
"yarn",
|
||||
"npx",
|
||||
["electron", path.join("build", "electron.js")],
|
||||
true,
|
||||
cleanup
|
||||
@@ -91,7 +102,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
|
||||
const s = spawn(cmd[0], cmd.slice(1), {
|
||||
cwd,
|
||||
env: ENV,
|
||||
env: { ...process.env, NO_COLOR: "true" },
|
||||
shell: false
|
||||
});
|
||||
|
||||
@@ -105,15 +116,14 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
});
|
||||
}
|
||||
|
||||
async function exec(cmd, cwd) {
|
||||
async function exec(cmd) {
|
||||
try {
|
||||
console.log(">", cmd);
|
||||
|
||||
return execSync(cmd, {
|
||||
env: ENV,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
cwd: cwd || process.cwd()
|
||||
shell: false
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
@@ -126,7 +136,7 @@ function execAsync(cmd, args, restartable, onExit) {
|
||||
|
||||
const proc = spawn(cmd, args, {
|
||||
stdio: "inherit",
|
||||
env: ENV,
|
||||
env: process.env,
|
||||
shell: false
|
||||
});
|
||||
|
||||
@@ -169,10 +179,9 @@ async function cleanup() {
|
||||
}
|
||||
|
||||
async function isBundleSame() {
|
||||
const bundle = Buffer.concat([
|
||||
await fs.readFile(path.join(__dirname, "..", "build", "electron.js")),
|
||||
await fs.readFile(path.join(__dirname, "..", "build", "preload.js"))
|
||||
]);
|
||||
const bundle = await fs.readFile(
|
||||
path.join(__dirname, "..", "build", "electron.js")
|
||||
);
|
||||
|
||||
const hashSum = crypto.createHash("sha256");
|
||||
hashSum.update(bundle);
|
||||
|
||||
@@ -16,13 +16,11 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { writeFileSync, rmSync } = require("fs");
|
||||
const { writeFileSync, rmSync, readFileSync } = 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";
|
||||
@@ -33,7 +31,7 @@ module.exports = async function (configuration) {
|
||||
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}"`;
|
||||
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
|
||||
|
||||
console.debug("Signing", configuration.path, "using command", command);
|
||||
|
||||
@@ -41,13 +39,16 @@ module.exports = async function (configuration) {
|
||||
|
||||
console.debug("Signed", configuration.path);
|
||||
|
||||
rmSync(FilesCatalog);
|
||||
deleteCatalog();
|
||||
};
|
||||
|
||||
function createCatalog(path) {
|
||||
const catalogPath = join(__dirname, "_catalog");
|
||||
writeFileSync(catalogPath, relative(__dirname, path));
|
||||
return catalogPath;
|
||||
writeFileSync("_catalog", relative(__dirname, path));
|
||||
return join(__dirname, "_catalog");
|
||||
}
|
||||
|
||||
function deleteCatalog() {
|
||||
rmSync("_catalog");
|
||||
}
|
||||
|
||||
function psexec(cmd) {
|
||||
@@ -23,8 +23,6 @@ import { osIntegrationRouter } from "./os-integration";
|
||||
import { spellCheckerRouter } from "./spell-checker";
|
||||
import { updaterRouter } from "./updater";
|
||||
import { bridgeRouter } from "./bridge";
|
||||
import { safeStorageRouter } from "./safe-storage";
|
||||
import { windowRouter } from "./window";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -33,9 +31,7 @@ export const router = t.router({
|
||||
integration: osIntegrationRouter,
|
||||
spellChecker: spellCheckerRouter,
|
||||
updater: updaterRouter,
|
||||
bridge: bridgeRouter,
|
||||
safeStorage: safeStorageRouter,
|
||||
window: windowRouter
|
||||
bridge: bridgeRouter
|
||||
});
|
||||
|
||||
export const api = router.createCaller({});
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
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";
|
||||
@@ -31,8 +31,6 @@ import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
import { rm } from "fs/promises";
|
||||
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -56,23 +54,6 @@ export const osIntegrationRouter = t.router({
|
||||
config.zoomFactor = factor;
|
||||
}),
|
||||
|
||||
customDns: t.procedure.query(() => config.customDns),
|
||||
setCustomDns: t.procedure
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input: customDns }) => {
|
||||
if (customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
config.customDns = !!customDns;
|
||||
}),
|
||||
|
||||
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||
setProxyRules: t.procedure
|
||||
.input(z.string().optional())
|
||||
.mutation(({ input: proxyRules }) => {
|
||||
globalThis.window?.webContents.session.setProxy({ proxyRules });
|
||||
config.proxyRules = proxyRules || "";
|
||||
}),
|
||||
|
||||
privacyMode: t.procedure.query(() => config.privacyMode),
|
||||
setPrivacyMode: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
@@ -145,17 +126,10 @@ export const osIntegrationRouter = t.router({
|
||||
.input(z.object({ filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { filePath } = input;
|
||||
if (!filePath) return;
|
||||
return resolvePath(filePath);
|
||||
}),
|
||||
|
||||
deleteFile: t.procedure.input(z.string()).query(async ({ input }) => {
|
||||
await rm(input);
|
||||
}),
|
||||
|
||||
restart: t.procedure.query(() => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
@@ -184,45 +158,16 @@ export const osIntegrationRouter = t.router({
|
||||
}),
|
||||
bringToFront: t.procedure.query(() => bringToFront()),
|
||||
changeTheme: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
theme: Theme,
|
||||
windowControlsIconColor: z.string().optional(),
|
||||
backgroundColor: z.string().optional()
|
||||
})
|
||||
)
|
||||
.mutation(
|
||||
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
|
||||
if (windowControlsIconColor) {
|
||||
config.windowControlsIconColor = windowControlsIconColor;
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
!config.desktopSettings.nativeTitlebar
|
||||
)
|
||||
globalThis.window?.setTitleBarOverlay({
|
||||
symbolColor: windowControlsIconColor
|
||||
});
|
||||
}
|
||||
|
||||
if (backgroundColor) {
|
||||
config.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
setTheme(theme);
|
||||
}
|
||||
),
|
||||
.input(Theme)
|
||||
.mutation(({ input }) => setTheme(input)),
|
||||
|
||||
onThemeChanged: t.procedure.subscription(() =>
|
||||
observable<"dark" | "light">((emit) => {
|
||||
const updated = () => {
|
||||
nativeTheme.on("updated", () => {
|
||||
if (getTheme() === "system") {
|
||||
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
|
||||
}
|
||||
};
|
||||
nativeTheme.on("updated", updated);
|
||||
return () => {
|
||||
nativeTheme.off("updated", updated);
|
||||
};
|
||||
});
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
@@ -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"));
|
||||
})
|
||||
});
|
||||
@@ -118,13 +118,5 @@ export const spellCheckerRouter = t.router({
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
|
||||
config.isSpellCheckerEnabled = enabled;
|
||||
}),
|
||||
words: t.procedure.query(() =>
|
||||
globalThis.window?.webContents.session.listWordsInSpellCheckerDictionary()
|
||||
),
|
||||
deleteWord: t.procedure.input(z.string()).mutation(({ input: word }) => {
|
||||
globalThis.window?.webContents.session.removeWordFromSpellCheckerDictionary(
|
||||
word
|
||||
);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,143 +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 { Database, Statement } from "better-sqlite3-multiple-ciphers";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
type SQLiteCompatibleType =
|
||||
| number
|
||||
| string
|
||||
| Uint8Array
|
||||
| Array<number>
|
||||
| bigint
|
||||
| null;
|
||||
|
||||
export class SQLite {
|
||||
sqlite?: Database;
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
constructor() {
|
||||
console.log("new sqlite worker");
|
||||
}
|
||||
|
||||
async open(filePath: string) {
|
||||
if (this.sqlite) {
|
||||
console.error("Database is already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sqlite = require("better-sqlite3-multiple-ciphers")(
|
||||
filePath
|
||||
).unsafeMode(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async prepare(sql: string): Promise<Statement | undefined> {
|
||||
if (!this.sqlite) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = this.preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const prepared = this.sqlite.prepare(sql);
|
||||
if (!prepared) return;
|
||||
|
||||
this.preparedStatements.set(sql, prepared);
|
||||
|
||||
// reset retry count on success
|
||||
this.retryCounter[sql] = 0;
|
||||
return prepared;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (this.retryCounter[sql] < 5) {
|
||||
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return this.prepare(sql);
|
||||
} else this.retryCounter[sql] = 0;
|
||||
|
||||
if (ex instanceof Error) ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async exec<R>(
|
||||
sql: string,
|
||||
parameters: SQLiteCompatibleType[] = []
|
||||
): Promise<QueryResult<R>> {
|
||||
const prepared = await this.prepare(sql);
|
||||
if (!prepared) return { rows: [] };
|
||||
try {
|
||||
if (prepared.reader) {
|
||||
return {
|
||||
rows: prepared.all(parameters) as R[]
|
||||
};
|
||||
} else {
|
||||
const { changes, lastInsertRowid } = prepared.run(parameters);
|
||||
const numAffectedRows =
|
||||
changes !== undefined && changes !== null && !isNaN(changes)
|
||||
? BigInt(changes)
|
||||
: undefined;
|
||||
return {
|
||||
numAffectedRows,
|
||||
insertId:
|
||||
lastInsertRowid !== undefined && lastInsertRowid !== null
|
||||
? typeof lastInsertRowid === "bigint"
|
||||
? lastInsertRowid
|
||||
: BigInt(lastInsertRowid)
|
||||
: undefined,
|
||||
rows: [] as R[]
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (!this.sqlite) throw new Error("No database is not opened.");
|
||||
return await this.exec(sql, parameters);
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.sqlite) return;
|
||||
|
||||
this.preparedStatements.clear();
|
||||
this.sqlite.close();
|
||||
this.sqlite = undefined;
|
||||
}
|
||||
|
||||
async delete(filePath: string) {
|
||||
await this.close();
|
||||
await require("fs/promises").rm(filePath, {
|
||||
force: true,
|
||||
maxRetries: 5,
|
||||
retryDelay: 500
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,38 +28,16 @@ type UpdateInfo = { version: string };
|
||||
type Progress = { percent: number };
|
||||
|
||||
const t = initTRPC.create();
|
||||
let cancellationToken: CancellationToken | undefined = undefined;
|
||||
let downloadTimeout: NodeJS.Timeout | 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;
|
||||
clearTimeout(downloadTimeout);
|
||||
await new Promise<string[]>((resolve, reject) => {
|
||||
downloadTimeout = setTimeout(async () => {
|
||||
cancellationToken = new CancellationToken();
|
||||
autoUpdater.isUpdaterActive();
|
||||
await autoUpdater
|
||||
.downloadUpdate(cancellationToken)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => (cancellationToken = undefined));
|
||||
}, 1000);
|
||||
});
|
||||
const cancellationToken = new CancellationToken();
|
||||
await autoUpdater.downloadUpdate(cancellationToken);
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
if (cancellationToken) return;
|
||||
clearTimeout(downloadTimeout);
|
||||
await new Promise<void>((resolve) => {
|
||||
downloadTimeout = setTimeout(async () => {
|
||||
await autoUpdater
|
||||
.checkForUpdates()
|
||||
.catch(console.error)
|
||||
.finally(resolve);
|
||||
}, 1000);
|
||||
});
|
||||
await autoUpdater.checkForUpdates();
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
@@ -79,18 +57,7 @@ export const updaterRouter = t.router({
|
||||
"update-not-available"
|
||||
),
|
||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||
"update-available",
|
||||
() => {
|
||||
if (!config.automaticUpdates) return false;
|
||||
autoUpdater.emit("download-progress", {
|
||||
bytesPerSecond: 0,
|
||||
delta: 0,
|
||||
percent: 0,
|
||||
total: 100,
|
||||
transferred: 0
|
||||
});
|
||||
return true;
|
||||
}
|
||||
"update-available"
|
||||
),
|
||||
onError: createSubscription("error")
|
||||
});
|
||||
@@ -98,14 +65,12 @@ export const updaterRouter = t.router({
|
||||
function createSubscription<
|
||||
TName extends keyof AppUpdaterEvents,
|
||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
|
||||
>(eventName: TName) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<TReturnType>((emit) => {
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
if (handler?.(args[0])) return;
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.removeAllListeners(eventName);
|
||||
autoUpdater.addListener(eventName, listener);
|
||||
return () => {
|
||||
autoUpdater.removeListener(eventName, listener);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -20,5 +20,3 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
export * from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
export { type DesktopIntegration } from "./utils/config";
|
||||
export { SQLite } from "./api/sqlite-kysely.js";
|
||||
|
||||
@@ -35,7 +35,6 @@ import path from "path";
|
||||
import { bringToFront } from "./utils/bring-to-front";
|
||||
import { bridge } from "./api/bridge";
|
||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
@@ -83,28 +82,10 @@ async function createWindow() {
|
||||
size: 512,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
}),
|
||||
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
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,
|
||||
nodeIntegrationInWorker: true,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
@@ -131,7 +112,6 @@ async function createWindow() {
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
|
||||
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
@@ -141,7 +121,7 @@ async function createWindow() {
|
||||
setupJumplist();
|
||||
|
||||
if (isDevelopment())
|
||||
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
|
||||
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
@@ -157,9 +137,6 @@ async function createWindow() {
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
if (config.customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
|
||||
if (!isDevelopment()) registerProtocol();
|
||||
await createWindow();
|
||||
configureAutoUpdater();
|
||||
|
||||
@@ -20,14 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
// import type { NNCrypto } from "@notesnook/crypto";
|
||||
import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
@@ -40,5 +40,11 @@ process.once("loaded", async () => {
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.NativeNNCrypto =
|
||||
process.platform === "win32" &&
|
||||
process.arch !== "x64" &&
|
||||
process.arch !== "ia32"
|
||||
? undefined
|
||||
: require("@notesnook/crypto").NNCrypto;
|
||||
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
|
||||
@@ -25,11 +25,7 @@ Type=Application
|
||||
Version=${app.getVersion()}
|
||||
Name=${app.getName()}
|
||||
Comment=${app.getName()} startup script
|
||||
Exec=${
|
||||
process.env.APPIMAGE
|
||||
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
|
||||
: `${process.execPath}${hidden ? " --hidden" : ""}`
|
||||
}
|
||||
Exec=${process.execPath}${hidden ? " --hidden" : ""}
|
||||
StartupNotify=false
|
||||
Terminal=false`;
|
||||
|
||||
|
||||
@@ -20,22 +20,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { config } from "./config";
|
||||
|
||||
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
|
||||
? "beta"
|
||||
: "latest";
|
||||
async function configureAutoUpdater() {
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
|
||||
useMultipleRangeRequest: false,
|
||||
channel: CHANNEL
|
||||
url: `https://notesnook.com/releases/${process.platform}/`,
|
||||
useMultipleRangeRequest: false
|
||||
});
|
||||
|
||||
autoUpdater.autoDownload = config.automaticUpdates;
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
}
|
||||
|
||||
export { configureAutoUpdater };
|
||||
|
||||
@@ -25,8 +25,7 @@ export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
startMinimized: z.boolean().optional(),
|
||||
minimizeToSystemTray: z.boolean().optional(),
|
||||
closeToSystemTray: z.boolean().optional(),
|
||||
nativeTitlebar: z.boolean().optional()
|
||||
closeToSystemTray: z.boolean().optional()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
@@ -36,20 +35,13 @@ export const config = {
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false,
|
||||
nativeTitlebar: 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"
|
||||
automaticUpdates: true
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
|
||||
export function enableCustomDns() {
|
||||
app.configureHostResolver({
|
||||
secureDnsServers: [
|
||||
"https://mozilla.cloudflare-dns.com/dns-query",
|
||||
"https://dns.quad9.net/dns-query"
|
||||
],
|
||||
enableBuiltInResolver: true
|
||||
});
|
||||
}
|
||||
|
||||
export function disableCustomDns() {
|
||||
app.configureHostResolver({
|
||||
secureDnsServers: [],
|
||||
enableBuiltInResolver: true
|
||||
});
|
||||
}
|
||||
@@ -131,17 +131,6 @@ function setupMenu() {
|
||||
);
|
||||
}
|
||||
|
||||
if (params.mediaType === "image")
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
id: "copy-image",
|
||||
label: "Copy Image",
|
||||
click() {
|
||||
globalThis.window?.webContents.copyImageAt(params.x, params.y);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { net, protocol } from "electron";
|
||||
import { protocol } from "electron";
|
||||
import { isDevelopment } from "./index";
|
||||
import { createReadStream } from "fs";
|
||||
import { extname, normalize } from "path";
|
||||
@@ -62,7 +62,26 @@ function registerProtocol() {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
return net.fetch(request, { bypassCustomProtocolHandlers: true });
|
||||
if (request.headers.has("X-Content-Length")) {
|
||||
request.headers.set(
|
||||
"Content-Length",
|
||||
request.headers.get("X-Content-Length") || "0"
|
||||
);
|
||||
request.headers.delete("X-Content-Length");
|
||||
}
|
||||
const headers = Object.fromEntries(request.headers.entries());
|
||||
|
||||
return await fetch(request.url, {
|
||||
signal: request.signal,
|
||||
mode: request.mode,
|
||||
headers,
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
credentials: request.credentials,
|
||||
referrer: (request as any).referrer,
|
||||
duplex: "half",
|
||||
redirect: "manual"
|
||||
});
|
||||
}
|
||||
});
|
||||
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
|
||||
@@ -41,10 +41,7 @@ function setTheme(theme: Theme) {
|
||||
}
|
||||
|
||||
function getBackgroundColor() {
|
||||
return (
|
||||
config.backgroundColor ||
|
||||
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
|
||||
);
|
||||
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
|
||||
2
apps/mobile/.gitignore
vendored
@@ -38,7 +38,7 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
*.hprof
|
||||
**/.xcode.env.local
|
||||
ios/.xcode.env.local
|
||||
# Android/IntelliJ
|
||||
#
|
||||
rn-build-deps/
|
||||
|
||||
@@ -16,6 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "@azure/core-asynciterator-polyfill";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
@@ -40,25 +41,29 @@ import { useUserStore } from "./stores/use-user-store";
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
SettingsService.onFirstLaunch();
|
||||
let { appLockMode } = SettingsService.get();
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
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={{
|
||||
@@ -96,10 +101,10 @@ const App = () => {
|
||||
let currTheme =
|
||||
useThemeStore.getState().colorScheme === "dark"
|
||||
? SettingsService.getProperty("darkTheme")
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
: SettingsService.getProperty("lightTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element: () => JSX.Element) => {
|
||||
export const withTheme = (Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 28 KiB |
156
apps/mobile/app/common/analytics/index.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
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 { Platform } from "react-native";
|
||||
import { MMKV } from "../database/mmkv";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
|
||||
const baseUrl = "https://analytics.streetwriters.co/api/collect";
|
||||
|
||||
const UA =
|
||||
Platform.OS === "ios"
|
||||
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
|
||||
: `
|
||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @returns
|
||||
*/
|
||||
async function canUpdateAnalytics(route, conditions = []) {
|
||||
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
}
|
||||
|
||||
if (eventsList && eventsList[route]) {
|
||||
console.log("analytics: event already sent", route);
|
||||
return false;
|
||||
}
|
||||
if (route !== "/welcome") {
|
||||
for (let cond of conditions) {
|
||||
if (!eventsList || !eventsList[cond]) {
|
||||
console.log("analytics: conditions not met for event", route, cond);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("analytics: will send event", route);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveAnalytics(route, value = true) {
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
} else {
|
||||
eventsList = {};
|
||||
}
|
||||
eventsList[route] = value;
|
||||
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
|
||||
}
|
||||
|
||||
/**
|
||||
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
|
||||
* @param {Routes} prevRoute
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @param {boolean} once
|
||||
* @returns
|
||||
*/
|
||||
|
||||
async function pageView(
|
||||
route,
|
||||
prevRoute = "",
|
||||
conditions = ["/welcome"],
|
||||
once = true
|
||||
) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
|
||||
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
|
||||
hostname: `notesnook-${Platform.OS}`,
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "pageview"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log("analytics: event sent", route);
|
||||
await saveAnalytics(route);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEvent(type, value, once = true) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(type)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: "/",
|
||||
event_type: type,
|
||||
event_value: value,
|
||||
hostname: "notesnook-android-app",
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "event"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log(response.status);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
sendEvent,
|
||||
pageView,
|
||||
saveAnalytics
|
||||
};
|
||||
@@ -17,39 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// 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 = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
const APPLOCK_CIPHER = "applockCipher";
|
||||
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
@@ -60,227 +37,61 @@ const KEYSTORE_CONFIG = Platform.select({
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||
const key = getDatabaseKey();
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, 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_DB_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) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
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_DB_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
name,
|
||||
credentials.key,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeIOSKeychain(username, password) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
username,
|
||||
password,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// upgrades ios keychain to use accessGroups
|
||||
// so we have access to keychain in share extension.
|
||||
await upgradeIOSKeychain(credentials.username, credentials.password);
|
||||
return credentials.password;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +109,7 @@ export async function generateCryptoKey(password, salt) {
|
||||
let credentials = await Sodium.deriveKey(password, salt || null);
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
console.log("generateCryptoKey: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,8 +123,6 @@ export async function decrypt(password, data) {
|
||||
return undefined;
|
||||
let _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
@@ -326,11 +135,6 @@ export async function decryptMulti(password, data) {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,66 +16,48 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import "./logger";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key)
|
||||
throw new Error("Database setup failed, could not get database key");
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.43.108:5264",
|
||||
// AUTH_HOST: "http://192.168.43.108:8264",
|
||||
// SSE_HOST: "http://192.168.43.108:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.108:9264",
|
||||
// ISSUES_HOST: "http://192.168.43.108:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
database.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
return new RNSqliteDriver({ async: true, dbName: name });
|
||||
},
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}),
|
||||
tempStore: "memory",
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL",
|
||||
password: key
|
||||
}
|
||||
});
|
||||
}
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
export const db = database;
|
||||
let DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
|
||||
const setLogger = () => {
|
||||
DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
};
|
||||
|
||||
export { DatabaseLogger, setLogger };
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -16,30 +16,14 @@ 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 { initalize } from "@notesnook/core/dist/logger";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import { KV } from "./storage";
|
||||
|
||||
import { initialize } from "@notesnook/core/dist/logger";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
let loggerLoaded = false;
|
||||
const initializeLogger = async () => {
|
||||
if (loggerLoaded) return;
|
||||
await initialize({
|
||||
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"
|
||||
});
|
||||
setLogger();
|
||||
loggerLoaded = true;
|
||||
};
|
||||
initalize(new KV(LoggerStorage));
|
||||
|
||||
export { initializeLogger };
|
||||
export { LoggerStorage };
|
||||
|
||||
@@ -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 || [] : []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,15 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey
|
||||
removeCryptoKey,
|
||||
decryptMulti,
|
||||
encryptMulti
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
@@ -91,8 +91,7 @@ export class KV {
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
if (!keys) return true;
|
||||
return this.storage.removeItems(keys);
|
||||
return this.storage.removeItems(...keys);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
|
||||
@@ -16,60 +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";
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -80,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;
|
||||
}
|
||||
|
||||
@@ -18,20 +18,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core/dist/utils/filename";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -42,15 +46,15 @@ export const FileDownloadStatus = {
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAllAttachments() {
|
||||
const attachments = await db.attachments.all.ids();
|
||||
export function downloadAllAttachments() {
|
||||
const attachments = db.attachments.all;
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param {string[]} attachments
|
||||
* @param attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
@@ -80,15 +84,13 @@ export async function downloadAttachments(
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||
: `${cacheDir}/notesnook-attachments.zip`;
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const hash = attachment.hash;
|
||||
let attachment = attachments[i];
|
||||
const hash = attachment.metadata.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
@@ -112,20 +114,19 @@ export async function downloadAttachments(
|
||||
}
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Success,
|
||||
attachment: attachment
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Success
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
ToastManager.error(e, "Error downloading attachment");
|
||||
console.log("Error downloading attachment", e);
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
@@ -162,7 +163,7 @@ export async function downloadAttachments(
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
console.log("Error zipping attachments", e);
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
@@ -188,9 +189,9 @@ export default async function downloadAttachment(
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
DatabaseLogger.log("Attachment not found");
|
||||
console.log("attachment not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -205,46 +206,47 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
try {
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db.fs.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
|
||||
if (!(await exists(attachment.metadata.hash))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
|
||||
return await db.attachments.read(
|
||||
attachment.hash,
|
||||
attachment.metadata.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
attachment.metadata.filename,
|
||||
attachment.metadata.type
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
length: attachment.size,
|
||||
length: attachment.length,
|
||||
alg: attachment.alg,
|
||||
hash: attachment.hash,
|
||||
hashType: attachment.hashType,
|
||||
mime: attachment.mimeType,
|
||||
hash: attachment.metadata.hash,
|
||||
hashType: attachment.metadata.hashType,
|
||||
mime: attachment.metadata.type,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
@@ -252,13 +254,22 @@ export default async function downloadAttachment(
|
||||
);
|
||||
|
||||
if (!options.silent) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!isImage(attachment.metadata?.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
@@ -271,23 +282,20 @@ export default async function downloadAttachment(
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.hash,
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
console.log("download attachment error: ", e);
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -17,127 +17,70 @@ 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 NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
return true;
|
||||
}
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (size === 0) {
|
||||
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
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");
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
if (!downloadUrl) throw new Error("Unable to resolve download url");
|
||||
let totalSize = 0;
|
||||
console.log("Download starting");
|
||||
let request = RNFetchBlob.config({
|
||||
path: tempFilePath,
|
||||
path: path,
|
||||
IOSBackgroundTask: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.progress(async (recieved, total) => {
|
||||
.progress((recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(0, total, filename, recieved, "download");
|
||||
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
totalSize = total;
|
||||
console.log("downloading: ", recieved, total);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
};
|
||||
|
||||
let response = await request;
|
||||
console.log(response.info().headers);
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
response.info().headers?.["Content-Type"];
|
||||
|
||||
if (contentType === "application/xml") {
|
||||
const error = parseS3Error(await response.text());
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
await fileCheck(response, totalSize);
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
}
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
ToastManager.show({
|
||||
if (e.message !== "canceled") {
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -146,33 +89,42 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
});
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
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.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;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
if (!isInternetReachable) return;
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!isInternetReachable) return { success: true };
|
||||
const attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) return { failed: "Attachment not found." };
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
console.log("File Size", size);
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0)
|
||||
return {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
if (size <= 0) return { failed: "File length is 0." };
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
}
|
||||
|
||||
@@ -17,22 +17,17 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { checkAttachment, downloadFile } from "./download";
|
||||
import { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
|
||||
import {
|
||||
clearFileStorage,
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
hashBase64
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
import { cancelable } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
@@ -44,10 +39,5 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
checkAttachment
|
||||
};
|
||||
|
||||
@@ -17,17 +17,17 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
console.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
@@ -35,6 +35,14 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = !attachment.metadata.type
|
||||
? false
|
||||
: /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = !attachment.metadata.type
|
||||
? false
|
||||
: /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
@@ -42,15 +50,25 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
},
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
cipherData.outputType === "base64"
|
||||
? isPng || isJpeg
|
||||
? "cache"
|
||||
: "base64"
|
||||
: "text"
|
||||
);
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
console.log("file decrypted...");
|
||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||
const dCachePath = `${cacheDir}/${output}`;
|
||||
output = await compressToBase64(
|
||||
`file://${dCachePath}`,
|
||||
isPng ? "PNG" : "JPEG"
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
DatabaseLogger.error(e);
|
||||
console.log("readEncrypted", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +85,7 @@ export async function hashBase64(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64(data, key) {
|
||||
export async function writeEncryptedBase64({ data, key }) {
|
||||
await createCacheDir();
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
@@ -77,8 +95,6 @@ export async function writeEncryptedBase64(data, key) {
|
||||
});
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
console.log("encrypted file output: ", output);
|
||||
output.size = output.length;
|
||||
delete output.length;
|
||||
return {
|
||||
...output,
|
||||
alg: "xcha-stream"
|
||||
@@ -128,7 +144,7 @@ export async function clearFileStorage() {
|
||||
export async function createCacheDir() {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
DatabaseLogger.log("Cache directory created");
|
||||
console.log("Cache directory created");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,35 +175,7 @@ export async function migrateFilesFromCache() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
@@ -206,19 +194,16 @@ export async function exists(filename) {
|
||||
}
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.size + totalAbytes;
|
||||
const expectedFileSize = attachment.length + totalAbytes;
|
||||
|
||||
const stat = await RNFetchBlob.fs.stat(
|
||||
existsInAppGroup ? appGroupPath : path
|
||||
);
|
||||
|
||||
if (stat.size !== expectedFileSize) {
|
||||
DatabaseLogger.log(
|
||||
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
|
||||
);
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(console.log);
|
||||
@@ -229,29 +214,3 @@ export async function exists(filename) {
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter((file) => !appGroupFiles.includes(file));
|
||||
}
|
||||
|
||||
return missingFiles;
|
||||
}
|
||||
|
||||
export async function getCacheSize() {
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -17,47 +17,29 @@ 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 { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, 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 { cacheDir, getUploadedFileSize } from "./utils";
|
||||
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
const uploadedFileSize = await getUploadedFileSize(filename);
|
||||
|
||||
if (uploadedFileSize === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (uploadedFileSize !== 0) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
|
||||
const uploadUrl = res.ok ? await res.text() : await res.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
}
|
||||
|
||||
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 =
|
||||
@@ -68,15 +50,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
uploadFilePath = appGroupPath;
|
||||
exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
let request = RNFetchBlob.config({
|
||||
@@ -113,8 +87,14 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
DatabaseLogger.info(
|
||||
`File upload status: ${filename}, ${status}, ${text}`
|
||||
);
|
||||
let attachment = await db.attachments.attachment(filename);
|
||||
let attachment = db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (
|
||||
!isImage(attachment.metadata.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
throw new Error(
|
||||
@@ -127,10 +107,8 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
return result;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,10 @@ 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 * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import hosts from "@notesnook/core/dist/utils/constants";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export const cacheDir =
|
||||
@@ -37,36 +34,24 @@ export function getRandomId(prefix) {
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
export function extractValueFromXmlTag(code, xml) {
|
||||
if (!xml.includes(code)) return `Unknown ${code}`;
|
||||
return xml.slice(
|
||||
xml.indexOf(`<${code}>`) + code.length + 2,
|
||||
xml.indexOf(`</${code}>`)
|
||||
);
|
||||
}
|
||||
|
||||
const error = {
|
||||
Code: "UNKNOWN",
|
||||
Message: xml
|
||||
};
|
||||
|
||||
try {
|
||||
if (!xml) return error;
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
|
||||
const ErrorElement = doc.getElementsByTagName("Error")[0];
|
||||
if (!ErrorElement) return error;
|
||||
|
||||
for (const child of ErrorElement.children) {
|
||||
if (
|
||||
child.textContent &&
|
||||
(child.tagName === "Code" || child.tagName === "Message")
|
||||
)
|
||||
error[child.tagName] = child.textContent;
|
||||
export async function fileCheck(response, totalSize) {
|
||||
if (totalSize < 1000) {
|
||||
let text = await response.text();
|
||||
if (text.startsWith("<?xml")) {
|
||||
let errorJson = {
|
||||
Code: extractValueFromXmlTag("Code", text),
|
||||
Message: extractValueFromXmlTag("Message", text)
|
||||
};
|
||||
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
|
||||
}
|
||||
return error;
|
||||
} catch (e) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +59,12 @@ export function cancelable(operation) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
};
|
||||
return (filename, requestOptions) => {
|
||||
return (filename, { url, headers }) => {
|
||||
return {
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
execute: () => operation(filename, { url, headers }, cancelToken),
|
||||
cancel: async () => {
|
||||
await cancelToken.cancel();
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -105,21 +91,3 @@ export async function releasePermissions(path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,18 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { FlatList, View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
|
||||
export const Announcement = ({ color }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return !announcement || selectionMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
@@ -47,8 +45,7 @@ export const Announcement = ({ color }) => {
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.secondary.background,
|
||||
paddingBottom: 12,
|
||||
...getContainerBorder(colors.secondary.background)
|
||||
paddingBottom: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -20,13 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { Linking, View } from "react-native";
|
||||
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eCloseAnnouncementDialog } from "../../utils/events";
|
||||
import Sync from "../../services/sync";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PricingPlans } from "../premium/pricing-plans";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import { allowedOnPlatform, getStyle } from "./functions";
|
||||
|
||||
@@ -54,6 +56,13 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else if (item.type === "force-sync") {
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Progress.present();
|
||||
Sync.run("global", true, true, () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
@@ -94,7 +103,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.sm}
|
||||
type="plain"
|
||||
type="gray"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
@@ -140,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
type="gray"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
</Heading>
|
||||
|
||||
<Button
|
||||
type="plain"
|
||||
type="gray"
|
||||
icon="close"
|
||||
height={null}
|
||||
onPress={() => {
|
||||
|
||||
215
apps/mobile/app/components/app-lock-overlay/index.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { db } from "../../common/database";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
const AppLockedOverlay = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const appLocked = useUserStore((state) => state.appLocked);
|
||||
const lockApp = useUserStore((state) => state.lockApp);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const passwordInputRef = useRef();
|
||||
const password = useRef();
|
||||
const appState = useAppState();
|
||||
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
|
||||
const onUnlockAppRequested = useCallback(async () => {
|
||||
if (!(await BiometricService.isBiometryAvailable())) return;
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await NotesnookModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
}
|
||||
useSettingStore.getState().setRequestBiometrics(true);
|
||||
|
||||
let unlocked = await BiometricService.validateUser(
|
||||
"Unlock to access your notes",
|
||||
""
|
||||
);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
useSettingStore.getState().setRequestBiometrics(false);
|
||||
}, 1);
|
||||
}, [lockApp]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password.current) return;
|
||||
try {
|
||||
let unlocked = await db.user.verifyPassword(password.current);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
biometricUnlockAwaitingUserInput.current ||
|
||||
useUserStore.getState().disableAppLockRequests
|
||||
)
|
||||
return;
|
||||
if (appLocked && appState === "active") {
|
||||
biometricUnlockAwaitingUserInput.current = true;
|
||||
onUnlockAppRequested();
|
||||
}
|
||||
}, [appState, onUnlockAppRequested, appLocked]);
|
||||
|
||||
return (
|
||||
appLocked && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
width:
|
||||
deviceMode !== "mobile"
|
||||
? "50%"
|
||||
: Platform.OS == "ios"
|
||||
? "95%"
|
||||
: "100%",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="fingerprint"
|
||||
size={100}
|
||||
customStyle={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
marginBottom: 20,
|
||||
marginTop: user ? 0 : 50
|
||||
}}
|
||||
onPress={onUnlockAppRequested}
|
||||
color={colors.primary.border}
|
||||
/>
|
||||
<Heading
|
||||
color={colors.primary.heading}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Unlock your notes
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
maxWidth: "90%"
|
||||
}}
|
||||
>
|
||||
{"Please verify it's you"}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
secureTextEntry
|
||||
placeholder="Enter account password"
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: user ? 25 : 25
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
<>
|
||||
<Button
|
||||
title="Continue"
|
||||
type="accent"
|
||||
onPress={onSubmit}
|
||||
width={250}
|
||||
height={45}
|
||||
style={{
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
title="Unlock with Biometrics"
|
||||
width={250}
|
||||
onPress={onUnlockAppRequested}
|
||||
icon={"fingerprint"}
|
||||
type="transparent"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLockedOverlay;
|
||||
@@ -1,334 +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,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
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 { ToastManager } from "../../services/event-manager";
|
||||
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 { Toast } from "../toast";
|
||||
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 { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
|
||||
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 { height } = useWindowDimensions();
|
||||
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);
|
||||
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);
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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();
|
||||
}
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
}
|
||||
}, [appState, onUnlockAppRequested, appLocked]);
|
||||
|
||||
return appLocked ? (
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
justifyContent: "center",
|
||||
minHeight: height
|
||||
}}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Toast context="local" />
|
||||
<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>
|
||||
</KeyboardAwareScrollView>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AppLockedOverlay;
|
||||
@@ -17,13 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
@@ -31,18 +27,14 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import picker from "../../screens/editor/tiptap/picker";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import {
|
||||
eCloseAttachmentDialog,
|
||||
eCloseSheet,
|
||||
eDBItemUpdate,
|
||||
eOnLoadNote
|
||||
} from "../../utils/events";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -51,43 +43,31 @@ import { openNote } from "../list-items/note/wrapper";
|
||||
import { DateMeta } from "../properties/date-meta";
|
||||
import { Button } from "../ui/button";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
close,
|
||||
setAttachments,
|
||||
fwdRef
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
|
||||
close?: () => void;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const contextId = attachment.hash;
|
||||
const [filename, setFilename] = useState(attachment.filename);
|
||||
const contextId = attachment.metadata.hash;
|
||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||
const [currentProgress] = useAttachmentProgress(attachment);
|
||||
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
const [loading, setLoading] = useState<{
|
||||
name?: string;
|
||||
}>({});
|
||||
const [failed, setFailed] = useState(attachment.failed);
|
||||
const [notes, setNotes] = useState([]);
|
||||
const [loading, setLoading] = useState({
|
||||
name: null
|
||||
});
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs().cancel(attachment.hash);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
await db.fs.cancel(attachment.metadata.hash);
|
||||
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,22 +98,19 @@ const Actions = ({
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
||||
setFailed(result.failed);
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + result.failed,
|
||||
let res = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
ToastEvent.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
setFailed(undefined);
|
||||
db.attachments.markAsFailed(attachment.id);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
ToastManager.show({
|
||||
setFailed(null);
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
@@ -142,7 +119,7 @@ const Actions = ({
|
||||
|
||||
setAttachments();
|
||||
setLoading({
|
||||
name: undefined
|
||||
name: null
|
||||
});
|
||||
},
|
||||
icon: "file-check"
|
||||
@@ -151,20 +128,19 @@ const Actions = ({
|
||||
name: "Rename",
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
context: contextId as any,
|
||||
context: contextId,
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
defaultValue: attachment.filename,
|
||||
defaultValue: attachment.metadata.filename,
|
||||
positivePress: async (value) => {
|
||||
if (value && value.length > 0) {
|
||||
await db.attachments.add({
|
||||
hash: attachment.hash,
|
||||
hash: attachment.metadata.hash,
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
@@ -175,40 +151,34 @@ const Actions = ({
|
||||
{
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
const relations = await db.relations.to(attachment, "note").get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
await db.attachments.remove(attachment.metadata.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?.();
|
||||
close();
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
];
|
||||
|
||||
const getNotes = useCallback(() => {
|
||||
let allNotes = db.notes.all;
|
||||
let attachmentNotes = attachment.noteIds?.map((id) => {
|
||||
let index = allNotes?.findIndex((note) => id === note.id);
|
||||
if (index !== -1) {
|
||||
return allNotes[index];
|
||||
} else {
|
||||
return {
|
||||
type: "notfound",
|
||||
title: `Note with id ${id} does not exist.`,
|
||||
id: id
|
||||
};
|
||||
}
|
||||
});
|
||||
return attachmentNotes;
|
||||
}, [attachment.noteIds]);
|
||||
|
||||
useEffect(() => {
|
||||
db.relations
|
||||
.to(attachment, "note")
|
||||
.selector.items()
|
||||
.then((items) => {
|
||||
setNotes(items);
|
||||
});
|
||||
}, [attachment]);
|
||||
setNotes(getNotes());
|
||||
}, [attachment, getNotes]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -221,11 +191,10 @@ const Actions = ({
|
||||
}}
|
||||
>
|
||||
<Dialog context={contextId} />
|
||||
<SheetProvider context={contextId} />
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
marginBottom: notes && notes.length > 0 ? 0 : 12
|
||||
}}
|
||||
>
|
||||
@@ -242,28 +211,44 @@ const Actions = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 6,
|
||||
gap: 10
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{attachment.mimeType}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.metadata.type}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{formatBytes(attachment.size)}
|
||||
<Paragraph
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{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}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{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"
|
||||
@@ -272,7 +257,7 @@ const Actions = ({
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.hash}
|
||||
{attachment.metadata.hash}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -283,7 +268,7 @@ const Actions = ({
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
@@ -299,15 +284,25 @@ const Actions = ({
|
||||
</Heading>
|
||||
|
||||
{notes.map((item) => (
|
||||
<Pressable
|
||||
<PressableButton
|
||||
onPress={async () => {
|
||||
if (item.type === "notfound") {
|
||||
ToastEvent.show({
|
||||
heading: "Note not found",
|
||||
message:
|
||||
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
await sleep(150);
|
||||
eSendEvent(eCloseAttachmentDialog);
|
||||
await sleep(300);
|
||||
openNote(item, (item as any).type === "trash");
|
||||
openNote(item, item.type === "trash");
|
||||
}}
|
||||
style={{
|
||||
customStyle={{
|
||||
paddingVertical: 12,
|
||||
alignItems: "flex-start",
|
||||
|
||||
@@ -316,7 +311,7 @@ const Actions = ({
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||
</Pressable>
|
||||
</PressableButton>
|
||||
))}
|
||||
</>
|
||||
</View>
|
||||
@@ -326,16 +321,17 @@ const Actions = ({
|
||||
<Button
|
||||
key={item.name}
|
||||
buttonType={{
|
||||
text:
|
||||
item.name === "Delete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
text: item.on
|
||||
? colors.primary.accent
|
||||
: item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
title={item.name}
|
||||
icon={item.icon}
|
||||
loading={loading?.name === item.name}
|
||||
type="plain"
|
||||
type={item.on ? "shade" : "gray"}
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
@@ -354,7 +350,7 @@ const Actions = ({
|
||||
{failed ? (
|
||||
<Notice
|
||||
type="alert"
|
||||
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
|
||||
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
|
||||
size="small"
|
||||
/>
|
||||
) : null}
|
||||
@@ -363,11 +359,7 @@ const Actions = ({
|
||||
);
|
||||
};
|
||||
|
||||
Actions.present = (
|
||||
attachment: Attachment,
|
||||
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
|
||||
context?: string
|
||||
) => {
|
||||
Actions.present = (attachment, set, context) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close) => (
|
||||
171
apps/mobile/app/components/attachments/attachment-item.js
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Actions from "./actions";
|
||||
|
||||
function getFileExtension(filename) {
|
||||
var ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
|
||||
export const AttachmentItem = ({
|
||||
attachment,
|
||||
encryption,
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
encryption
|
||||
);
|
||||
const onPress = () => {
|
||||
if (!pressable) return;
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
type="grayBg"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: -5
|
||||
}}
|
||||
>
|
||||
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
||||
|
||||
<Paragraph
|
||||
adjustsFontSizeToFit
|
||||
size={6}
|
||||
color={colors.static.white}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
>
|
||||
{getFileExtension(attachment.metadata.filename).toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.sm - 1}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginBottom: 2.5
|
||||
}}
|
||||
numberOfLines={1}
|
||||
lineBreakMode="middle"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{attachment.metadata.filename}
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentProgress ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
marginLeft: 5,
|
||||
marginTop: 5,
|
||||
marginRight: -5
|
||||
}}
|
||||
>
|
||||
<ProgressCircleComponent
|
||||
size={SIZE.xxl}
|
||||
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
|
||||
showsText
|
||||
textStyle={{
|
||||
fontSize: 10
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
formatText={(progress) => (progress * 100).toFixed(0)}
|
||||
borderWidth={0}
|
||||
thickness={2}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<>
|
||||
{attachment.failed ? (
|
||||
<IconButton
|
||||
onPress={onPress}
|
||||
name="alert-circle-outline"
|
||||
color={colors.error.paragraph}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -21,10 +21,7 @@ import React, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -32,19 +29,8 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
|
||||
const DownloadAttachments = ({
|
||||
close,
|
||||
attachments,
|
||||
isNote,
|
||||
update
|
||||
}: {
|
||||
attachments: VirtualizedGrouping<Attachment>;
|
||||
close?: ((ctx?: string | undefined) => void) | undefined;
|
||||
isNote?: boolean;
|
||||
update?: (props: PresentSheetOptions) => void;
|
||||
}) => {
|
||||
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
@@ -53,58 +39,63 @@ const DownloadAttachments = ({
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef<string>();
|
||||
const groupId = useRef();
|
||||
|
||||
const onDownload = async () => {
|
||||
update?.({
|
||||
update({
|
||||
disableClosing: true
|
||||
} as PresentSheetOptions);
|
||||
});
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
await attachments.ids(),
|
||||
(progress: number, statusText: string) =>
|
||||
setProgress({ value: progress, statusText }),
|
||||
attachments,
|
||||
(progress, statusText) => setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update?.({
|
||||
update({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update?.({
|
||||
update({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
});
|
||||
canceled.current = true;
|
||||
if (!groupId.current) return;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs().cancel(groupId.current);
|
||||
await db.fs.cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
setResult(new Map());
|
||||
groupId.current = undefined;
|
||||
groupId.current = null;
|
||||
};
|
||||
|
||||
const successResults = () => {
|
||||
const results = [];
|
||||
for (let [key, value] of result.entries()) {
|
||||
if (value.status === 1) results.push(db.attachments.attachment(key));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
const failedResults = () => {
|
||||
const results = [];
|
||||
for (const value of result.values()) {
|
||||
if (value.status === 0) results.push(value.attachment);
|
||||
for (let [key, value] of result.entries()) {
|
||||
if (value.status === 0) results.push(db.attachments.attachment(key));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments?.placeholders?.length - failedResults().length;
|
||||
attachments.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments?.placeholders.length
|
||||
attachments.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
@@ -166,11 +157,7 @@ const DownloadAttachments = ({
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={
|
||||
progress.value
|
||||
? progress.value / attachments.placeholders?.length
|
||||
: 0
|
||||
}
|
||||
progress={progress.value ? progress.value / attachments.length : 0}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
@@ -187,7 +174,7 @@ const DownloadAttachments = ({
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments.placeholders : undefined}
|
||||
data={downloading ? attachments : []}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -202,15 +189,14 @@ const DownloadAttachments = ({
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(index) => "attachment_download" + index}
|
||||
renderItem={({ index }) => {
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
id={index}
|
||||
attachment={item}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -223,9 +209,7 @@ const DownloadAttachments = ({
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
onPress={close}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
@@ -243,10 +227,8 @@ const DownloadAttachments = ({
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="secondary"
|
||||
onPress={close}
|
||||
type="grayBg"
|
||||
title="No"
|
||||
/>
|
||||
<Button
|
||||
@@ -276,11 +258,7 @@ const DownloadAttachments = ({
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (
|
||||
context: string,
|
||||
attachments: VirtualizedGrouping<Attachment>,
|
||||
isNote?: boolean
|
||||
) => {
|
||||
DownloadAttachments.present = (context, attachments, isNote) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
@@ -17,25 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
Attachment,
|
||||
Note,
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
@@ -43,93 +34,82 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
isAudio,
|
||||
isDocument,
|
||||
isImage,
|
||||
isVideo
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
|
||||
const DEFAULT_SORTING: SortOptions = {
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
|
||||
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
export const AttachmentDialog = ({ note }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<Attachment>>();
|
||||
const attachmentSearchValue = useRef<string>();
|
||||
const searchTimer = useRef<NodeJS.Timeout>();
|
||||
const [attachments, setAttachments] = useState(
|
||||
note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])]
|
||||
);
|
||||
|
||||
const attachmentSearchValue = useRef();
|
||||
const searchTimer = useRef();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
if (note) {
|
||||
db.attachments
|
||||
.ofNote(note.id, "all")
|
||||
.sorted({
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => {
|
||||
setAttachments(attachments);
|
||||
});
|
||||
} else {
|
||||
db.attachments.all
|
||||
.sorted({
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
}
|
||||
}, [note]);
|
||||
const onChangeText = (text) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [note, refresh]);
|
||||
|
||||
const onChangeText = async (text: string) => {
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
refresh();
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
const results = await db.lookup
|
||||
.attachments(attachmentSearchValue.current as string)
|
||||
.sorted();
|
||||
searchTimer.current = setTimeout(() => {
|
||||
let results = db.lookup.attachments(
|
||||
attachments,
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
setAttachments(results);
|
||||
|
||||
setAttachments(filterAttachments(currentFilter, results));
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ index }: { item: boolean; index: number }) => (
|
||||
const renderItem = ({ item }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={async () => {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
setAttachments={() => {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
attachment={item}
|
||||
context="attachments-list"
|
||||
/>
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
|
||||
for (let i = 0; i < attachments.placeholders.length; i++) {
|
||||
const attachment = (await attachments.item(i))?.item;
|
||||
if (!attachment) continue;
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
const checkedAttachments = [];
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
if (result.failed) {
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
await db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(attachment.id);
|
||||
await db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
checkedAttachments.push(
|
||||
db.attachments.attachment(attachment.metadata.hash)
|
||||
);
|
||||
setAttachments([...checkedAttachments]);
|
||||
}
|
||||
refresh();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -156,40 +136,32 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = async (type: string) => {
|
||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||
const filterAttachments = (type, _attachments) => {
|
||||
const attachments =
|
||||
_attachments || note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
switch (type) {
|
||||
case "all":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.all;
|
||||
break;
|
||||
return attachments;
|
||||
case "images":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "images")
|
||||
: db.attachments.images;
|
||||
break;
|
||||
return attachments.filter((attachment) =>
|
||||
isImage(attachment.metadata.type)
|
||||
);
|
||||
case "video":
|
||||
items = items = note
|
||||
? db.attachments.ofNote(note.id, "videos")
|
||||
: db.attachments.videos;
|
||||
break;
|
||||
return attachments.filter((attachment) =>
|
||||
isVideo(attachment.metadata.type)
|
||||
);
|
||||
case "audio":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "audio")
|
||||
: db.attachments.audios;
|
||||
break;
|
||||
return attachments.filter((attachment) =>
|
||||
isAudio(attachment.metadata.type)
|
||||
);
|
||||
case "documents":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "documents")
|
||||
: db.attachments.documents;
|
||||
return attachments.filter((attachment) =>
|
||||
isDocument(attachment.metadata.type)
|
||||
);
|
||||
}
|
||||
|
||||
return await items.sorted({
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -228,7 +200,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
) : (
|
||||
<IconButton
|
||||
name="check-all"
|
||||
style={{
|
||||
customStyle={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
@@ -241,13 +213,12 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
customStyle={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
@@ -264,7 +235,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -284,11 +255,12 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
>
|
||||
{attachmentTypes.map((item) => (
|
||||
<Button
|
||||
type={
|
||||
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
|
||||
}
|
||||
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
title={
|
||||
item.title +
|
||||
` (${filterAttachments(item.filterBy)?.length || 0})`
|
||||
}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
@@ -298,9 +270,9 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
? "transparent"
|
||||
: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
onPress={() => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(await filterAttachments(item.filterBy));
|
||||
setAttachments(filterAttachments(item.filterBy));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -331,7 +303,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments?.placeholders}
|
||||
data={attachments}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
@@ -354,9 +326,8 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
AttachmentDialog.present = (note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />,
|
||||
keyboardHandlerDisabled: true
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
});
|
||||
};
|
||||
@@ -158,7 +158,7 @@ const AuthModal = () => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
type="gray"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
|
||||
@@ -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,24 +64,18 @@ export const ChangePassword = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (result.error)
|
||||
throw new Error(`Failed to create backup: ${result.error}`);
|
||||
|
||||
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",
|
||||
@@ -101,7 +92,6 @@ export const ChangePassword = () => {
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<DialogHeader
|
||||
title="Change password"
|
||||
paragraph="Enter your old and new passwords"
|
||||
@@ -139,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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
|
||||
|
||||
const sendRecoveryEmail = async () => {
|
||||
if (!email.current || error) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Account email is required.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
|
||||
SettingsService.set({
|
||||
lastRecoveryEmailTime: Date.now()
|
||||
});
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Check your email to reset password",
|
||||
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
|
||||
type: "success",
|
||||
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
|
||||
setSent(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Recovery email not sent",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -113,7 +113,7 @@ export const ForgotPassword = () => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
customStyle={{
|
||||
width: null,
|
||||
height: null
|
||||
}}
|
||||
|
||||
@@ -59,6 +59,22 @@ const Auth = ({ navigation, route }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* {initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
customStyle={{
|
||||
position: 'absolute',
|
||||
zIndex: 999,
|
||||
left: 12,
|
||||
top: Platform.OS === 'ios' ? 12 + insets.top : insets.top
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
<Toast context="local" />
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -17,16 +17,12 @@ 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, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
@@ -38,6 +34,10 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import Sync from "../../services/sync";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -66,7 +66,7 @@ export const Login = ({ changeMode }) => {
|
||||
Progress.present();
|
||||
setTimeout(() => {
|
||||
if (!useUserStore.getState().syncing) {
|
||||
Sync.run("global", false, "full");
|
||||
Sync.run("global", false, true);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
@@ -103,7 +103,7 @@ export const Login = ({ changeMode }) => {
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomWidth: 1,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
@@ -183,11 +183,7 @@ export const Login = ({ changeMode }) => {
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -226,7 +222,7 @@ export const Login = ({ changeMode }) => {
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
type="gray"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -246,7 +242,6 @@ export const Login = ({ changeMode }) => {
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
|
||||
@@ -19,12 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Modal, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastManager,
|
||||
ToastEvent,
|
||||
eSendEvent,
|
||||
eSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
@@ -37,7 +37,6 @@ import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
@@ -47,6 +46,7 @@ import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
@@ -69,9 +69,6 @@ export const SessionExpired = () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -87,11 +84,8 @@ export const SessionExpired = () => {
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
setVisible(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -108,15 +102,11 @@ export const SessionExpired = () => {
|
||||
|
||||
const open = React.useCallback(async () => {
|
||||
try {
|
||||
let res = await db.tokenManager.getToken();
|
||||
let res = await db.user.tokenManager.getToken();
|
||||
if (!res) throw new Error("no token found");
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
if (db.user.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
|
||||
const key = await db.user.getEncryptionKey();
|
||||
if (!key) throw new Error("No encryption key found.");
|
||||
|
||||
Sync.run("global", false, "full", async (complete) => {
|
||||
Sync.run("global", false, true, async (complete) => {
|
||||
if (!complete) {
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
@@ -131,14 +121,12 @@ export const SessionExpired = () => {
|
||||
setVisible(false);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
setFocused(false);
|
||||
setVisible(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
@@ -151,15 +139,9 @@ export const SessionExpired = () => {
|
||||
animated={false}
|
||||
centered={false}
|
||||
onShow={async () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
await sleep(300);
|
||||
passwordInputRef.current?.focus();
|
||||
setFocused(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}}
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
@@ -184,7 +166,7 @@ export const SessionExpired = () => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
customStyle={{
|
||||
width: 60,
|
||||
height: 60
|
||||
}}
|
||||
|
||||
@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -35,7 +35,6 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -53,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const { width, height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
type: "error",
|
||||
@@ -78,7 +77,6 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
hideAuth();
|
||||
SettingsService.setProperty("encryptedBackup", true);
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
@@ -87,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Signup failed",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -113,7 +111,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
|
||||
@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
@@ -32,12 +32,11 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -139,15 +138,12 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
setSending(false);
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
ToastManager.error(e, "Error sending 2FA Code", "local");
|
||||
ToastEvent.error(e, "Error sending 2FA Code", "local");
|
||||
}
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
@@ -155,7 +151,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
customStyle={{
|
||||
width: 70,
|
||||
height: 70
|
||||
}}
|
||||
@@ -187,7 +183,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={seconds ? "plain" : "transparent"}
|
||||
type={seconds ? "gray" : "transparent"}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
@@ -218,7 +214,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
@@ -230,7 +225,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
height: 60,
|
||||
borderWidth: 0,
|
||||
@@ -253,7 +247,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
|
||||
<Button
|
||||
title={secondaryMethodsText[currentMethod.method]}
|
||||
type="plain"
|
||||
type="gray"
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
@@ -261,7 +255,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
) : (
|
||||
<>
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
<PressableButton
|
||||
key={item.title}
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
@@ -269,7 +263,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
isPrimary: false
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
customStyle={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
marginTop: 0,
|
||||
@@ -281,8 +275,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
type="grayAccent"
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
marginRight: 10
|
||||
@@ -298,12 +292,12 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
>
|
||||
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
</PressableButton>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { db } from "../../common/database";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
(!password.current && step === LoginSteps.passwordAuth) ||
|
||||
(!email.current && step === LoginSteps.emailAuth)
|
||||
) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
type: "error",
|
||||
@@ -83,7 +83,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 500);
|
||||
}, 1);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
@@ -121,7 +121,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const finishWithError = async (e) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Login failed",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -135,7 +135,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
PremiumService.setPremiumStatus();
|
||||
setUser(user);
|
||||
clearMessage();
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Login successful",
|
||||
message: `Logged in as ${user.email}`,
|
||||
type: "success",
|
||||
|
||||
@@ -17,23 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ItemMap, ItemType } from "@notesnook/core";
|
||||
import {
|
||||
ResolvedItemOptions,
|
||||
useResolvedItem
|
||||
} from "../hooks/use-resolved-item";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
type ResolvedItemProps<TItemType extends ItemType> =
|
||||
ResolvedItemOptions<TItemType> & {
|
||||
children: (item: {
|
||||
item: ItemMap[TItemType];
|
||||
data: unknown;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
export function ResolvedItem<TItemType extends ItemType>(
|
||||
props: ResolvedItemProps<TItemType>
|
||||
) {
|
||||
const { children } = props;
|
||||
const { item, data } = useResolvedItem(props) || {};
|
||||
return item ? <>{children({ item, data })}</> : null;
|
||||
}
|
||||
export const ContainerHeader = (props: PropsWithChildren) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return !selectionMode ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Keyboard, View } from "react-native";
|
||||
import Animated, {
|
||||
@@ -35,13 +34,12 @@ import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { SIZE, normalize } from "../../utils/size";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
|
||||
interface FloatingButtonProps {
|
||||
title: string;
|
||||
interface FloatingButton {
|
||||
title?: string;
|
||||
onPress: () => void;
|
||||
color?: string;
|
||||
shouldShow?: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
}
|
||||
|
||||
@@ -50,12 +48,11 @@ const FloatingButton = ({
|
||||
onPress,
|
||||
color,
|
||||
alwaysVisible = false
|
||||
}: FloatingButtonProps) => {
|
||||
}: FloatingButton) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const translate = useSharedValue(0);
|
||||
const route = useRoute();
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
@@ -118,11 +115,11 @@ const FloatingButton = ({
|
||||
animatedStyle
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={color}
|
||||
style={{
|
||||
accentColor={colors.static[color as keyof typeof colors.static]}
|
||||
customStyle={{
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
}}
|
||||
@@ -142,18 +139,12 @@ const FloatingButton = ({
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
route.name === "Notebooks"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
}
|
||||
name={title === "Clear all trash" ? "delete" : "plus"}
|
||||
color={colors.primary.accentForeground}
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</PressableButton>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,49 +17,41 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { KeyboardAvoidingView, Platform } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { Header } from "../header";
|
||||
import SelectionHeader from "../selection-header";
|
||||
|
||||
export const Container = ({ children }: PropsWithChildren) => {
|
||||
const { colors } = useThemeColors();
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const keyboard = useKeyboard();
|
||||
const [height, setHeight] = useState(0);
|
||||
const windowHeightRef = useRef(Dimensions.get("window").height);
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const timerRef = useRef<NodeJS.Timeout>();
|
||||
useEffect(() => {
|
||||
if (windowHeight !== windowHeightRef.current) {
|
||||
setHeight(0);
|
||||
windowHeightRef.current = windowHeight;
|
||||
}
|
||||
}, [windowHeight]);
|
||||
|
||||
return (
|
||||
<View
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
enabled={Platform.OS === "ios" && !floating}
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom,
|
||||
height: height || "100%",
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
onLayout={(event) => {
|
||||
const height = event.nativeEvent.layout.height;
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
if (!keyboard.keyboardShown) {
|
||||
setHeight(height);
|
||||
}
|
||||
}, 500);
|
||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
|
||||
}}
|
||||
>
|
||||
{!introCompleted ? null : (
|
||||
<>
|
||||
<SelectionHeader />
|
||||
<Header />
|
||||
</>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function DelayLayout({
|
||||
}: IDelayLayoutProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const loading = useDelayLayout(
|
||||
!props.delay || props.delay < 300 ? 0 : props.delay
|
||||
!props.delay || props.delay < 300 ? 300 : props.delay
|
||||
);
|
||||
const Placeholder = placeholder[props.type || "default"];
|
||||
|
||||
|
||||
@@ -17,16 +17,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AnnouncementDialog } from "../announcements";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { AddTopicDialog } from "../dialogs/add-topic";
|
||||
import { LoadingDialog } from "../dialogs/loading";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
import ResultDialog from "../dialogs/result";
|
||||
import { VaultDialog } from "../dialogs/vault";
|
||||
import ImagePreview from "../image-preview";
|
||||
@@ -36,32 +35,33 @@ import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import Progress from "../dialogs/progress";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const loading = useNoteStore((state) => state.loading);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppLockPassword />
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<AddTopicDialog colors={colors} />
|
||||
<PremiumDialog colors={colors} />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<SheetProvider />
|
||||
<SheetProvider context="sync_progress" />
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<Expiring />
|
||||
{loading ? null : <Expiring />}
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<Progress />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,9 +21,11 @@ import { eSendEvent } from "../../services/event-manager";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseAddNotebookDialog,
|
||||
eCloseAddTopicDialog,
|
||||
eCloseMoveNoteDialog,
|
||||
eOpenActionSheet,
|
||||
eOpenAddNotebookDialog,
|
||||
eOpenAddTopicDialog,
|
||||
eOpenMoveNoteDialog
|
||||
} from "../../utils/events";
|
||||
|
||||
@@ -50,3 +52,9 @@ export const AddNotebookEvent = (notebook) => {
|
||||
export const HideAddNotebookEvent = (notebook) => {
|
||||
eSendEvent(eCloseAddNotebookDialog, notebook);
|
||||
};
|
||||
export const AddTopicEvent = (topic) => {
|
||||
eSendEvent(eOpenAddTopicDialog, topic);
|
||||
};
|
||||
export const HideAddTopicEvent = (notebook) => {
|
||||
eSendEvent(eCloseAddTopicDialog, notebook);
|
||||
};
|
||||
|
||||
@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
@@ -36,25 +35,6 @@ import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
|
||||
export interface BaseDialogProps extends PropsWithChildren {
|
||||
animation?: "fade" | "none" | "slide" | undefined;
|
||||
visible: boolean;
|
||||
onRequestClose?: () => void;
|
||||
onShow?: () => void;
|
||||
premium?: boolean;
|
||||
statusBarTranslucent?: boolean;
|
||||
transparent?: boolean;
|
||||
centered?: boolean;
|
||||
bottom?: boolean;
|
||||
background?: string | ColorValue;
|
||||
animated?: boolean;
|
||||
bounce?: boolean;
|
||||
closeOnTouch?: boolean;
|
||||
useSafeArea?: boolean;
|
||||
avoidKeyboardResize?: boolean;
|
||||
enableSheetKeyboardHandler?: boolean;
|
||||
}
|
||||
|
||||
const BaseDialog = ({
|
||||
visible,
|
||||
onRequestClose,
|
||||
@@ -66,14 +46,14 @@ const BaseDialog = ({
|
||||
transparent,
|
||||
centered = true,
|
||||
bottom = false,
|
||||
background = null,
|
||||
animated = true,
|
||||
bounce = true,
|
||||
closeOnTouch = true,
|
||||
useSafeArea = true,
|
||||
avoidKeyboardResize = false,
|
||||
enableSheetKeyboardHandler,
|
||||
background
|
||||
}: BaseDialogProps) => {
|
||||
enableSheetKeyboardHandler = false
|
||||
}) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const appState = useAppState();
|
||||
const lockEvents = useRef(false);
|
||||
@@ -87,8 +67,7 @@ const BaseDialog = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (useUserStore.getState().disableAppLockRequests) return;
|
||||
|
||||
if (SettingsService.canLockAppInBackground()) {
|
||||
if (SettingsService.get().appLockMode === "background") {
|
||||
if (appState === "background") {
|
||||
setIntervalVisible(false);
|
||||
if (useUserStore.getState().appLocked) {
|
||||
@@ -169,7 +148,7 @@ const BaseDialog = ({
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={closeOnTouch ? onRequestClose : undefined}
|
||||
onPress={closeOnTouch ? onRequestClose : null}
|
||||
style={styles.overlayButton}
|
||||
/>
|
||||
{premium}
|
||||
@@ -25,7 +25,6 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
|
||||
const DialogButtons = ({
|
||||
onPressPositive,
|
||||
@@ -36,7 +35,7 @@ const DialogButtons = ({
|
||||
doneText,
|
||||
positiveType
|
||||
}) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -45,15 +44,9 @@ const DialogButtons = ({
|
||||
{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 60,
|
||||
borderBottomRightRadius: 5,
|
||||
borderBottomLeftRadius: 5,
|
||||
paddingHorizontal: 12,
|
||||
borderTopWidth: 0.7,
|
||||
borderTopColor: getColorLinearShade(
|
||||
colors.secondary.background,
|
||||
0.05,
|
||||
isDark
|
||||
)
|
||||
borderBottomRightRadius: 10,
|
||||
borderBottomLeftRadius: 10,
|
||||
paddingHorizontal: 12
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -87,7 +80,7 @@ const DialogButtons = ({
|
||||
onPress={onPressNegative}
|
||||
fontSize={SIZE.md}
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="plain"
|
||||
type="gray"
|
||||
bold
|
||||
title={negativeTitle}
|
||||
/>
|
||||
|
||||
@@ -18,43 +18,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
|
||||
const DialogContainer = ({
|
||||
width,
|
||||
height,
|
||||
style,
|
||||
...restProps
|
||||
}: ViewProps & {
|
||||
width?: any;
|
||||
height?: any;
|
||||
noBorder?: boolean;
|
||||
}) => {
|
||||
const DialogContainer = ({ width, height, ...restProps }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...restProps}
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
},
|
||||
restProps?.noBorder
|
||||
? {}
|
||||
: {
|
||||
...getElevationStyle(5),
|
||||
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||
}
|
||||
]}
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -22,7 +22,7 @@ import { Text, View, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableProps } from "../ui/pressable";
|
||||
import { PressableButtonProps } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
@@ -34,7 +34,7 @@ type DialogHeaderProps = {
|
||||
onPress?: () => void;
|
||||
loading?: boolean;
|
||||
title?: string;
|
||||
type?: PressableProps["type"];
|
||||
type?: PressableButtonProps["type"];
|
||||
icon?: string;
|
||||
};
|
||||
paragraphColor?: string;
|
||||
@@ -86,9 +86,7 @@ const DialogHeader = ({
|
||||
>
|
||||
{title}{" "}
|
||||
{titlePart ? (
|
||||
<Text style={{ color: colors.primary.accent }}>
|
||||
{titlePart}
|
||||
</Text>
|
||||
<Text style={{ color: colors.primary.accent }}>{titlePart}</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
|
||||
@@ -103,8 +101,8 @@ const DialogHeader = ({
|
||||
fontSize={13}
|
||||
title={button.title}
|
||||
icon={button.icon}
|
||||
type={button.type || "secondary"}
|
||||
height={30}
|
||||
type={button.type || "grayBg"}
|
||||
height={25}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -25,12 +25,12 @@ type DialogInfo = {
|
||||
paragraph?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (...args: any[]) => void;
|
||||
positivePress?: (value: unknown) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
| "plain"
|
||||
| "secondary"
|
||||
| "gray"
|
||||
| "grayBg"
|
||||
| "accent"
|
||||
| "inverted"
|
||||
| "shade"
|
||||
@@ -41,19 +41,7 @@ type DialogInfo = {
|
||||
input: boolean;
|
||||
inputPlaceholder: string;
|
||||
defaultValue: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: string;
|
||||
check?: {
|
||||
info: string;
|
||||
type?: string;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
notice: {
|
||||
text: string;
|
||||
type: "alert" | "information";
|
||||
};
|
||||
context: "global" | "local";
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -36,16 +36,11 @@ import DialogButtons from "./dialog-buttons";
|
||||
import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const values = useRef({
|
||||
inputValue: undefined
|
||||
});
|
||||
const [inputValue, setInputValue] = useState(null);
|
||||
const inputRef = useRef();
|
||||
const [dialogInfo, setDialogInfo] = useState({
|
||||
title: "",
|
||||
@@ -63,17 +58,25 @@ export const Dialog = ({ context = "global" }) => {
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent",
|
||||
defaultValue: false
|
||||
type: "transparent"
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
};
|
||||
}, [show]);
|
||||
|
||||
const onPressPositive = async () => {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
let result = await dialogInfo.positivePress(
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
checked
|
||||
inputValue || dialogInfo.defaultValue
|
||||
);
|
||||
if (result === false) {
|
||||
return;
|
||||
@@ -88,26 +91,14 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(data.check?.defaultValue);
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
setInputValue(data.defaultValue);
|
||||
},
|
||||
[context]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
};
|
||||
}, [show]);
|
||||
|
||||
const hide = () => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setInputValue(null);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -115,6 +106,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (dialogInfo.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
|
||||
hide();
|
||||
};
|
||||
|
||||
@@ -124,8 +116,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
maxHeight: 450,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
...getContainerBorder(colors.primary.border, 0.5)
|
||||
paddingTop: 12
|
||||
};
|
||||
|
||||
return visible ? (
|
||||
@@ -168,7 +159,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.inputValue = value;
|
||||
setInputValue(value);
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
@@ -176,33 +167,19 @@ export const Dialog = ({ context = "global" }) => {
|
||||
onSubmit={onPressPositive}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setChecked(!checked);
|
||||
setInputValue(!inputValue);
|
||||
}}
|
||||
icon={
|
||||
checked
|
||||
inputValue
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
@@ -210,10 +187,9 @@ export const Dialog = ({ context = "global" }) => {
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
type={inputValue ? dialogInfo.check.type : "gray"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||