Compare commits
1 Commits
fix-precen
...
fix-defaul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53baa1a852 |
@@ -15,7 +15,6 @@ const SCOPES = [
|
|||||||
"mobile",
|
"mobile",
|
||||||
"web",
|
"web",
|
||||||
"vericrypt",
|
"vericrypt",
|
||||||
"monograph",
|
|
||||||
"desktop",
|
"desktop",
|
||||||
"crypto",
|
"crypto",
|
||||||
"editor",
|
"editor",
|
||||||
@@ -35,8 +34,7 @@ const SCOPES = [
|
|||||||
"common",
|
"common",
|
||||||
"global",
|
"global",
|
||||||
"docs",
|
"docs",
|
||||||
"themebuilder",
|
"themebuilder"
|
||||||
"intl"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ apps/mobile/e2e/
|
|||||||
|
|
||||||
# web
|
# web
|
||||||
apps/web/public/an.js
|
apps/web/public/an.js
|
||||||
apps/web/src/common/sqlite/wa-sqlite-async.js
|
|
||||||
apps/web/src/common/sqlite/wa-sqlite.js
|
|
||||||
|
|
||||||
# editor
|
# editor
|
||||||
packages/editor/styles/
|
packages/editor/styles/
|
||||||
|
|||||||
@@ -78,13 +78,7 @@ module.exports = {
|
|||||||
semi: ["error", "always"],
|
semi: ["error", "always"],
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"header/header": ["error", "block", LICENSE, 1],
|
"header/header": ["error", "block", LICENSE, 1]
|
||||||
"@typescript-eslint/no-empty-interface": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
allowSingleExtends: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Are you facing a bug or a crash in Notesnook?
|
description: Are you facing a bug or a crash in Notesnook?
|
||||||
type: Bug
|
labels: ["Type: Bug", "Status: Pending"]
|
||||||
|
assignees:
|
||||||
|
- thecodrr
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Notesnook Discord Community
|
- name: Notesnook Discord Community
|
||||||
url: https://go.notesnook.com/discord
|
url: https://discord.gg/6mHHyncHJE
|
||||||
about: Reach out to us directly & discuss your issues, suggestions & other feedback!
|
about: Reach out to us directly & discuss your issues, suggestions & other feedback!
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest a feature you are missing in Notesnook
|
description: Suggest a feature you are missing in Notesnook
|
||||||
type: Feature
|
labels: ["Type: Feature Request", "Status: Pending"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 16.x
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|||||||
100
.github/workflows/android.e2e.yml
vendored
@@ -1,100 +0,0 @@
|
|||||||
name: e2e-android
|
|
||||||
on: workflow_dispatch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
e2e-android:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
API_LEVEL: 34
|
|
||||||
ARCH: x86_64
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Enable KVM
|
|
||||||
run: |
|
|
||||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
||||||
sudo udevadm control --reload-rules
|
|
||||||
sudo udevadm trigger --name-match=kvm
|
|
||||||
|
|
||||||
- name: Free Disk Space
|
|
||||||
uses: jlumbroso/free-disk-space@main
|
|
||||||
with:
|
|
||||||
tool-cache: false
|
|
||||||
android: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
|
||||||
|
|
||||||
- name: Install node modules
|
|
||||||
run: |
|
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
|
||||||
npm run bootstrap -- --scope=mobile
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v3
|
|
||||||
with:
|
|
||||||
gradle-version: wrapper
|
|
||||||
cache-read-only: false
|
|
||||||
|
|
||||||
- name: Use specific Java version for the builds
|
|
||||||
uses: joschi/setup-jdk@v2
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
architecture: "x64"
|
|
||||||
|
|
||||||
- name: Install Detox CLI
|
|
||||||
run: npm install detox-cli --global
|
|
||||||
|
|
||||||
- name: Detox build
|
|
||||||
run: |
|
|
||||||
yarn build:android
|
|
||||||
ls apps/mobile/native/android/app/build/outputs/apk
|
|
||||||
ls apps/mobile/native/android/app/build/outputs/apk/release
|
|
||||||
|
|
||||||
- name: Get device name
|
|
||||||
id: device
|
|
||||||
run: |
|
|
||||||
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
|
|
||||||
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: AVD cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: avd-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.android/avd/*
|
|
||||||
~/.android/adb*
|
|
||||||
key: ${{ steps.device.outputs.AVD_NAME }}
|
|
||||||
|
|
||||||
- name: create AVD and generate snapshot for caching
|
|
||||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
with:
|
|
||||||
api-level: ${{ env.API_LEVEL }}
|
|
||||||
arch: ${{ env.ARCH }}
|
|
||||||
avd-name: ${{ steps.device.outputs.AVD_NAME }}
|
|
||||||
force-avd-creation: false
|
|
||||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
disable-animations: true
|
|
||||||
script: echo "Generated AVD snapshot for caching."
|
|
||||||
|
|
||||||
- name: Detox test
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
with:
|
|
||||||
api-level: ${{ env.API_LEVEL }}
|
|
||||||
arch: ${{ env.ARCH }}
|
|
||||||
avd-name: ${{ steps.device.outputs.AVD_NAME }}
|
|
||||||
disable-animations: true
|
|
||||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
force-avd-creation: false
|
|
||||||
script: yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: detox-artifacts
|
|
||||||
path: artifacts
|
|
||||||
retention-days: 14
|
|
||||||
113
.github/workflows/android.publish.internal.yml
vendored
@@ -6,11 +6,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
env:
|
|
||||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
|
||||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
|
||||||
NDK_CCACHE: ccache
|
|
||||||
USE_CCACHE: 1
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -29,128 +24,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||||
npm run bootstrap -- --scope=mobile
|
npm run bootstrap -- --scope=mobile
|
||||||
|
|
||||||
- name: Make Gradlew Executable
|
- name: Make Gradlew Executable
|
||||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
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
|
- name: Build unsigned app bundle
|
||||||
run: yarn release:android:bundle
|
run: yarn release:android:bundle
|
||||||
|
|
||||||
- name: CCache Stats after build
|
|
||||||
run: ccache -sv
|
|
||||||
|
|
||||||
- name: Sign app bundle for Playstore release
|
- name: Sign app bundle for Playstore release
|
||||||
id: sign_app
|
id: sign_app
|
||||||
uses: r0adkll/sign-android-release@master
|
uses: r0adkll/sign-android-release@v1
|
||||||
with:
|
with:
|
||||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
alias: ${{ secrets.ALIAS }}
|
alias: ${{ secrets.ALIAS }}
|
||||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
keyPassword: ${{ 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
|
- name: Publish to Playstore
|
||||||
id: deploy
|
id: deploy
|
||||||
uses: r0adkll/upload-google-play@v1.1.1
|
uses: r0adkll/upload-google-play@v1
|
||||||
with:
|
with:
|
||||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||||
packageName: com.streetwriters.notesnook
|
packageName: com.streetwriters.notesnook
|
||||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||||
track: alpha
|
track: internal
|
||||||
status: completed
|
status: completed
|
||||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
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
|
|
||||||
|
|||||||
82
.github/workflows/android.publish.yml
vendored
@@ -6,11 +6,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
env:
|
|
||||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
|
||||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
|
||||||
NDK_CCACHE: ccache
|
|
||||||
USE_CCACHE: 1
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -22,7 +17,7 @@ jobs:
|
|||||||
- name: Use specific Java version for the builds
|
- name: Use specific Java version for the builds
|
||||||
uses: joschi/setup-jdk@v2
|
uses: joschi/setup-jdk@v2
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "11"
|
||||||
architecture: "x64"
|
architecture: "x64"
|
||||||
|
|
||||||
- name: Install node modules
|
- name: Install node modules
|
||||||
@@ -33,54 +28,9 @@ jobs:
|
|||||||
- name: Make Gradlew Executable
|
- name: Make Gradlew Executable
|
||||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
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
|
- name: Build unsigned app bundle
|
||||||
run: yarn release:android:bundle
|
run: yarn release:android:bundle
|
||||||
|
|
||||||
- name: CCache Stats after build
|
|
||||||
run: ccache -sv
|
|
||||||
|
|
||||||
- name: Sign app bundle for Playstore release
|
- name: Sign app bundle for Playstore release
|
||||||
id: sign_app
|
id: sign_app
|
||||||
uses: r0adkll/sign-android-release@master
|
uses: r0adkll/sign-android-release@master
|
||||||
@@ -93,6 +43,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: "33.0.0"
|
BUILD_TOOLS_VERSION: "33.0.0"
|
||||||
|
|
||||||
|
- name: Publish to Playstore
|
||||||
|
id: deploy
|
||||||
|
uses: r0adkll/upload-google-play@v1.1.1
|
||||||
|
with:
|
||||||
|
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||||
|
packageName: com.streetwriters.notesnook
|
||||||
|
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||||
|
track: production
|
||||||
|
status: completed
|
||||||
|
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||||
|
|
||||||
- name: Build apks for Github release
|
- name: Build apks for Github release
|
||||||
run: yarn release:android
|
run: yarn release:android
|
||||||
|
|
||||||
@@ -122,17 +83,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: apps/mobile
|
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
|
- name: Create release draft on Github
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
@@ -146,11 +96,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-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.apk
|
||||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.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
|
|
||||||
|
|||||||
2
.github/workflows/core.tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|||||||
78
.github/workflows/desktop.publish.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
run: npx nx build:desktop @notesnook/web
|
run: npx nx build:desktop @notesnook/web
|
||||||
|
|
||||||
- name: Archive build artifact
|
- name: Archive build artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: apps/web/build/**/*
|
path: apps/web/build/**/*
|
||||||
@@ -82,44 +82,20 @@ jobs:
|
|||||||
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
||||||
tag_name: 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
|
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
|
||||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
|
||||||
|
|
||||||
- name: Generate flatpak sources
|
|
||||||
if: inputs.publish-github && inputs.build-linux
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y flatpak flatpak-builder pipx
|
|
||||||
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
|
|
||||||
cd /tmp/flatpak-builder-tools/node
|
|
||||||
pipx install .
|
|
||||||
cd -
|
|
||||||
node scripts/generate-sources.mjs
|
|
||||||
|
|
||||||
- name: Upload flatpak sources
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
if: inputs.publish-github && inputs.build-linux
|
|
||||||
with:
|
|
||||||
draft: true
|
|
||||||
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
|
||||||
tag_name: v${{ steps.app_metadata.outputs.app_version }}
|
|
||||||
files: ./generated-sources.json
|
|
||||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
name: Build for macOS
|
name: Build for macOS
|
||||||
needs: build
|
needs: build
|
||||||
if: inputs.build-mac
|
if: inputs.build-mac
|
||||||
runs-on: macos-14
|
runs-on: macos-12
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|
||||||
- name: Install setuptools
|
|
||||||
run: brew install python-setuptools
|
|
||||||
|
|
||||||
- name: Setup notarization
|
- name: Setup notarization
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/private_keys/
|
mkdir -p ~/private_keys/
|
||||||
@@ -146,7 +122,7 @@ jobs:
|
|||||||
platform: "MAC_OS"
|
platform: "MAC_OS"
|
||||||
|
|
||||||
- name: Download build
|
- name: Download build
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: ./apps/web/build
|
path: ./apps/web/build
|
||||||
@@ -156,12 +132,6 @@ jobs:
|
|||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||||
npm run bootstrap -- --scope=desktop
|
npm run bootstrap -- --scope=desktop
|
||||||
|
|
||||||
- name: Install sqlite-better-trigram for all arch
|
|
||||||
run: |
|
|
||||||
npm i --cpu arm64 sqlite-better-trigram
|
|
||||||
npm i --cpu x64 sqlite-better-trigram
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Install provisioning profile
|
- name: Install provisioning profile
|
||||||
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
@@ -173,7 +143,7 @@ jobs:
|
|||||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||||
run: |
|
run: |
|
||||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
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
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
- name: Build zip and dmg
|
- name: Build zip and dmg
|
||||||
@@ -183,13 +153,13 @@ jobs:
|
|||||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||||
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
||||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
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: |
|
run: |
|
||||||
npx nx run release --project @notesnook/desktop
|
npx nx run release --project @notesnook/desktop
|
||||||
if [ ${{ inputs.publish-github }} == true ]; then
|
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
|
else
|
||||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
|
npx electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||||
fi
|
fi
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
@@ -214,7 +184,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
@@ -228,7 +198,7 @@ jobs:
|
|||||||
if: inputs.publish-snap
|
if: inputs.publish-snap
|
||||||
|
|
||||||
- name: Download build
|
- name: Download build
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: ./apps/web/build
|
path: ./apps/web/build
|
||||||
@@ -238,12 +208,6 @@ jobs:
|
|||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||||
npm run bootstrap -- --scope=desktop
|
npm run bootstrap -- --scope=desktop
|
||||||
|
|
||||||
- name: Install sqlite-better-trigram for all arch
|
|
||||||
run: |
|
|
||||||
npm i --cpu arm64 sqlite-better-trigram
|
|
||||||
npm i --cpu x64 sqlite-better-trigram
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build Electron wrapper
|
- name: Build Electron wrapper
|
||||||
run: npx nx run release --project @notesnook/desktop
|
run: npx nx run release --project @notesnook/desktop
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
@@ -251,7 +215,7 @@ jobs:
|
|||||||
- name: Build snap
|
- name: Build snap
|
||||||
if: inputs.publish-snap
|
if: inputs.publish-snap
|
||||||
run: |
|
run: |
|
||||||
yarn electron-builder --linux snap:x64 -p never
|
npx electron-builder --linux snap:x64 -p never
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
- name: Build AppImage
|
- name: Build AppImage
|
||||||
@@ -259,9 +223,9 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [ ${{ inputs.publish-github }} == true ]; then
|
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
|
else
|
||||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||||
fi
|
fi
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
@@ -279,22 +243,22 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup .NET Core SDK
|
- name: Setup .NET Core SDK
|
||||||
uses: actions/setup-dotnet@v2
|
uses: actions/setup-dotnet@v2
|
||||||
with:
|
with:
|
||||||
dotnet-version: 6.0.x
|
dotnet-version: 6.0.x
|
||||||
|
|
||||||
- name: Install TrustedSigning
|
- name: Install AzureCodeSigning
|
||||||
run: Install-Module -Name TrustedSigning -RequiredVersion 0.3.15 -Force -Repository PSGallery
|
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|
||||||
- name: Download build
|
- name: Download build
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: ./apps/web/build
|
path: ./apps/web/build
|
||||||
@@ -304,12 +268,6 @@ jobs:
|
|||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||||
npm run bootstrap -- --scope=desktop
|
npm run bootstrap -- --scope=desktop
|
||||||
|
|
||||||
- name: Install sqlite-better-trigram for all arch
|
|
||||||
run: |
|
|
||||||
npm i --cpu arm64 sqlite-better-trigram
|
|
||||||
npm i --cpu x64 sqlite-better-trigram
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npx nx run release --project @notesnook/desktop
|
run: npx nx run release --project @notesnook/desktop
|
||||||
|
|
||||||
|
|||||||
233
.github/workflows/desktop.tests.yml
vendored
@@ -1,233 +0,0 @@
|
|||||||
name: Test @notesnook/desktop
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "master"
|
|
||||||
paths:
|
|
||||||
- "app/desktop/**"
|
|
||||||
# re-run workflow if workflow file changes
|
|
||||||
- ".github/workflows/desktop.tests.yml"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- name: Generate desktop build
|
|
||||||
run: npx nx build:desktop @notesnook/web
|
|
||||||
|
|
||||||
- name: Archive build artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: apps/web/build/**/*
|
|
||||||
|
|
||||||
test-macos-x64:
|
|
||||||
name: Test macOS x64
|
|
||||||
needs: build
|
|
||||||
runs-on: macos-13
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
|
||||||
|
|
||||||
- name: Download build
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: ./apps/web/build
|
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
|
||||||
npm run bootstrap -- --scope=desktop
|
|
||||||
|
|
||||||
- name: Build Electron wrapper
|
|
||||||
run: npx nx run release --project @notesnook/desktop
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: |
|
|
||||||
yarn electron-builder --mac --dir --x64
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Run tests x64
|
|
||||||
run: |
|
|
||||||
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: test-results-macos-x64
|
|
||||||
path: apps/desktop/test-results
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
test-macos:
|
|
||||||
name: Test macOS
|
|
||||||
needs: build
|
|
||||||
runs-on: macos-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
|
||||||
|
|
||||||
- name: Download build
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: ./apps/web/build
|
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
|
||||||
npm run bootstrap -- --scope=desktop
|
|
||||||
|
|
||||||
- name: Build Electron wrapper
|
|
||||||
run: npx nx run release --project @notesnook/desktop
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: |
|
|
||||||
yarn electron-builder --mac --dir --arm64
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Run tests arm64
|
|
||||||
run: |
|
|
||||||
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: test-results-macos
|
|
||||||
path: apps/desktop/test-results
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
test-linux:
|
|
||||||
name: Test for Linux
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
|
||||||
|
|
||||||
- name: Download build
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: ./apps/web/build
|
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
|
||||||
npm run bootstrap -- --scope=desktop
|
|
||||||
|
|
||||||
- name: Install sqlite-better-trigram for all arch
|
|
||||||
run: |
|
|
||||||
npm i --cpu arm64 sqlite-better-trigram
|
|
||||||
npm i --cpu x64 sqlite-better-trigram
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build Electron wrapper
|
|
||||||
run: npx nx run release --project @notesnook/desktop
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: |
|
|
||||||
yarn electron-builder --linux --dir --arm64 --x64
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
EXECUTABLE_PATH=output/linux-unpacked/notesnook xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: test-results-linux
|
|
||||||
path: apps/desktop/test-results
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
test-windows:
|
|
||||||
name: Test for Windows
|
|
||||||
needs: build
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
|
||||||
|
|
||||||
- name: Download build
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: ./apps/web/build
|
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
|
||||||
npm run bootstrap -- --scope=desktop
|
|
||||||
|
|
||||||
- name: Install sqlite-better-trigram for all arch
|
|
||||||
run: |
|
|
||||||
npm i --cpu arm64 sqlite-better-trigram
|
|
||||||
npm i --cpu x64 sqlite-better-trigram
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build Electron wrapper
|
|
||||||
run: npx nx run release --project @notesnook/desktop
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: |
|
|
||||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
|
|
||||||
working-directory: ./apps/desktop
|
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: test-results-win
|
|
||||||
path: apps/desktop/test-results
|
|
||||||
retention-days: 5
|
|
||||||
2
.github/workflows/editor.tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|||||||
4
.github/workflows/help.publish.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Publish Notesnook Help
|
name: Publish Notesnook Help
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|||||||
56
.github/workflows/ios.publish.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: macos-latest
|
runs-on: macos-13
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -22,45 +22,11 @@ jobs:
|
|||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: npx nx run @notesnook/mobile:build
|
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
|
- name: Install Pods
|
||||||
run: npm run prepare:ios
|
run: npm run prepare:ios
|
||||||
|
|
||||||
- name: CCache Stats Before Build
|
|
||||||
run: ccache -sv
|
|
||||||
|
|
||||||
- name: Build iOS App
|
- name: Build iOS App
|
||||||
uses: ammarahm-ed/ios-build-action@master
|
uses: yukiarrr/ios-build-action@v1.11.2
|
||||||
with:
|
with:
|
||||||
bundle-identifier: org.streetwriters.notesnook
|
bundle-identifier: org.streetwriters.notesnook
|
||||||
scheme: Notesnook
|
scheme: Notesnook
|
||||||
@@ -86,22 +52,16 @@ jobs:
|
|||||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||||
|
|
||||||
- name: CCache Stats After Build
|
- name: 'Upload app to TestFlight'
|
||||||
run: ccache -sv
|
|
||||||
|
|
||||||
- name: "Upload app to TestFlight"
|
|
||||||
uses: apple-actions/upload-testflight-build@v1
|
uses: apple-actions/upload-testflight-build@v1
|
||||||
with:
|
with:
|
||||||
app-path: Notesnook.ipa
|
app-path: Notesnook.ipa
|
||||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||||
api-private-key: ${{ secrets.API_KEY }}
|
api-private-key: ${{ secrets.API_KEY }}
|
||||||
|
|
||||||
- name: Upload Notesnook.ipa to Github
|
- name: Upload to Github
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Notesnook.zip
|
name: Notesnook.ipa
|
||||||
path: |
|
path: Notesnook.ipa
|
||||||
Notesnook.ipa
|
|
||||||
apps/mobile/native/ios/**/*.map
|
|
||||||
packages/editor-mobile/sourcemaps/*.map
|
|
||||||
|
|||||||
89
.github/workflows/monograph.publish.yml
vendored
@@ -1,89 +0,0 @@
|
|||||||
# This workflow uses actions that are not certified by GitHub.
|
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
|
|
||||||
# GitHub recommends pinning actions to a commit SHA.
|
|
||||||
# To get a newer version, you will need to update the SHA.
|
|
||||||
# You can also reference a tag or branch, but the action may change without warning.
|
|
||||||
|
|
||||||
name: Publish @notesnook/monograph
|
|
||||||
|
|
||||||
on: workflow_dispatch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_registry:
|
|
||||||
name: Push Docker image to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- 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=monograph
|
|
||||||
|
|
||||||
- name: Collect package metadata
|
|
||||||
id: package_metadata
|
|
||||||
working-directory: ./apps/monograph
|
|
||||||
run: |
|
|
||||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
|
||||||
|
|
||||||
- name: Generate build
|
|
||||||
run: npx nx build @notesnook/monograph
|
|
||||||
|
|
||||||
# Setup Buildx
|
|
||||||
- name: Docker Setup Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
ecr: auto
|
|
||||||
logout: true
|
|
||||||
|
|
||||||
# Pull previous image from docker hub to use it as cache to improve the image build time.
|
|
||||||
- name: docker pull cache image
|
|
||||||
continue-on-error: true
|
|
||||||
run: docker pull streetwriters/monograph:latest
|
|
||||||
|
|
||||||
# Setup QEMU
|
|
||||||
# - name: Set up QEMU
|
|
||||||
# uses: docker/setup-qemu-action@v2
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: streetwriters/monograph
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
id: push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: apps/monograph
|
|
||||||
file: apps/monograph/Dockerfile
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
|
||||||
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
|
|
||||||
cache-from: streetwriters/monograph:latest
|
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
with:
|
|
||||||
subject-name: index.docker.io/streetwriters/monograph
|
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
2
.github/workflows/theme-builder.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|||||||
2
.github/workflows/vericrypt.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|||||||
44
.github/workflows/web.benchmarks.yml
vendored
@@ -1,44 +0,0 @@
|
|||||||
name: Benchmark @notesnook/web
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "master"
|
|
||||||
paths:
|
|
||||||
- "apps/web/**"
|
|
||||||
# re-run workflow if workflow file changes
|
|
||||||
- ".github/workflows/web.benchmarks.yml"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bench:
|
|
||||||
name: Bench
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build:web
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: npx playwright install chromium --with-deps
|
|
||||||
working-directory: apps/web
|
|
||||||
|
|
||||||
- name: Run benchmarks
|
|
||||||
id: benchmark
|
|
||||||
run: node __bench__/app.bench.mjs
|
|
||||||
working-directory: apps/web
|
|
||||||
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' }}
|
||||||
2
.github/workflows/web.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|||||||
12
.github/workflows/web.tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
run: npm run build:test:web
|
run: npm run build:test:web
|
||||||
|
|
||||||
- name: Archive build artifact
|
- name: Archive build artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: apps/web/build/**/*
|
path: apps/web/build/**/*
|
||||||
@@ -53,10 +53,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download build
|
- name: Download build
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: ./apps/web/build
|
path: ./apps/web/build
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: |
|
run: |
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
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
|
- name: Validate build extraction
|
||||||
run: ls -ld ./apps/web/build/index.html
|
run: ls -ld ./apps/web/build/index.html
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
working-directory: apps/web
|
working-directory: apps/web
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: test-results-${{ matrix.shard }}
|
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
|
.nyc_output
|
||||||
site
|
site
|
||||||
node_modules.backup
|
node_modules.backup
|
||||||
.nx
|
.nx
|
||||||
@@ -11,5 +11,4 @@ apps/mobile/native/
|
|||||||
apps/web/public/an.js
|
apps/web/public/an.js
|
||||||
|
|
||||||
# editor
|
# editor
|
||||||
packages/editor/styles/
|
packages/editor/styles/
|
||||||
/.nx/cache
|
|
||||||
3
apps/desktop/.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
output
|
output
|
||||||
dist
|
dist
|
||||||
_catalog
|
|
||||||
@@ -40,35 +40,12 @@ Once you are inside the `./notesnook` directory, run the preparation step:
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can finally start the desktop app for development:
|
Now you can finally start the desktop app:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run start:desktop
|
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
|
## Developer guide
|
||||||
|
|
||||||
### The tech stack
|
### The tech stack
|
||||||
|
|||||||
@@ -1,53 +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 test from "node:test";
|
|
||||||
import { launchApp } from "./utils.mjs";
|
|
||||||
import assert from "assert";
|
|
||||||
import slugify from "slugify";
|
|
||||||
import path from "path";
|
|
||||||
import { mkdir } from "fs/promises";
|
|
||||||
|
|
||||||
test("make sure app loads", async (t) => {
|
|
||||||
const { app, page } = await launchApp();
|
|
||||||
try {
|
|
||||||
await page.waitForSelector("#authForm");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
|
||||||
);
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForSelector(".ProseMirror");
|
|
||||||
} catch (e) {
|
|
||||||
await mkdir("test-results", { recursive: true });
|
|
||||||
await page.screenshot({
|
|
||||||
path: path.join(
|
|
||||||
"test-results",
|
|
||||||
`${slugify(t.name)}-${process.platform}-${process.arch}-error.png`
|
|
||||||
)
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
await app.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,38 +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 { _electron as electron } from "playwright";
|
|
||||||
|
|
||||||
const executablePath = process.env.EXECUTABLE_PATH;
|
|
||||||
|
|
||||||
// process.platform === "linux"
|
|
||||||
// ? "output/linux-unpacked/Notesnook"
|
|
||||||
// : process.platform === "darwin"
|
|
||||||
// ? "output/mac/Notesnook.app/Contents/MacOS/Notesnook"
|
|
||||||
// : "output/win-unpacked/Notesnook.exe";
|
|
||||||
|
|
||||||
export async function launchApp() {
|
|
||||||
const app = await electron.launch({
|
|
||||||
executablePath
|
|
||||||
});
|
|
||||||
|
|
||||||
const page = await app.firstWindow();
|
|
||||||
|
|
||||||
return { app, page };
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
owner: streetwriters
|
owner: streetwriters
|
||||||
repo: notesnook
|
repo: notesnook
|
||||||
provider: github
|
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 |
5814
apps/desktop/package-lock.json
generated
@@ -2,58 +2,35 @@
|
|||||||
"name": "@notesnook/desktop",
|
"name": "@notesnook/desktop",
|
||||||
"productName": "Notesnook",
|
"productName": "Notesnook",
|
||||||
"description": "Your private note taking space",
|
"description": "Your private note taking space",
|
||||||
"version": "3.0.21",
|
"version": "2.6.17",
|
||||||
"appAppleId": "1544027013",
|
"appAppleId": "1544027013",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/cjs/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/esm/index.js",
|
"types": "./dist/index.d.ts",
|
||||||
"types": "./dist/types/index.d.ts",
|
|
||||||
"sideEffects": false,
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"require": {
|
|
||||||
"types": "./dist/types/index.d.ts",
|
|
||||||
"default": "./dist/cjs/index.js"
|
|
||||||
},
|
|
||||||
"import": {
|
|
||||||
"types": "./dist/types/index.d.ts",
|
|
||||||
"default": "./dist/esm/index.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"homepage": "https://notesnook.com/",
|
"homepage": "https://notesnook.com/",
|
||||||
"repository": "https://github.com/streetwriters/notesnook",
|
"repository": "https://github.com/streetwriters/notesnook",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/core": "4.11.4",
|
"@notesnook/crypto": "file:../../packages/crypto",
|
||||||
"@notesnook/intl": "file:../../packages/intl",
|
"@trpc/client": "10.38.3",
|
||||||
"@trpc/client": "10.45.2",
|
"@trpc/server": "10.38.3",
|
||||||
"@trpc/server": "10.45.2",
|
"electron-trpc": "0.5.2",
|
||||||
"better-sqlite3-multiple-ciphers": "11.5.0",
|
"electron-updater": "6.1.4",
|
||||||
"electron-trpc": "0.6.1",
|
"icojs": "^0.17.1",
|
||||||
"electron-updater": "^6.3.4",
|
|
||||||
"icojs": "^0.19.4",
|
|
||||||
"sqlite-better-trigram": "^0.0.2",
|
|
||||||
"typed-emitter": "^2.1.0",
|
"typed-emitter": "^2.1.0",
|
||||||
"yargs": "^17.7.2",
|
"yargs": "^17.6.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@streetwriters/kysely": "^0.27.4",
|
"@types/node": "18.16.1",
|
||||||
"@types/node": "22.7.0",
|
"@types/yargs": "^17.0.24",
|
||||||
"@types/yargs": "^17.0.33",
|
"chokidar": "^3.5.3",
|
||||||
"chokidar": "^4.0.1",
|
"electron": "25.9.8",
|
||||||
"electron": "^31.7.4",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder-notarize": "^1.5.1",
|
||||||
"esbuild": "^0.24.0",
|
"esbuild": "^0.17.19",
|
||||||
"node-abi": "^3.68.0",
|
|
||||||
"node-gyp-build": "^4.8.2",
|
|
||||||
"playwright": "^1.48.2",
|
|
||||||
"prebuildify": "^6.0.1",
|
|
||||||
"slugify": "^1.6.6",
|
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"undici": "^6.19.8",
|
"undici": "^5.23.0"
|
||||||
"vitest": "^2.1.5"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"dmg-license": "^1.0.11"
|
"dmg-license": "^1.0.11"
|
||||||
@@ -62,11 +39,10 @@
|
|||||||
"start": "node scripts/dev.mjs",
|
"start": "node scripts/dev.mjs",
|
||||||
"staging": "node scripts/build.mjs --run",
|
"staging": "node scripts/build.mjs --run",
|
||||||
"release": "node scripts/build.mjs",
|
"release": "node scripts/build.mjs",
|
||||||
"build": "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",
|
"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",
|
"postinstall": "patch-package"
|
||||||
"test": "node --test --test-timeout 30000 --test-force-exit"
|
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Streetwriters (Private) Limited",
|
"name": "Streetwriters (Private) Limited",
|
||||||
@@ -78,7 +54,7 @@
|
|||||||
"productName": "Notesnook",
|
"productName": "Notesnook",
|
||||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||||
"generateUpdatesFilesForAllChannels": true,
|
"npmRebuild": false,
|
||||||
"asar": false,
|
"asar": false,
|
||||||
"files": [
|
"files": [
|
||||||
"!*.chunk.js.map",
|
"!*.chunk.js.map",
|
||||||
@@ -88,30 +64,9 @@
|
|||||||
"!build/banner.jpg",
|
"!build/banner.jpg",
|
||||||
"!build/*.ico",
|
"!build/*.ico",
|
||||||
"!build/*.png",
|
"!build/*.png",
|
||||||
"!node_modules${/*}",
|
"!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/sqlite-better-trigram",
|
|
||||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
|
||||||
{
|
|
||||||
"from": "node_modules/sqlite-better-trigram-linux-${arch}",
|
|
||||||
"to": "node_modules/sqlite-better-trigram-linux-${arch}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "node_modules/sqlite-better-trigram-darwin-${arch}",
|
|
||||||
"to": "node_modules/sqlite-better-trigram-darwin-${arch}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "node_modules/sqlite-better-trigram-windows-${arch}",
|
|
||||||
"to": "node_modules/sqlite-better-trigram-windows-${arch}"
|
|
||||||
},
|
|
||||||
"node_modules/sodium-native/index.js",
|
|
||||||
"node_modules/sodium-native/package.json"
|
|
||||||
],
|
],
|
||||||
|
"afterSign": "electron-builder-notarize",
|
||||||
"afterPack": "./scripts/removeLocales.js",
|
"afterPack": "./scripts/removeLocales.js",
|
||||||
"mac": {
|
"mac": {
|
||||||
"bundleVersion": "240",
|
"bundleVersion": "240",
|
||||||
@@ -139,8 +94,7 @@
|
|||||||
"entitlements": "assets/entitlements.mac.plist",
|
"entitlements": "assets/entitlements.mac.plist",
|
||||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||||
"gatekeeperAssess": false,
|
"gatekeeperAssess": false,
|
||||||
"icon": "assets/icons/app.icns",
|
"icon": "assets/icons/app.icns"
|
||||||
"notarize": true
|
|
||||||
},
|
},
|
||||||
"dmg": {
|
"dmg": {
|
||||||
"contents": [
|
"contents": [
|
||||||
@@ -181,12 +135,10 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"signtoolOptions": {
|
"signingHashAlgorithms": [
|
||||||
"signingHashAlgorithms": [
|
"sha256"
|
||||||
"sha256"
|
],
|
||||||
],
|
"sign": "./sign.js",
|
||||||
"sign": "./scripts/sign.js"
|
|
||||||
},
|
|
||||||
"icon": "assets/icons/app.ico"
|
"icon": "assets/icons/app.ico"
|
||||||
},
|
},
|
||||||
"portable": {
|
"portable": {
|
||||||
@@ -194,8 +146,7 @@
|
|||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": true,
|
"oneClick": true,
|
||||||
"createDesktopShortcut": "always",
|
"createDesktopShortcut": "always"
|
||||||
"deleteAppDataOnUninstall": true
|
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||||
index 739a48c..7a7b4af 100644
|
index fcb7f54..cb1c7f7 100644
|
||||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||||
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
|
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||||
StartupWMClass: appInfo.productName,
|
StartupWMClass: appInfo.productName,
|
||||||
...extra,
|
...extra,
|
||||||
...targetSpecificOptions.desktop,
|
...targetSpecificOptions.desktop,
|
||||||
@@ -10,7 +10,7 @@ index 739a48c..7a7b4af 100644
|
|||||||
};
|
};
|
||||||
const description = this.getDescription(targetSpecificOptions);
|
const description = this.getDescription(targetSpecificOptions);
|
||||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||||
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
|
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||||
data += `\n${name}=${desktopMeta[name]}`;
|
data += `\n${name}=${desktopMeta[name]}`;
|
||||||
}
|
}
|
||||||
data += "\n";
|
data += "\n";
|
||||||
23
apps/desktop/patches/electron-trpc+0.5.2.patch
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||||
|
index 379cf3b..2644e5d 100644
|
||||||
|
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||||
|
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||||
|
@@ -221,9 +221,16 @@ class G {
|
||||||
|
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
|
||||||
|
}
|
||||||
|
detachWindow(r) {
|
||||||
|
+
|
||||||
|
y(this, c, i(this, c).filter((n) => n !== r));
|
||||||
|
- for (const [n, t] of i(this, u).entries())
|
||||||
|
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||||
|
+ for (const [n, t] of i(this, u).entries()) {
|
||||||
|
+ try {
|
||||||
|
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||||
|
+ } catch(e) {
|
||||||
|
+ console.error(e);
|
||||||
|
+ // ignore
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
|
||||||
index 07971db..b67a3cf 100644
|
|
||||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
|
||||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
|
||||||
@@ -537,8 +537,15 @@ class be {
|
|
||||||
attachWindow(r) {
|
|
||||||
F(this, w).includes(r) || (y("Attaching window", r.id), F(this, w).push(r), v(this, N, Z).call(this, r));
|
|
||||||
}
|
|
||||||
- detachWindow(r) {
|
|
||||||
- y("Detaching window", r.id), L(this, w, F(this, w).filter((e) => e !== r)), v(this, R, M).call(this, { webContentsId: r.webContents.id });
|
|
||||||
+ detachWindow(r, webContentsId) {
|
|
||||||
+ y("Detaching window", r.id);
|
|
||||||
+
|
|
||||||
+ if (r.isDestroyed() && webContentsId === undefined) {
|
|
||||||
+ throw new Error('webContentsId is required when calling detachWindow on a destroyed window');
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ L(this, w, F(this, w).filter((e) => e !== r));
|
|
||||||
+ v(this, R, M).call(this, { webContentsId: webContentsId ?? r.webContents.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
|
||||||
@@ -548,6 +555,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
|
||||||
for (const [i, a] of F(this, E).entries())
|
|
||||||
i.startsWith(`${r}-${e ?? ""}`) && (y("Closing subscription", i), a.unsubscribe(), F(this, E).delete(i));
|
|
||||||
}, N = new WeakSet(), Z = function(r) {
|
|
||||||
+ const webContentsId = r.webContents.id;
|
|
||||||
r.webContents.on("did-start-navigation", ({ frame: e }) => {
|
|
||||||
y(
|
|
||||||
"Handling webContents `did-start-navigation` event",
|
|
||||||
@@ -558,7 +566,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
|
||||||
frameRoutingId: e.routingId
|
|
||||||
});
|
|
||||||
}), r.webContents.on("destroyed", () => {
|
|
||||||
- y("Handling webContents `destroyed` event"), this.detachWindow(r);
|
|
||||||
+ y("Handling webContents `destroyed` event"), this.detachWindow(r, webContentsId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const me = ({
|
|
||||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs, { readFile, writeFile } from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { existsSync, readFileSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import yargs from "yargs-parser";
|
import yargs from "yargs-parser";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
@@ -28,76 +28,90 @@ import { fileURLToPath } from "url";
|
|||||||
const args = yargs(process.argv);
|
const args = yargs(process.argv);
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const packageJson = JSON.parse(
|
const sodiumNativePrebuildPath = (arch) =>
|
||||||
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
|
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"));
|
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||||
|
|
||||||
await fs.rm("./build/", { force: true, recursive: true });
|
await fs.rm("./build/", { force: true, recursive: true });
|
||||||
|
|
||||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||||
await exec(
|
await exec(`cd ${webAppPath} && npm run build:desktop`);
|
||||||
"yarn nx build:desktop @notesnook/web",
|
|
||||||
path.join(__dirname, "..", "..", "..")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// temporary until there's support for prebuilt binaries for linux ARM
|
|
||||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
|
||||||
|
|
||||||
// if (os.platform() === "win32")
|
|
||||||
// await exec(
|
|
||||||
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
|
||||||
// path.join(__dirname, "..", "node_modules", "sodium-native")
|
|
||||||
// );
|
|
||||||
|
|
||||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true
|
force: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (args.variant === "mas") {
|
if (args.variant === "mas") {
|
||||||
await exec(`yarn run bundle:mas`);
|
await exec(`npm run bundle:mas`);
|
||||||
} else {
|
} else {
|
||||||
await exec(`yarn run bundle`);
|
await exec(`npm run bundle`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await exec(`yarn run build`);
|
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) {
|
if (args.run) {
|
||||||
await exec(`yarn electron-builder --dir --x64`);
|
await exec(`npx electron-builder --dir --x64`);
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
await exec(`./output/darwin-unpacked/Notesnook`);
|
||||||
} else {
|
} else {
|
||||||
await exec(`./output/linux-unpacked/Notesnook`);
|
await exec(`./output/linux-unpacked/Notesnook`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exec(cmd, cwd) {
|
async function exec(cmd) {
|
||||||
return childProcess.execSync(cmd, {
|
return childProcess.execSync(cmd, {
|
||||||
env: { ...process.env, NOTESNOOK_STAGING: true },
|
env: process.env,
|
||||||
stdio: "inherit",
|
stdio: "inherit"
|
||||||
cwd: cwd || process.cwd()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function patchBetterSQLite3() {
|
|
||||||
const jsonPath = path.join(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"node_modules",
|
|
||||||
"better-sqlite3-multiple-ciphers",
|
|
||||||
"package.json"
|
|
||||||
);
|
|
||||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
|
||||||
|
|
||||||
json.version = "11.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,16 +27,20 @@ import crypto from "crypto";
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
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 RUNNING_PROCESSES = [];
|
||||||
const RESTARTABLE_PROCESSES = [];
|
const RESTARTABLE_PROCESSES = [];
|
||||||
let lastBundleHash = null;
|
let lastBundleHash = null;
|
||||||
const ENV = {
|
|
||||||
...process.env,
|
|
||||||
NO_COLOR: "true",
|
|
||||||
FORCE_COLOR: "false",
|
|
||||||
COLOR: "0"
|
|
||||||
};
|
|
||||||
process.chdir(path.join(__dirname, ".."));
|
|
||||||
|
|
||||||
await onChange(true);
|
await onChange(true);
|
||||||
|
|
||||||
@@ -54,23 +58,26 @@ process.on("SIGINT", async (s) => {
|
|||||||
async function onChange(first) {
|
async function onChange(first) {
|
||||||
if (first) {
|
if (first) {
|
||||||
await fs.rm("./build/", { force: true, recursive: true });
|
await fs.rm("./build/", { force: true, recursive: true });
|
||||||
|
|
||||||
await exec("npm rebuild electron --verbose --foreground-scripts");
|
|
||||||
|
|
||||||
await exec("yarn electron-builder install-app-deps");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await exec(`yarn run bundle`);
|
await exec(`npm run bundle`);
|
||||||
await exec(`yarn run build`);
|
execAsync(`npx`, [`tsc`]);
|
||||||
|
|
||||||
if (await isBundleSame()) {
|
if (await isBundleSame()) {
|
||||||
console.log("Bundle is same. Doing nothing.");
|
console.log("Bundle is same. Doing nothing.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
await spawnAndWaitUntil(
|
await spawnAndWaitUntil(
|
||||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||||
path.join(__dirname, "..", "..", ".."),
|
path.join(__dirname, "..", "..", ".."),
|
||||||
(data) => data.includes("Network: use --host to expose")
|
(data) => data.includes("Network: use --host to expose")
|
||||||
);
|
);
|
||||||
@@ -82,7 +89,7 @@ async function onChange(first) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
execAsync(
|
execAsync(
|
||||||
"yarn",
|
"npx",
|
||||||
["electron", path.join("build", "electron.js")],
|
["electron", path.join("build", "electron.js")],
|
||||||
true,
|
true,
|
||||||
cleanup
|
cleanup
|
||||||
@@ -95,7 +102,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
|||||||
|
|
||||||
const s = spawn(cmd[0], cmd.slice(1), {
|
const s = spawn(cmd[0], cmd.slice(1), {
|
||||||
cwd,
|
cwd,
|
||||||
env: ENV,
|
env: { ...process.env, NO_COLOR: "true" },
|
||||||
shell: false
|
shell: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,15 +116,14 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exec(cmd, cwd) {
|
async function exec(cmd) {
|
||||||
try {
|
try {
|
||||||
console.log(">", cmd);
|
console.log(">", cmd);
|
||||||
|
|
||||||
return execSync(cmd, {
|
return execSync(cmd, {
|
||||||
env: ENV,
|
env: process.env,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
shell: false,
|
shell: false
|
||||||
cwd: cwd || process.cwd()
|
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
//ignore
|
//ignore
|
||||||
@@ -130,7 +136,7 @@ function execAsync(cmd, args, restartable, onExit) {
|
|||||||
|
|
||||||
const proc = spawn(cmd, args, {
|
const proc = spawn(cmd, args, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
env: ENV,
|
env: process.env,
|
||||||
shell: false
|
shell: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,10 +179,9 @@ async function cleanup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function isBundleSame() {
|
async function isBundleSame() {
|
||||||
const bundle = Buffer.concat([
|
const bundle = await fs.readFile(
|
||||||
await fs.readFile(path.join(__dirname, "..", "build", "electron.js")),
|
path.join(__dirname, "..", "build", "electron.js")
|
||||||
await fs.readFile(path.join(__dirname, "..", "build", "preload.js"))
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
const hashSum = crypto.createHash("sha256");
|
const hashSum = crypto.createHash("sha256");
|
||||||
hashSum.update(bundle);
|
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
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 { execSync } = require("child_process");
|
||||||
const { relative, join } = require("path");
|
const { relative, join } = require("path");
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
module.exports = async function (configuration) {
|
||||||
if (process.env.NOTESNOOK_STAGING) return;
|
|
||||||
|
|
||||||
const Endpoint = "https://weu.codesigning.azure.net";
|
const Endpoint = "https://weu.codesigning.azure.net";
|
||||||
const CodeSigningAccountName = "Notesnook";
|
const CodeSigningAccountName = "Notesnook";
|
||||||
const CertificateProfileName = "Notesnook";
|
const CertificateProfileName = "Notesnook";
|
||||||
@@ -33,7 +31,7 @@ module.exports = async function (configuration) {
|
|||||||
const DescriptionUrl = "https://notesnook.com/";
|
const DescriptionUrl = "https://notesnook.com/";
|
||||||
const FilesCatalog = createCatalog(configuration.path);
|
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);
|
console.debug("Signing", configuration.path, "using command", command);
|
||||||
|
|
||||||
@@ -41,13 +39,16 @@ module.exports = async function (configuration) {
|
|||||||
|
|
||||||
console.debug("Signed", configuration.path);
|
console.debug("Signed", configuration.path);
|
||||||
|
|
||||||
rmSync(FilesCatalog);
|
deleteCatalog();
|
||||||
};
|
};
|
||||||
|
|
||||||
function createCatalog(path) {
|
function createCatalog(path) {
|
||||||
const catalogPath = join(__dirname, "_catalog");
|
writeFileSync("_catalog", relative(__dirname, path));
|
||||||
writeFileSync(catalogPath, relative(__dirname, path));
|
return join(__dirname, "_catalog");
|
||||||
return catalogPath;
|
}
|
||||||
|
|
||||||
|
function deleteCatalog() {
|
||||||
|
rmSync("_catalog");
|
||||||
}
|
}
|
||||||
|
|
||||||
function psexec(cmd) {
|
function psexec(cmd) {
|
||||||
@@ -23,8 +23,6 @@ import { osIntegrationRouter } from "./os-integration";
|
|||||||
import { spellCheckerRouter } from "./spell-checker";
|
import { spellCheckerRouter } from "./spell-checker";
|
||||||
import { updaterRouter } from "./updater";
|
import { updaterRouter } from "./updater";
|
||||||
import { bridgeRouter } from "./bridge";
|
import { bridgeRouter } from "./bridge";
|
||||||
import { safeStorageRouter } from "./safe-storage";
|
|
||||||
import { windowRouter } from "./window";
|
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
|
|
||||||
@@ -33,9 +31,7 @@ export const router = t.router({
|
|||||||
integration: osIntegrationRouter,
|
integration: osIntegrationRouter,
|
||||||
spellChecker: spellCheckerRouter,
|
spellChecker: spellCheckerRouter,
|
||||||
updater: updaterRouter,
|
updater: updaterRouter,
|
||||||
bridge: bridgeRouter,
|
bridge: bridgeRouter
|
||||||
safeStorage: safeStorageRouter,
|
|
||||||
window: windowRouter
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const api = router.createCaller({});
|
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 { initTRPC } from "@trpc/server";
|
||||||
import { z } from "zod";
|
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 { AutoLaunch } from "../utils/autolaunch";
|
||||||
import { config, DesktopIntegration } from "../utils/config";
|
import { config, DesktopIntegration } from "../utils/config";
|
||||||
import { bringToFront } from "../utils/bring-to-front";
|
import { bringToFront } from "../utils/bring-to-front";
|
||||||
@@ -31,8 +31,6 @@ import { observable } from "@trpc/server/observable";
|
|||||||
import { AssetManager } from "../utils/asset-manager";
|
import { AssetManager } from "../utils/asset-manager";
|
||||||
import { isFlatpak } from "../utils";
|
import { isFlatpak } from "../utils";
|
||||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||||
import { rm } from "fs/promises";
|
|
||||||
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
|
|
||||||
@@ -56,15 +54,6 @@ export const osIntegrationRouter = t.router({
|
|||||||
config.zoomFactor = factor;
|
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),
|
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||||
setProxyRules: t.procedure
|
setProxyRules: t.procedure
|
||||||
.input(z.string().optional())
|
.input(z.string().optional())
|
||||||
@@ -145,17 +134,10 @@ export const osIntegrationRouter = t.router({
|
|||||||
.input(z.object({ filePath: z.string() }))
|
.input(z.object({ filePath: z.string() }))
|
||||||
.query(({ input }) => {
|
.query(({ input }) => {
|
||||||
const { filePath } = input;
|
const { filePath } = input;
|
||||||
|
if (!filePath) return;
|
||||||
return resolvePath(filePath);
|
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
|
showNotification: t.procedure
|
||||||
.input(NotificationOptions)
|
.input(NotificationOptions)
|
||||||
.query(({ input }) => {
|
.query(({ input }) => {
|
||||||
@@ -184,45 +166,16 @@ export const osIntegrationRouter = t.router({
|
|||||||
}),
|
}),
|
||||||
bringToFront: t.procedure.query(() => bringToFront()),
|
bringToFront: t.procedure.query(() => bringToFront()),
|
||||||
changeTheme: t.procedure
|
changeTheme: t.procedure
|
||||||
.input(
|
.input(Theme)
|
||||||
z.object({
|
.mutation(({ input }) => setTheme(input)),
|
||||||
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);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
onThemeChanged: t.procedure.subscription(() =>
|
onThemeChanged: t.procedure.subscription(() =>
|
||||||
observable<"dark" | "light">((emit) => {
|
observable<"dark" | "light">((emit) => {
|
||||||
const updated = () => {
|
nativeTheme.on("updated", () => {
|
||||||
if (getTheme() === "system") {
|
if (getTheme() === "system") {
|
||||||
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
|
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"));
|
|
||||||
})
|
|
||||||
});
|
|
||||||
@@ -1,145 +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 "@streetwriters/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);
|
|
||||||
const betterTrigram = require("sqlite-better-trigram");
|
|
||||||
betterTrigram.load(this.sqlite);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 };
|
type Progress = { percent: number };
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
let cancellationToken: CancellationToken | undefined = undefined;
|
|
||||||
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
|
|
||||||
|
|
||||||
export const updaterRouter = t.router({
|
export const updaterRouter = t.router({
|
||||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||||
download: t.procedure.query(async () => {
|
download: t.procedure.query(async () => {
|
||||||
if (cancellationToken) return;
|
const cancellationToken = new CancellationToken();
|
||||||
clearTimeout(downloadTimeout);
|
await autoUpdater.downloadUpdate(cancellationToken);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
check: t.procedure.query(async () => {
|
check: t.procedure.query(async () => {
|
||||||
if (cancellationToken) return;
|
await autoUpdater.checkForUpdates();
|
||||||
clearTimeout(downloadTimeout);
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
downloadTimeout = setTimeout(async () => {
|
|
||||||
await autoUpdater
|
|
||||||
.checkForUpdates()
|
|
||||||
.catch(console.error)
|
|
||||||
.finally(resolve);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
toggleAutoUpdates: t.procedure
|
toggleAutoUpdates: t.procedure
|
||||||
@@ -79,18 +57,7 @@ export const updaterRouter = t.router({
|
|||||||
"update-not-available"
|
"update-not-available"
|
||||||
),
|
),
|
||||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||||
"update-available",
|
"update-available"
|
||||||
() => {
|
|
||||||
if (!config.automaticUpdates) return false;
|
|
||||||
autoUpdater.emit("download-progress", {
|
|
||||||
bytesPerSecond: 0,
|
|
||||||
delta: 0,
|
|
||||||
percent: 0,
|
|
||||||
total: 100,
|
|
||||||
transferred: 0
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
onError: createSubscription("error")
|
onError: createSubscription("error")
|
||||||
});
|
});
|
||||||
@@ -98,14 +65,12 @@ export const updaterRouter = t.router({
|
|||||||
function createSubscription<
|
function createSubscription<
|
||||||
TName extends keyof AppUpdaterEvents,
|
TName extends keyof AppUpdaterEvents,
|
||||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||||
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
|
>(eventName: TName) {
|
||||||
return t.procedure.subscription(() => {
|
return t.procedure.subscription(() => {
|
||||||
return observable<TReturnType>((emit) => {
|
return observable<TReturnType>((emit) => {
|
||||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||||
if (handler?.(args[0])) return;
|
|
||||||
emit.next(args[0]);
|
emit.next(args[0]);
|
||||||
};
|
};
|
||||||
autoUpdater.removeAllListeners(eventName);
|
|
||||||
autoUpdater.addListener(eventName, listener);
|
autoUpdater.addListener(eventName, listener);
|
||||||
return () => {
|
return () => {
|
||||||
autoUpdater.removeListener(eventName, listener);
|
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
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
@@ -17,8 +17,6 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { PATHS } from "./constants";
|
export * from "./constants";
|
||||||
export type { AppRouter } from "./api";
|
export type { AppRouter } from "./api";
|
||||||
export { type UpdateInfo } from "builder-util-runtime";
|
export { type UpdateInfo } from "builder-util-runtime";
|
||||||
export { type DesktopIntegration } from "./utils/config";
|
|
||||||
export { SQLite } from "./api/sqlite-kysely.js";
|
|
||||||
|
|||||||
@@ -35,21 +35,6 @@ import path from "path";
|
|||||||
import { bringToFront } from "./utils/bring-to-front";
|
import { bringToFront } from "./utils/bring-to-front";
|
||||||
import { bridge } from "./api/bridge";
|
import { bridge } from "./api/bridge";
|
||||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||||
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
|
||||||
import { Messages, setI18nGlobal } from "@notesnook/intl";
|
|
||||||
import { i18n } from "@lingui/core";
|
|
||||||
|
|
||||||
const locale =
|
|
||||||
process.env.NODE_ENV === "development"
|
|
||||||
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
|
|
||||||
: import("@notesnook/intl/locales/$en.json");
|
|
||||||
locale.then(({ default: locale }) => {
|
|
||||||
i18n.load({
|
|
||||||
en: locale.messages as unknown as Messages
|
|
||||||
});
|
|
||||||
i18n.activate("en");
|
|
||||||
});
|
|
||||||
setI18nGlobal(i18n);
|
|
||||||
|
|
||||||
// only run a single instance
|
// only run a single instance
|
||||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||||
@@ -97,28 +82,10 @@ async function createWindow() {
|
|||||||
size: 512,
|
size: 512,
|
||||||
format: process.platform === "win32" ? "ico" : "png"
|
format: process.platform === "win32" ? "ico" : "png"
|
||||||
}),
|
}),
|
||||||
|
|
||||||
...(config.desktopSettings.nativeTitlebar
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
|
|
||||||
frame: process.platform === "win32" || process.platform === "darwin",
|
|
||||||
titleBarOverlay: {
|
|
||||||
height: 37,
|
|
||||||
color: "#00000000",
|
|
||||||
symbolColor: config.windowControlsIconColor
|
|
||||||
},
|
|
||||||
trafficLightPosition: {
|
|
||||||
x: 16,
|
|
||||||
y: 12
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
zoomFactor: config.zoomFactor,
|
zoomFactor: config.zoomFactor,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
nodeIntegrationInWorker: true,
|
|
||||||
spellcheck: config.isSpellCheckerEnabled,
|
spellcheck: config.isSpellCheckerEnabled,
|
||||||
preload: __dirname + "/preload.js"
|
preload: __dirname + "/preload.js"
|
||||||
}
|
}
|
||||||
@@ -155,7 +122,7 @@ async function createWindow() {
|
|||||||
setupJumplist();
|
setupJumplist();
|
||||||
|
|
||||||
if (isDevelopment())
|
if (isDevelopment())
|
||||||
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
|
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url);
|
||||||
@@ -171,9 +138,6 @@ async function createWindow() {
|
|||||||
app.once("ready", async () => {
|
app.once("ready", async () => {
|
||||||
console.info("App ready. Opening window.");
|
console.info("App ready. Opening window.");
|
||||||
|
|
||||||
if (config.customDns) enableCustomDns();
|
|
||||||
else disableCustomDns();
|
|
||||||
|
|
||||||
if (!isDevelopment()) registerProtocol();
|
if (!isDevelopment()) registerProtocol();
|
||||||
await createWindow();
|
await createWindow();
|
||||||
configureAutoUpdater();
|
configureAutoUpdater();
|
||||||
|
|||||||
@@ -19,25 +19,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
|
|
||||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||||
// import type { NNCrypto } from "@notesnook/crypto";
|
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||||
|
import type { NNCrypto } from "@notesnook/crypto";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { platform } from "os";
|
import { platform } from "os";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var os: () => "mas" | ReturnType<typeof platform>;
|
var os: () => "mas" | ReturnType<typeof platform>;
|
||||||
var electronTRPC: any;
|
var electronTRPC: RendererGlobalElectronTRPC;
|
||||||
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.once("loaded", async () => {
|
process.once("loaded", async () => {
|
||||||
const electronTRPC = {
|
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||||
sendMessage: (operation: any) =>
|
sendMessage: (operation) =>
|
||||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||||
onMessage: (callback: any) =>
|
onMessage: (callback) =>
|
||||||
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
|
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
|
||||||
};
|
};
|
||||||
globalThis.electronTRPC = electronTRPC;
|
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());
|
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import { NativeImage, nativeImage } from "electron";
|
import { NativeImage, nativeImage } from "electron";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { isDevelopment } from "./index";
|
import { isDevelopment } from "./index";
|
||||||
import { ParsedImage, parseICO } from "icojs";
|
import { parse, ParsedImage } from "icojs";
|
||||||
import { getSystemTheme } from "./theme";
|
import { getSystemTheme } from "./theme";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export class AssetManager {
|
|||||||
`${icon}${prefix}.ico`
|
`${icon}${prefix}.ico`
|
||||||
);
|
);
|
||||||
const icoBuffer = await readFile(icoPath);
|
const icoBuffer = await readFile(icoPath);
|
||||||
const images = await parseICO(icoBuffer, "image/png");
|
const images = await parse(icoBuffer, "image/png");
|
||||||
ALL_ICONS.push({ id: icon, images, prefix });
|
ALL_ICONS.push({ id: icon, images, prefix });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,22 +20,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import { autoUpdater } from "electron-updater";
|
import { autoUpdater } from "electron-updater";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
|
|
||||||
? "beta"
|
|
||||||
: "latest";
|
|
||||||
async function configureAutoUpdater() {
|
async function configureAutoUpdater() {
|
||||||
autoUpdater.setFeedURL({
|
autoUpdater.setFeedURL({
|
||||||
provider: "generic",
|
provider: "generic",
|
||||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
|
url: `https://notesnook.com/releases/${process.platform}/`,
|
||||||
useMultipleRangeRequest: false,
|
useMultipleRangeRequest: false
|
||||||
channel: CHANNEL
|
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.autoDownload = config.automaticUpdates;
|
autoUpdater.autoDownload = config.automaticUpdates;
|
||||||
autoUpdater.allowDowngrade = false;
|
autoUpdater.allowDowngrade = false;
|
||||||
autoUpdater.allowPrerelease = false;
|
autoUpdater.allowPrerelease = false;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
autoUpdater.disableWebInstaller = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { configureAutoUpdater };
|
export { configureAutoUpdater };
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ export const DesktopIntegration = z.object({
|
|||||||
autoStart: z.boolean().optional(),
|
autoStart: z.boolean().optional(),
|
||||||
startMinimized: z.boolean().optional(),
|
startMinimized: z.boolean().optional(),
|
||||||
minimizeToSystemTray: z.boolean().optional(),
|
minimizeToSystemTray: z.boolean().optional(),
|
||||||
closeToSystemTray: z.boolean().optional(),
|
closeToSystemTray: z.boolean().optional()
|
||||||
nativeTitlebar: z.boolean().optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||||
@@ -36,20 +35,14 @@ export const config = {
|
|||||||
autoStart: false,
|
autoStart: false,
|
||||||
startMinimized: false,
|
startMinimized: false,
|
||||||
minimizeToSystemTray: false,
|
minimizeToSystemTray: false,
|
||||||
closeToSystemTray: false,
|
closeToSystemTray: false
|
||||||
nativeTitlebar: false
|
|
||||||
},
|
},
|
||||||
privacyMode: false,
|
privacyMode: false,
|
||||||
isSpellCheckerEnabled: true,
|
isSpellCheckerEnabled: true,
|
||||||
zoomFactor: 1,
|
zoomFactor: 1,
|
||||||
theme: nativeTheme.themeSource,
|
theme: nativeTheme.themeSource,
|
||||||
automaticUpdates: true,
|
automaticUpdates: true,
|
||||||
proxyRules: "",
|
proxyRules: ""
|
||||||
customDns: true,
|
|
||||||
|
|
||||||
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
|
|
||||||
windowControlsIconColor:
|
|
||||||
nativeTheme.themeSource === "dark" ? "#ffffff" : "#000000"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfigKey = keyof typeof config;
|
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
import { Menu, MenuItem, clipboard, shell } from "electron";
|
import { Menu, MenuItem, clipboard, shell } from "electron";
|
||||||
|
|
||||||
function setupMenu() {
|
function setupMenu() {
|
||||||
@@ -41,7 +40,7 @@ function setupMenu() {
|
|||||||
if (params.misspelledWord) {
|
if (params.misspelledWord) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.addToDictionary(),
|
label: "Add to dictionary",
|
||||||
click: () =>
|
click: () =>
|
||||||
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
|
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
|
||||||
params.misspelledWord
|
params.misspelledWord
|
||||||
@@ -60,7 +59,7 @@ function setupMenu() {
|
|||||||
if (params.linkURL.length) {
|
if (params.linkURL.length) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.openInBrowser(),
|
label: "Open in browser",
|
||||||
click: () => shell.openExternal(params.linkURL)
|
click: () => shell.openExternal(params.linkURL)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -69,7 +68,7 @@ function setupMenu() {
|
|||||||
if (params.isEditable) {
|
if (params.isEditable) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.undo(),
|
label: "Undo",
|
||||||
role: "undo",
|
role: "undo",
|
||||||
enabled: params.isEditable,
|
enabled: params.isEditable,
|
||||||
accelerator: "CommandOrControl+Z"
|
accelerator: "CommandOrControl+Z"
|
||||||
@@ -78,7 +77,7 @@ function setupMenu() {
|
|||||||
|
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.redo(),
|
label: "Redo",
|
||||||
role: "redo",
|
role: "redo",
|
||||||
enabled: params.isEditable,
|
enabled: params.isEditable,
|
||||||
accelerator: "CommandOrControl+Y"
|
accelerator: "CommandOrControl+Y"
|
||||||
@@ -95,7 +94,7 @@ function setupMenu() {
|
|||||||
if (params.isEditable)
|
if (params.isEditable)
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.cut(),
|
label: "Cut",
|
||||||
role: "cut",
|
role: "cut",
|
||||||
enabled: params.selectionText.length > 0,
|
enabled: params.selectionText.length > 0,
|
||||||
accelerator: "CommandOrControl+X"
|
accelerator: "CommandOrControl+X"
|
||||||
@@ -105,7 +104,7 @@ function setupMenu() {
|
|||||||
if (params.linkURL?.length) {
|
if (params.linkURL?.length) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.copyLink(),
|
label: "Copy link",
|
||||||
click() {
|
click() {
|
||||||
clipboard.writeText(params.linkURL);
|
clipboard.writeText(params.linkURL);
|
||||||
}
|
}
|
||||||
@@ -114,7 +113,7 @@ function setupMenu() {
|
|||||||
|
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.copyLinkText(),
|
label: "Copy link text",
|
||||||
click() {
|
click() {
|
||||||
clipboard.writeText(params.linkText);
|
clipboard.writeText(params.linkText);
|
||||||
}
|
}
|
||||||
@@ -125,7 +124,7 @@ function setupMenu() {
|
|||||||
if (params.selectionText.length) {
|
if (params.selectionText.length) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.copy(),
|
label: "Copy",
|
||||||
role: "copy",
|
role: "copy",
|
||||||
accelerator: "CommandOrControl+C"
|
accelerator: "CommandOrControl+C"
|
||||||
})
|
})
|
||||||
@@ -136,7 +135,7 @@ function setupMenu() {
|
|||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
id: "copy-image",
|
id: "copy-image",
|
||||||
label: strings.copyImage(),
|
label: "Copy Image",
|
||||||
click() {
|
click() {
|
||||||
globalThis.window?.webContents.copyImageAt(params.x, params.y);
|
globalThis.window?.webContents.copyImageAt(params.x, params.y);
|
||||||
}
|
}
|
||||||
@@ -146,7 +145,7 @@ function setupMenu() {
|
|||||||
if (params.isEditable)
|
if (params.isEditable)
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.paste(),
|
label: "Paste",
|
||||||
role: "pasteAndMatchStyle",
|
role: "pasteAndMatchStyle",
|
||||||
enabled: clipboard.readText("clipboard").length > 0,
|
enabled: clipboard.readText("clipboard").length > 0,
|
||||||
accelerator: "CommandOrControl+V"
|
accelerator: "CommandOrControl+V"
|
||||||
|
|||||||
@@ -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/>.
|
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 { isDevelopment } from "./index";
|
||||||
import { createReadStream } from "fs";
|
import { createReadStream } from "fs";
|
||||||
import { extname, normalize } from "path";
|
import { extname, normalize } from "path";
|
||||||
@@ -62,7 +62,26 @@ function registerProtocol() {
|
|||||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||||
});
|
});
|
||||||
} else {
|
} 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"`);
|
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||||
|
|||||||
@@ -41,10 +41,7 @@ function setTheme(theme: Theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBackgroundColor() {
|
function getBackgroundColor() {
|
||||||
return (
|
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
|
||||||
config.backgroundColor ||
|
|
||||||
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemTheme() {
|
function getSystemTheme() {
|
||||||
@@ -57,8 +54,7 @@ function getSystemTheme() {
|
|||||||
nativeTheme.themeSource = oldThemeSource;
|
nativeTheme.themeSource = oldThemeSource;
|
||||||
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||||
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
|
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
return currentTheme;
|
return currentTheme;
|
||||||
@@ -73,8 +69,7 @@ function changeTheme(theme: Theme) {
|
|||||||
nativeTheme.themeSource = theme;
|
nativeTheme.themeSource = theme;
|
||||||
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||||
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
|
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
"extends": "../../tsconfig",
|
"extends": "../../tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"]
|
||||||
"moduleResolution": "Bundler"
|
|
||||||
},
|
},
|
||||||
"include": ["src", "global.d.ts"]
|
"include": ["src", "global.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
3
apps/mobile/.gitignore
vendored
@@ -38,8 +38,7 @@ DerivedData
|
|||||||
*.ipa
|
*.ipa
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
*.hprof
|
*.hprof
|
||||||
**/.xcode.env.local
|
ios/.xcode.env.local
|
||||||
native/cache
|
|
||||||
# Android/IntelliJ
|
# Android/IntelliJ
|
||||||
#
|
#
|
||||||
rn-build-deps/
|
rn-build-deps/
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { i18n } from "@lingui/core";
|
import "@azure/core-asynciterator-polyfill";
|
||||||
import { I18nProvider } from "@lingui/react";
|
|
||||||
import {
|
import {
|
||||||
THEME_COMPATIBILITY_VERSION,
|
THEME_COMPATIBILITY_VERSION,
|
||||||
useThemeEngineStore
|
useThemeEngineStore
|
||||||
@@ -42,32 +41,34 @@ import { useUserStore } from "./stores/use-user-store";
|
|||||||
I18nManager.allowRTL(false);
|
I18nManager.allowRTL(false);
|
||||||
I18nManager.forceRTL(false);
|
I18nManager.forceRTL(false);
|
||||||
I18nManager.swapLeftAndRightInRTL(false);
|
I18nManager.swapLeftAndRightInRTL(false);
|
||||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
|
||||||
if (appLockEnabled || appLockMode !== "none") {
|
|
||||||
useUserStore.getState().lockApp(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
SettingsService.checkOrientation();
|
||||||
const App = () => {
|
const App = () => {
|
||||||
useAppEvents();
|
const init = useAppEvents();
|
||||||
//@ts-ignore
|
|
||||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
SettingsService.onFirstLaunch();
|
let { appLockMode } = SettingsService.get();
|
||||||
|
if (appLockMode && appLockMode !== "none") {
|
||||||
|
useUserStore.getState().lockApp(true);
|
||||||
|
}
|
||||||
|
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||||
|
init();
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
SettingsService.onFirstLaunch();
|
||||||
await Notifications.get();
|
await Notifications.get();
|
||||||
if (SettingsService.get().notifNotes) {
|
if (SettingsService.get().notifNotes) {
|
||||||
Notifications.pinQuickNote(true);
|
Notifications.pinQuickNote(true);
|
||||||
}
|
}
|
||||||
TipManager.init();
|
TipManager.init();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%"
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center"
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@@ -100,10 +101,10 @@ const App = () => {
|
|||||||
let currTheme =
|
let currTheme =
|
||||||
useThemeStore.getState().colorScheme === "dark"
|
useThemeStore.getState().colorScheme === "dark"
|
||||||
? SettingsService.getProperty("darkTheme")
|
? SettingsService.getProperty("darkTheme")
|
||||||
: SettingsService.getProperty("lighTheme");
|
: SettingsService.getProperty("lightTheme");
|
||||||
useThemeEngineStore.getState().setTheme(currTheme);
|
useThemeEngineStore.getState().setTheme(currTheme);
|
||||||
|
|
||||||
export const withTheme = (Element: () => JSX.Element) => {
|
export const withTheme = (Element) => {
|
||||||
return function AppWithThemeProvider() {
|
return function AppWithThemeProvider() {
|
||||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||||
state.colorScheme,
|
state.colorScheme,
|
||||||
@@ -142,11 +143,7 @@ export const withTheme = (Element: () => JSX.Element) => {
|
|||||||
currTheme = nextTheme;
|
currTheme = nextTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Element />;
|
||||||
<I18nProvider i18n={i18n}>
|
|
||||||
<Element />
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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/>.
|
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 { Platform } from "react-native";
|
||||||
import "react-native-get-random-values";
|
import "react-native-get-random-values";
|
||||||
import * as Keychain from "react-native-keychain";
|
import * as Keychain from "react-native-keychain";
|
||||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
|
||||||
import { generateSecureRandom } from "react-native-securerandom";
|
import { generateSecureRandom } from "react-native-securerandom";
|
||||||
import { DatabaseLogger } from ".";
|
import Sodium from "@ammarahmed/react-native-sodium";
|
||||||
import { MMKV } from "./mmkv";
|
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_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
||||||
|
|
||||||
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 KEYSTORE_CONFIG = Platform.select({
|
const KEYSTORE_CONFIG = Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
@@ -60,227 +37,61 @@ const KEYSTORE_CONFIG = Platform.select({
|
|||||||
android: {}
|
android: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
function generatePassword() {
|
export async function deriveCryptoKey(name, data) {
|
||||||
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) {
|
|
||||||
try {
|
try {
|
||||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||||
const userKeyCipher = await encrypt(
|
await Keychain.setInternetCredentials(
|
||||||
{
|
"notesnook",
|
||||||
key: await getDatabaseKey(),
|
name,
|
||||||
salt: NOTESNOOK_DB_KEY_SALT
|
credentials.key,
|
||||||
},
|
KEYSTORE_CONFIG
|
||||||
credentials.key
|
|
||||||
);
|
);
|
||||||
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||||
|
|
||||||
// Store encrypted user key in MMKV
|
|
||||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
|
||||||
return credentials.key;
|
return credentials.key;
|
||||||
} catch (e) {
|
} 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) {
|
export async function getCryptoKey(_name) {
|
||||||
try {
|
try {
|
||||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||||
|
let credentials = await Keychain.getInternetCredentials(
|
||||||
if (!keyCipher) {
|
"notesnook",
|
||||||
DatabaseLogger.info("User key cipher is null");
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = await decrypt(
|
|
||||||
{
|
|
||||||
key: await getDatabaseKey(),
|
|
||||||
salt: keyCipher.salt
|
|
||||||
},
|
|
||||||
keyCipher
|
|
||||||
);
|
|
||||||
|
|
||||||
return key;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("getCryptoKey", e);
|
console.error(e);
|
||||||
DatabaseLogger.error(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeCryptoKey(_name) {
|
export async function removeCryptoKey(_name) {
|
||||||
try {
|
try {
|
||||||
MMKV.removeItem(USER_KEY_CIPHER);
|
let result = await Keychain.resetInternetCredentials("notesnook");
|
||||||
await Keychain.resetInternetCredentials("notesnook");
|
return result;
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} 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);
|
let credentials = await Sodium.deriveKey(password, salt || null);
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DatabaseLogger.error(e);
|
console.log("generateCryptoKey: ", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,8 +123,6 @@ export async function decrypt(password, data) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
let _data = { ...data };
|
let _data = { ...data };
|
||||||
_data.output = "plain";
|
_data.output = "plain";
|
||||||
|
|
||||||
if (!password.salt) password.salt = data.salt;
|
|
||||||
return await Sodium.decrypt(password, _data);
|
return await Sodium.decrypt(password, _data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,11 +135,6 @@ export async function decryptMulti(password, data) {
|
|||||||
d.output = "plain";
|
d.output = "plain";
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.length && !password.salt) {
|
|
||||||
password.salt = data[0].salt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await Sodium.decryptMulti(password, data);
|
return await Sodium.decryptMulti(password, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,70 +16,48 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import "./logger";
|
|
||||||
import { database } from "@notesnook/common";
|
import { database } from "@notesnook/common";
|
||||||
import { logger as dbLogger } from "@notesnook/core";
|
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import * as Gzip from "react-native-gzip";
|
import * as Gzip from "react-native-gzip";
|
||||||
import EventSource from "../../utils/sse/even-source-ios";
|
import EventSource from "../../utils/sse/even-source-ios";
|
||||||
import AndroidEventSource from "../../utils/sse/event-source";
|
import AndroidEventSource from "../../utils/sse/event-source";
|
||||||
import {
|
|
||||||
SqliteAdapter,
|
|
||||||
SqliteIntrospector,
|
|
||||||
SqliteQueryCompiler
|
|
||||||
} from "@streetwriters/kysely";
|
|
||||||
import filesystem from "../filesystem";
|
import filesystem from "../filesystem";
|
||||||
|
import "./logger";
|
||||||
import Storage from "./storage";
|
import Storage from "./storage";
|
||||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
|
||||||
import { getDatabaseKey } from "./encryption";
|
|
||||||
import SettingsService from "../../services/settings";
|
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
|
|
||||||
export async function setupDatabase(password) {
|
database.host(
|
||||||
const key = await getDatabaseKey(password);
|
__DEV__
|
||||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
? {
|
||||||
|
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.setup(
|
||||||
|
Storage,
|
||||||
database.host({
|
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||||
API_HOST: "https://api.notesnook.com",
|
filesystem,
|
||||||
AUTH_HOST: "https://auth.streetwriters.co",
|
{
|
||||||
SSE_HOST: "https://events.streetwriters.co",
|
compress: Gzip.deflate,
|
||||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
decompress: Gzip.inflate
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const db = database;
|
export const db = database;
|
||||||
let DatabaseLogger = dbLogger.scope(Platform.OS);
|
export const DatabaseLogger = dbLogger;
|
||||||
|
|
||||||
const setLogger = () => {
|
|
||||||
DatabaseLogger = dbLogger.scope(Platform.OS);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { DatabaseLogger, setLogger };
|
|
||||||
|
|||||||
@@ -16,34 +16,14 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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";
|
const LoggerStorage = new MMKVLoader()
|
||||||
import {
|
.withInstanceID("notesnook_logs")
|
||||||
SqliteAdapter,
|
.initialize();
|
||||||
SqliteIntrospector,
|
|
||||||
SqliteQueryCompiler
|
|
||||||
} from "@streetwriters/kysely";
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
import { setLogger } from ".";
|
|
||||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
|
||||||
|
|
||||||
let loggerLoaded = false;
|
initalize(new KV(LoggerStorage));
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { initializeLogger };
|
export { LoggerStorage };
|
||||||
|
|||||||
@@ -1,128 +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 "@streetwriters/kysely";
|
|
||||||
import { CompiledQuery } from "@streetwriters/kysely";
|
|
||||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
|
|
||||||
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(strings.streamingNotSupported());
|
|
||||||
}
|
|
||||||
|
|
||||||
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 RNFetchBlob from "react-native-blob-util";
|
||||||
import {
|
import {
|
||||||
decrypt,
|
decrypt,
|
||||||
decryptMulti,
|
|
||||||
deriveCryptoKey,
|
deriveCryptoKey,
|
||||||
encrypt,
|
encrypt,
|
||||||
encryptMulti,
|
|
||||||
generateCryptoKey,
|
generateCryptoKey,
|
||||||
getCryptoKey,
|
getCryptoKey,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
hash,
|
hash,
|
||||||
removeCryptoKey
|
removeCryptoKey,
|
||||||
|
decryptMulti,
|
||||||
|
encryptMulti
|
||||||
} from "./encryption";
|
} from "./encryption";
|
||||||
import { MMKV } from "./mmkv";
|
import { MMKV } from "./mmkv";
|
||||||
|
|
||||||
@@ -91,8 +91,7 @@ export class KV {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeMulti(keys) {
|
async removeMulti(keys) {
|
||||||
if (!keys) return true;
|
return this.storage.removeItems(...keys);
|
||||||
return this.storage.removeItems(keys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear() {
|
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
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
import RNFetchBlob from "react-native-blob-util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scale down & compress images to screen width
|
* Scale down & compress images to screen width
|
||||||
* for loading in editor.
|
* for loading in editor.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function compressToBase64(path, type) {
|
export async function compressToBase64(path, type) {
|
||||||
const { width: screenWidth, scale } = Dimensions.get("window");
|
const { width, 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 response = await ImageResizer.createResizedImage(
|
const response = await ImageResizer.createResizedImage(
|
||||||
path,
|
path,
|
||||||
1920,
|
width * scale,
|
||||||
99999,
|
9999,
|
||||||
type,
|
type,
|
||||||
80,
|
80,
|
||||||
0,
|
0,
|
||||||
@@ -80,6 +40,11 @@ export async function compressToFile(path, type) {
|
|||||||
onlyScaleDown: true
|
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(path.replace("file://", "")).catch(console.log);
|
||||||
return response.uri;
|
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
|
||||||
|
return base64;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,21 +18,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Sodium from "@ammarahmed/react-native-sodium";
|
import Sodium from "@ammarahmed/react-native-sodium";
|
||||||
import { getFileNameWithExtension } from "@notesnook/core";
|
import {
|
||||||
import { strings } from "@notesnook/intl";
|
getFileNameWithExtension,
|
||||||
|
isImage,
|
||||||
|
isDocument
|
||||||
|
} from "@notesnook/core/dist/utils/filename";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
|
||||||
import * as ScopedStorage from "react-native-scoped-storage";
|
import * as ScopedStorage from "react-native-scoped-storage";
|
||||||
import { subscribe, zip } from "react-native-zip-archive";
|
import { subscribe, zip } from "react-native-zip-archive";
|
||||||
|
import RNFetchBlob from "react-native-blob-util";
|
||||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
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 { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
import { db } from "../database";
|
||||||
import { DatabaseLogger, db } from "../database";
|
|
||||||
import Storage from "../database/storage";
|
import Storage from "../database/storage";
|
||||||
import { createCacheDir, exists } from "./io";
|
|
||||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||||
|
import { createCacheDir, exists } from "./io";
|
||||||
|
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||||
|
|
||||||
export const FileDownloadStatus = {
|
export const FileDownloadStatus = {
|
||||||
Success: 1,
|
Success: 1,
|
||||||
@@ -43,22 +46,27 @@ export const FileDownloadStatus = {
|
|||||||
* Download all user's attachments
|
* Download all user's attachments
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function downloadAllAttachments() {
|
export function downloadAllAttachments() {
|
||||||
const attachments = await db.attachments.all.ids();
|
const attachments = db.attachments.all;
|
||||||
return downloadAttachments(attachments);
|
return downloadAttachments(attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads provided attachments to a .zip file
|
* Downloads provided attachments to a .zip file
|
||||||
* on user's device.
|
* on user's device.
|
||||||
* @param {string[]} attachments
|
* @param attachments
|
||||||
* @param onProgress
|
* @param onProgress
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function downloadAttachments(attachments) {
|
export async function downloadAttachments(
|
||||||
|
attachments,
|
||||||
|
onProgress,
|
||||||
|
canceled,
|
||||||
|
groupId
|
||||||
|
) {
|
||||||
await createCacheDir();
|
await createCacheDir();
|
||||||
if (!attachments || !attachments.length) return;
|
if (!attachments || !attachments.length) return;
|
||||||
const groupId = `download-all-${Date.now()}`;
|
const result = new Map();
|
||||||
|
|
||||||
let outputFolder;
|
let outputFolder;
|
||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
@@ -76,121 +84,95 @@ export async function downloadAttachments(attachments) {
|
|||||||
Platform.OS === "ios"
|
Platform.OS === "ios"
|
||||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||||
: `${cacheDir}/notesnook-attachments.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.unlink(zipSourceFolder);
|
||||||
}
|
|
||||||
|
|
||||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||||
|
|
||||||
const isCancelled = () => {
|
|
||||||
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
|
|
||||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
|
||||||
useAttachmentStore.getState().setDownloading({
|
|
||||||
groupId,
|
|
||||||
current: 0,
|
|
||||||
total: 0,
|
|
||||||
success: false,
|
|
||||||
message: strings.network.dowloadCancelled(),
|
|
||||||
canceled: true
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < attachments.length; i++) {
|
for (let i = 0; i < attachments.length; i++) {
|
||||||
if (isCancelled()) return;
|
let attachment = attachments[i];
|
||||||
let attachment = await db.attachments.attachment(attachments[i]);
|
const hash = attachment.metadata.hash;
|
||||||
const hash = attachment.hash;
|
|
||||||
try {
|
try {
|
||||||
useAttachmentStore.getState().setDownloading({
|
if (canceled.current) {
|
||||||
groupId: groupId,
|
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||||
current: i + 1,
|
return;
|
||||||
total: attachments.length,
|
}
|
||||||
filename: attachment.hash
|
onProgress?.(
|
||||||
});
|
i + 1 / attachments.length,
|
||||||
|
`Downloading attachments (${i + 1}/${
|
||||||
|
attachments.length
|
||||||
|
})... Please wait`
|
||||||
|
);
|
||||||
// Download to cache
|
// Download to cache
|
||||||
let uri = await downloadAttachment(hash, false, {
|
let uri = await downloadAttachment(hash, false, {
|
||||||
silent: true,
|
silent: true,
|
||||||
cache: true,
|
cache: true,
|
||||||
groupId: groupId
|
groupId: groupId
|
||||||
});
|
});
|
||||||
|
if (canceled.current) {
|
||||||
if (isCancelled()) return;
|
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||||
|
return;
|
||||||
if (!uri) throw new Error(strings.failedToDownloadFile());
|
}
|
||||||
|
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.
|
// 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);
|
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||||
} catch (e) {
|
result.set(hash, {
|
||||||
DatabaseLogger.error(e);
|
filename: attachment.metadata.filename,
|
||||||
}
|
status: FileDownloadStatus.Success
|
||||||
}
|
|
||||||
|
|
||||||
useAttachmentStore.getState().setDownloading({
|
|
||||||
groupId: groupId,
|
|
||||||
current: 0,
|
|
||||||
total: 0,
|
|
||||||
success: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isCancelled()) return;
|
|
||||||
|
|
||||||
let sub;
|
|
||||||
try {
|
|
||||||
useAttachmentStore.getState().setDownloading({
|
|
||||||
current: 0,
|
|
||||||
total: 1,
|
|
||||||
message: strings.savingZipFile(),
|
|
||||||
groupId
|
|
||||||
});
|
|
||||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
|
||||||
|
|
||||||
sub = subscribe(({ progress }) => {
|
|
||||||
useAttachmentStore.getState().setDownloading({
|
|
||||||
groupId,
|
|
||||||
current: progress,
|
|
||||||
total: 1,
|
|
||||||
message: strings.savingZipFile((progress * 100).toFixed(1))
|
|
||||||
});
|
});
|
||||||
});
|
} catch (e) {
|
||||||
await zip(zipSourceFolder, zipOutputFile);
|
result.set(hash, {
|
||||||
sub?.remove();
|
filename: attachment.metadata.filename,
|
||||||
|
status: FileDownloadStatus.Fail,
|
||||||
if (Platform.OS === "android") {
|
reason: e
|
||||||
// Move the zip to user selected directory.
|
});
|
||||||
const file = await ScopedStorage.createFile(
|
console.log("Error downloading attachment", e);
|
||||||
outputFolder,
|
|
||||||
`notesnook-attachments-${Date.now()}.zip`,
|
|
||||||
"application/zip"
|
|
||||||
);
|
|
||||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (canceled.current) {
|
||||||
|
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result?.size) {
|
||||||
|
let sub;
|
||||||
|
try {
|
||||||
|
onProgress?.(0, `Zipping... Please wait`);
|
||||||
|
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||||
|
|
||||||
useAttachmentStore.getState().setDownloading({
|
sub = subscribe(({ progress }) => {
|
||||||
current: 0,
|
onProgress(
|
||||||
total: 0,
|
progress,
|
||||||
message: undefined,
|
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||||
success: true,
|
);
|
||||||
groupId
|
});
|
||||||
});
|
await zip(zipSourceFolder, zipOutputFile);
|
||||||
releasePermissions(outputFolder);
|
sub?.remove();
|
||||||
} catch (e) {
|
onProgress(1, `Saving zip file... Please wait`);
|
||||||
useAttachmentStore.getState().setDownloading({
|
if (Platform.OS === "android") {
|
||||||
current: 0,
|
// Move the zip to user selected directory.
|
||||||
total: 0,
|
const file = await ScopedStorage.createFile(
|
||||||
message: undefined,
|
outputFolder,
|
||||||
success: false,
|
`notesnook-attachments-${Date.now()}.zip`,
|
||||||
groupId
|
"application/zip"
|
||||||
});
|
);
|
||||||
releasePermissions(outputFolder);
|
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||||
sub?.remove();
|
}
|
||||||
ToastManager.error(e, "Error zipping attachments");
|
|
||||||
}
|
onProgress?.(1, `Done`);
|
||||||
// Remove source & zip file from cache.
|
releasePermissions(outputFolder);
|
||||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
} catch (e) {
|
||||||
if (Platform.OS === "android") {
|
releasePermissions(outputFolder);
|
||||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
sub?.remove();
|
||||||
|
console.log("Error zipping attachments", e);
|
||||||
|
}
|
||||||
|
// Remove source & zip file from cache.
|
||||||
|
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||||
|
if (Platform.OS === "android") {
|
||||||
|
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function downloadAttachment(
|
export default async function downloadAttachment(
|
||||||
@@ -207,9 +189,9 @@ export default async function downloadAttachment(
|
|||||||
) {
|
) {
|
||||||
await createCacheDir();
|
await createCacheDir();
|
||||||
|
|
||||||
let attachment = await db.attachments.attachment(hash);
|
let attachment = db.attachments.attachment(hash);
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
DatabaseLogger.log("Attachment not found");
|
console.log("attachment not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,46 +206,47 @@ export default async function downloadAttachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db
|
console.log(
|
||||||
.fs()
|
"starting download attachment",
|
||||||
.downloadFile(
|
attachment.metadata.hash,
|
||||||
options.groupId || attachment.hash,
|
options.groupId
|
||||||
attachment.hash,
|
);
|
||||||
attachment.chunkSize
|
await db.fs.downloadFile(
|
||||||
);
|
options.groupId || attachment.metadata.hash,
|
||||||
if (!(await exists(attachment.hash))) {
|
attachment.metadata.hash
|
||||||
DatabaseLogger.log("Attachment does not exist after download.");
|
);
|
||||||
|
|
||||||
|
if (!(await exists(attachment.metadata.hash))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.base64 || options.text) {
|
if (options.base64 || options.text) {
|
||||||
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
|
|
||||||
return await db.attachments.read(
|
return await db.attachments.read(
|
||||||
attachment.hash,
|
attachment.metadata.hash,
|
||||||
options.base64 ? "base64" : "text"
|
options.base64 ? "base64" : "text"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filename = await getFileNameWithExtension(
|
let filename = getFileNameWithExtension(
|
||||||
attachment.filename,
|
attachment.metadata.filename,
|
||||||
attachment.mimeType
|
attachment.metadata.type
|
||||||
);
|
);
|
||||||
|
|
||||||
let key = await db.attachments.decryptKey(attachment.key);
|
let key = await db.attachments.decryptKey(attachment.key);
|
||||||
|
|
||||||
let info = {
|
let info = {
|
||||||
iv: attachment.iv,
|
iv: attachment.iv,
|
||||||
salt: attachment.salt,
|
salt: attachment.salt,
|
||||||
length: attachment.size,
|
length: attachment.length,
|
||||||
alg: attachment.alg,
|
alg: attachment.alg,
|
||||||
hash: attachment.hash,
|
hash: attachment.metadata.hash,
|
||||||
hashType: attachment.hashType,
|
hashType: attachment.metadata.hashType,
|
||||||
mime: attachment.mimeType,
|
mime: attachment.metadata.type,
|
||||||
fileName: options.cache ? undefined : filename,
|
fileName: options.cache ? undefined : filename,
|
||||||
uri: options.cache ? undefined : folder.uri,
|
uri: options.cache ? undefined : folder.uri,
|
||||||
chunkSize: attachment.chunkSize,
|
chunkSize: attachment.chunkSize,
|
||||||
appGroupId: IOS_APPGROUPID
|
appGroupId: IOS_APPGROUPID
|
||||||
};
|
};
|
||||||
|
|
||||||
let fileUri = await Sodium.decryptFile(
|
let fileUri = await Sodium.decryptFile(
|
||||||
key,
|
key,
|
||||||
info,
|
info,
|
||||||
@@ -271,38 +254,48 @@ export default async function downloadAttachment(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
ToastManager.show({
|
ToastEvent.show({
|
||||||
heading: strings.network.downloadSuccess(),
|
heading: "Download successful",
|
||||||
message: strings.network.fileDownloaded(filename),
|
message: filename + " downloaded",
|
||||||
type: "success"
|
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) {
|
if (Platform.OS === "ios" && !options.cache) {
|
||||||
fileUri = folder.uri + `/${filename}`;
|
fileUri = folder.uri + `/${filename}`;
|
||||||
}
|
}
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
presentSheet({
|
presentSheet({
|
||||||
title: strings.network.fileDownloaded(),
|
title: "File downloaded",
|
||||||
paragraph: strings.fileSaved(filename, Platform.OS),
|
paragraph: `${filename} saved to ${
|
||||||
|
Platform.OS === "android"
|
||||||
|
? "selected path"
|
||||||
|
: "File Manager/Notesnook/downloads"
|
||||||
|
}`,
|
||||||
icon: "download",
|
icon: "download",
|
||||||
context: global ? null : attachment.hash,
|
context: global ? null : attachment.metadata.hash,
|
||||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileUri;
|
return fileUri;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log("download attachment error: ", e);
|
||||||
if (attachment.dateUploaded) {
|
if (attachment.dateUploaded) {
|
||||||
RNFetchBlob.fs
|
RNFetchBlob.fs
|
||||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||||
.catch(console.log);
|
|
||||||
RNFetchBlob.fs
|
|
||||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
|
||||||
.catch(console.log);
|
.catch(console.log);
|
||||||
}
|
}
|
||||||
DatabaseLogger.error(e);
|
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||||
useAttachmentStore.getState().remove(attachment.hash);
|
|
||||||
if (options.throwError) {
|
if (options.throwError) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,172 +17,114 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { strings } from "@notesnook/intl";
|
import hosts from "@notesnook/core/dist/utils/constants";
|
||||||
import NetInfo from "@react-native-community/netinfo";
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
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 { 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 { createCacheDir, exists } from "./io";
|
||||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
|
||||||
|
|
||||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
export async function downloadFile(filename, data, cancelToken) {
|
||||||
if (!requestOptions) {
|
if (!data) return false;
|
||||||
DatabaseLogger.log(
|
|
||||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseLogger.log(`Downloading ${filename}`);
|
|
||||||
await createCacheDir();
|
await createCacheDir();
|
||||||
let { url, headers, chunkSize } = requestOptions;
|
|
||||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
let { url, headers } = data;
|
||||||
let originalFilePath = `${cacheDir}/${filename}`;
|
let path = `${cacheDir}/${filename}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await exists(filename)) {
|
if (await exists(filename)) {
|
||||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachment = await db.attachments.attachment(filename);
|
|
||||||
const size = await getUploadedFileSize(filename);
|
|
||||||
|
|
||||||
if (size === -1) {
|
|
||||||
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size === 0) {
|
|
||||||
const error = `${strings.fileLengthError()} (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 = `${strings.fileLengthMismatch(
|
|
||||||
attachment.size,
|
|
||||||
decryptedLength
|
|
||||||
)} (File hash: ${filename})`;
|
|
||||||
await db.attachments.markAsFailed(attachment.id, error);
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = await fetch(url, {
|
let res = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
if (!res.ok)
|
||||||
if (!res.ok) {
|
throw new Error(`${res.status}: Unable to resolve download url`);
|
||||||
DatabaseLogger.log(
|
|
||||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadUrl = await res.text();
|
const downloadUrl = await res.text();
|
||||||
|
|
||||||
if (!downloadUrl) {
|
if (!downloadUrl) throw new Error("Unable to resolve download url");
|
||||||
DatabaseLogger.log(
|
let totalSize = 0;
|
||||||
`Error downloading file: ${filename}, reason: Unable to resolve download url`
|
console.log("Download starting");
|
||||||
);
|
|
||||||
throw new Error(strings.failedToResolvedDownloadUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
|
||||||
let request = RNFetchBlob.config({
|
let request = RNFetchBlob.config({
|
||||||
path: tempFilePath,
|
path: path,
|
||||||
IOSBackgroundTask: true,
|
IOSBackgroundTask: true
|
||||||
overwrite: true
|
|
||||||
})
|
})
|
||||||
.fetch("GET", downloadUrl, null)
|
.fetch("GET", downloadUrl, null)
|
||||||
.progress(async (recieved, total) => {
|
.progress((recieved, total) => {
|
||||||
useAttachmentStore
|
useAttachmentStore
|
||||||
.getState()
|
.getState()
|
||||||
.setProgress(0, total, filename, recieved, "download");
|
.setProgress(0, total, filename, recieved, "download");
|
||||||
|
totalSize = total;
|
||||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
console.log("downloading: ", recieved, total);
|
||||||
});
|
});
|
||||||
|
|
||||||
cancelToken.cancel = () => {
|
cancelToken.cancel = () => {
|
||||||
useAttachmentStore.getState().remove(filename);
|
useAttachmentStore.getState().remove(filename);
|
||||||
request.cancel();
|
request.cancel();
|
||||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
|
||||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = await request;
|
let response = await request;
|
||||||
console.log(response.info().headers);
|
await fileCheck(response, totalSize);
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = response.info().status;
|
let status = response.info().status;
|
||||||
useAttachmentStore.getState().remove(filename);
|
useAttachmentStore.getState().remove(filename);
|
||||||
|
|
||||||
if (exists(originalFilePath)) {
|
|
||||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
|
||||||
|
|
||||||
if (!(await exists(filename))) {
|
|
||||||
throw new Error("File size mismatch");
|
|
||||||
}
|
|
||||||
|
|
||||||
return status >= 200 && status < 300;
|
return status >= 200 && status < 300;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
if (e.message !== "canceled") {
|
||||||
const toast = {
|
ToastEvent.show({
|
||||||
heading: strings.downloadError(),
|
heading: "Error downloading file",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "global"
|
context: "global"
|
||||||
};
|
});
|
||||||
ToastManager.show(toast);
|
ToastEvent.show({
|
||||||
toast.context = "local";
|
heading: "Error downloading file",
|
||||||
ToastManager.show(toast);
|
message: e.message,
|
||||||
|
type: "error",
|
||||||
|
context: "local"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useAttachmentStore.getState().remove(filename);
|
useAttachmentStore.getState().remove(filename);
|
||||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
console.log("Download file error:", e, url, headers);
|
||||||
DatabaseLogger.error(e, {
|
|
||||||
url,
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
return false;
|
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) {
|
export async function checkAttachment(hash) {
|
||||||
const internetState = await NetInfo.fetch();
|
const internetState = await NetInfo.fetch();
|
||||||
const isInternetReachable =
|
const isInternetReachable =
|
||||||
internetState.isConnected && internetState.isInternetReachable;
|
internetState.isConnected && internetState.isInternetReachable;
|
||||||
if (!isInternetReachable) return;
|
if (!isInternetReachable) return { success: true };
|
||||||
const attachment = await db.attachments.attachment(hash);
|
const attachment = db.attachments.attachment(hash);
|
||||||
if (!attachment) return { failed: "Attachment not found." };
|
if (!attachment) return { failed: "Attachment not found." };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const size = await getUploadedFileSize(hash);
|
const size = await getUploadedFileSize(hash);
|
||||||
console.log("File Size", size);
|
if (size <= 0) return { failed: "File length is 0." };
|
||||||
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})`
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { failed: e?.message };
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { checkAttachment, downloadFile } from "./download";
|
import { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
|
||||||
import {
|
import {
|
||||||
clearFileStorage,
|
clearFileStorage,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
exists,
|
exists,
|
||||||
readEncrypted,
|
readEncrypted,
|
||||||
writeEncryptedBase64,
|
writeEncryptedBase64,
|
||||||
hashBase64,
|
hashBase64
|
||||||
clearCache,
|
|
||||||
deleteCacheFileByName,
|
|
||||||
deleteCacheFileByPath,
|
|
||||||
bulkExists,
|
|
||||||
getCacheSize
|
|
||||||
} from "./io";
|
} from "./io";
|
||||||
import { uploadFile } from "./upload";
|
import { uploadFile } from "./upload";
|
||||||
import { cancelable, getUploadedFileSize } from "./utils";
|
import { cancelable } from "./utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
readEncrypted,
|
readEncrypted,
|
||||||
@@ -44,10 +39,5 @@ export default {
|
|||||||
exists,
|
exists,
|
||||||
clearFileStorage,
|
clearFileStorage,
|
||||||
getUploadedFileSize,
|
getUploadedFileSize,
|
||||||
checkAttachment,
|
checkAttachment
|
||||||
clearCache,
|
|
||||||
deleteCacheFileByName,
|
|
||||||
deleteCacheFileByPath,
|
|
||||||
bulkExists,
|
|
||||||
getCacheSize
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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/>.
|
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 { Platform } from "react-native";
|
||||||
|
import Sodium from "@ammarahmed/react-native-sodium";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
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 { IOS_APPGROUPID } from "../../utils/constants";
|
||||||
import { DatabaseLogger, db } from "../database";
|
|
||||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
|
||||||
|
|
||||||
export async function readEncrypted(filename, key, cipherData) {
|
export async function readEncrypted(filename, key, cipherData) {
|
||||||
await migrateFilesFromCache();
|
await migrateFilesFromCache();
|
||||||
DatabaseLogger.log("Read encrypted file...");
|
console.log("Read encrypted file...");
|
||||||
let path = `${cacheDir}/${filename}`;
|
let path = `${cacheDir}/${filename}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -35,6 +35,14 @@ export async function readEncrypted(filename, key, cipherData) {
|
|||||||
return false;
|
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(
|
let output = await Sodium.decryptFile(
|
||||||
key,
|
key,
|
||||||
{
|
{
|
||||||
@@ -42,15 +50,25 @@ export async function readEncrypted(filename, key, cipherData) {
|
|||||||
hash: filename,
|
hash: filename,
|
||||||
appGroupId: IOS_APPGROUPID
|
appGroupId: IOS_APPGROUPID
|
||||||
},
|
},
|
||||||
cipherData.outputType === "base64" ? "base64" : "text"
|
cipherData.outputType === "base64"
|
||||||
|
? isPng || isJpeg
|
||||||
|
? "cache"
|
||||||
|
: "base64"
|
||||||
|
: "text"
|
||||||
);
|
);
|
||||||
|
console.log("file decrypted...");
|
||||||
DatabaseLogger.log("File decrypted...");
|
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||||
|
const dCachePath = `${cacheDir}/${output}`;
|
||||||
|
output = await compressToBase64(
|
||||||
|
`file://${dCachePath}`,
|
||||||
|
isPng ? "PNG" : "JPEG"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||||
DatabaseLogger.error(e);
|
console.log("readEncrypted", e);
|
||||||
return false;
|
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();
|
await createCacheDir();
|
||||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||||
@@ -77,8 +95,6 @@ export async function writeEncryptedBase64(data, key) {
|
|||||||
});
|
});
|
||||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||||
console.log("encrypted file output: ", output);
|
console.log("encrypted file output: ", output);
|
||||||
output.size = output.length;
|
|
||||||
delete output.length;
|
|
||||||
return {
|
return {
|
||||||
...output,
|
...output,
|
||||||
alg: "xcha-stream"
|
alg: "xcha-stream"
|
||||||
@@ -126,13 +142,9 @@ export async function clearFileStorage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createCacheDir() {
|
export async function createCacheDir() {
|
||||||
try {
|
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
console.log("Cache directory created");
|
||||||
DatabaseLogger.log("Cache directory created");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
DatabaseLogger.error(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,35 +175,7 @@ export async function migrateFilesFromCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearCache() {
|
const ABYTES = 17;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exists(filename) {
|
export async function exists(filename) {
|
||||||
let path = `${cacheDir}/${filename}`;
|
let path = `${cacheDir}/${filename}`;
|
||||||
|
|
||||||
@@ -210,19 +194,16 @@ export async function exists(filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exists || existsInAppGroup) {
|
if (exists || existsInAppGroup) {
|
||||||
const attachment = await db.attachments.attachment(filename);
|
const attachment = db.attachments.attachment(filename);
|
||||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
|
||||||
const totalAbytes = totalChunks * ABYTES;
|
const totalAbytes = totalChunks * ABYTES;
|
||||||
const expectedFileSize = attachment.size + totalAbytes;
|
const expectedFileSize = attachment.length + totalAbytes;
|
||||||
|
|
||||||
const stat = await RNFetchBlob.fs.stat(
|
const stat = await RNFetchBlob.fs.stat(
|
||||||
existsInAppGroup ? appGroupPath : path
|
existsInAppGroup ? appGroupPath : path
|
||||||
);
|
);
|
||||||
|
|
||||||
if (stat.size !== expectedFileSize) {
|
if (stat.size !== expectedFileSize) {
|
||||||
DatabaseLogger.log(
|
|
||||||
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
|
|
||||||
);
|
|
||||||
RNFetchBlob.fs
|
RNFetchBlob.fs
|
||||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||||
.catch(console.log);
|
.catch(console.log);
|
||||||
@@ -233,37 +214,3 @@ export async function exists(filename) {
|
|||||||
}
|
}
|
||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkExists(files) {
|
|
||||||
try {
|
|
||||||
await createCacheDir();
|
|
||||||
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;
|
|
||||||
} catch (e) {
|
|
||||||
DatabaseLogger.error(e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCacheSize() {
|
|
||||||
await createCacheDir();
|
|
||||||
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,68 +17,43 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
import RNFetchBlob from "react-native-blob-util";
|
||||||
import { ToastManager } from "../../services/event-manager";
|
|
||||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
|
||||||
import { DatabaseLogger, db } from "../database";
|
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 { createCacheDir } from "./io";
|
||||||
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
|
|
||||||
|
|
||||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
export async function uploadFile(filename, data, cancelToken) {
|
||||||
if (!requestOptions) return false;
|
if (!data) return false;
|
||||||
let { url, headers } = requestOptions;
|
let { url, headers } = data;
|
||||||
await createCacheDir();
|
await createCacheDir();
|
||||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let filePath = `${cacheDir}/${filename}`;
|
let res = await fetch(url, {
|
||||||
let exists = await RNFetchBlob.fs.exists(filePath);
|
|
||||||
// Check for file in appGroupPath if it doesn't exist in cacheDir
|
|
||||||
if (!exists && Platform.OS === "ios") {
|
|
||||||
const iosAppGroup =
|
|
||||||
Platform.OS === "ios"
|
|
||||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
|
||||||
: null;
|
|
||||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
|
||||||
filePath = appGroupPath;
|
|
||||||
exists = await RNFetchBlob.fs.exists(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
`Trying to upload file at path ${filePath} that doest not exist.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
|
|
||||||
|
|
||||||
let remoteFileSize = await getUploadedFileSize(filename);
|
|
||||||
if (remoteFileSize === -1) return false;
|
|
||||||
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
|
|
||||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let uploadUrlResponse = await fetch(url, {
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||||
|
const uploadUrl = await res.text();
|
||||||
|
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
|
||||||
|
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||||
|
|
||||||
const uploadUrl = uploadUrlResponse.ok
|
const iosAppGroup =
|
||||||
? await uploadUrlResponse.text()
|
Platform.OS === "ios"
|
||||||
: await uploadUrlResponse.json();
|
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||||
|
: null;
|
||||||
if (typeof uploadUrl !== "string") {
|
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||||
throw new Error(
|
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
if (!exists && Platform.OS === "ios") {
|
||||||
);
|
uploadFilePath = appGroupPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||||
|
|
||||||
let uploadRequest = RNFetchBlob.config({
|
let request = RNFetchBlob.config({
|
||||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||||
})
|
})
|
||||||
.fetch(
|
.fetch(
|
||||||
@@ -87,7 +62,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
|||||||
{
|
{
|
||||||
"content-type": ""
|
"content-type": ""
|
||||||
},
|
},
|
||||||
RNFetchBlob.wrap(filePath)
|
RNFetchBlob.wrap(uploadFilePath)
|
||||||
)
|
)
|
||||||
.uploadProgress((sent, total) => {
|
.uploadProgress((sent, total) => {
|
||||||
useAttachmentStore
|
useAttachmentStore
|
||||||
@@ -100,33 +75,40 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
|||||||
|
|
||||||
cancelToken.cancel = () => {
|
cancelToken.cancel = () => {
|
||||||
useAttachmentStore.getState().remove(filename);
|
useAttachmentStore.getState().remove(filename);
|
||||||
uploadRequest.cancel();
|
request.cancel();
|
||||||
};
|
};
|
||||||
|
let response = await request;
|
||||||
|
|
||||||
let uploadResponse = await uploadRequest;
|
let status = response.info().status;
|
||||||
let status = uploadResponse.info().status;
|
let text = await response.text();
|
||||||
let uploaded = status >= 200 && status < 300;
|
let result = status >= 200 && status < 300 && text.length === 0;
|
||||||
|
|
||||||
useAttachmentStore.getState().remove(filename);
|
useAttachmentStore.getState().remove(filename);
|
||||||
|
if (result) {
|
||||||
if (!uploaded) {
|
DatabaseLogger.info(
|
||||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
`File upload status: ${filename}, ${status}, ${text}`
|
||||||
|
);
|
||||||
|
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(
|
throw new Error(
|
||||||
`${status}, name: ${fileInfo.filename}, length: ${
|
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
|
||||||
fileInfo.size
|
fileInfo.size
|
||||||
}, info: ${JSON.stringify(uploadResponse.info())}`
|
}, info: ${JSON.stringify(response.info())}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const attachment = await db.attachments.attachment(filename);
|
|
||||||
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
|
return result;
|
||||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
|
||||||
return uploaded;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
useAttachmentStore.getState().remove(filename);
|
useAttachmentStore.getState().remove(filename);
|
||||||
ToastManager.error(e, "File upload failed");
|
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
|
||||||
DatabaseLogger.error(e, "File upload failed", {
|
DatabaseLogger.error(e);
|
||||||
filename
|
|
||||||
});
|
|
||||||
return false;
|
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/>.
|
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 { Platform } from "react-native";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
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";
|
|
||||||
|
|
||||||
export const ABYTES = 17;
|
|
||||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||||
|
|
||||||
export const cacheDir =
|
export const cacheDir =
|
||||||
@@ -37,36 +34,24 @@ export function getRandomId(prefix) {
|
|||||||
.replace("0.", prefix || "");
|
.replace("0.", prefix || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function extractValueFromXmlTag(code, xml) {
|
||||||
*
|
if (!xml.includes(code)) return `Unknown ${code}`;
|
||||||
* @param {string | undefined} data
|
return xml.slice(
|
||||||
* @returns
|
xml.indexOf(`<${code}>`) + code.length + 2,
|
||||||
*/
|
xml.indexOf(`</${code}>`)
|
||||||
export function parseS3Error(data) {
|
);
|
||||||
const xml = typeof data === "string" ? data : null;
|
}
|
||||||
|
|
||||||
const error = {
|
export async function fileCheck(response, totalSize) {
|
||||||
Code: "UNKNOWN",
|
if (totalSize < 1000) {
|
||||||
Message: xml
|
let text = await response.text();
|
||||||
};
|
if (text.startsWith("<?xml")) {
|
||||||
|
let errorJson = {
|
||||||
try {
|
Code: extractValueFromXmlTag("Code", text),
|
||||||
if (!xml) return error;
|
Message: extractValueFromXmlTag("Message", text)
|
||||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
};
|
||||||
|
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return error;
|
|
||||||
} catch (e) {
|
|
||||||
return error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +59,12 @@ export function cancelable(operation) {
|
|||||||
const cancelToken = {
|
const cancelToken = {
|
||||||
cancel: () => {}
|
cancel: () => {}
|
||||||
};
|
};
|
||||||
return (filename, requestOptions) => {
|
return (filename, { url, headers }) => {
|
||||||
return {
|
return {
|
||||||
execute: () => operation(filename, requestOptions, cancelToken),
|
execute: () => operation(filename, { url, headers }, cancelToken),
|
||||||
cancel: async () => {
|
cancel: async () => {
|
||||||
await cancelToken.cancel();
|
await cancelToken.cancel();
|
||||||
|
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -105,36 +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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkUpload(filename, chunkSize, expectedSize) {
|
|
||||||
const size = await getUploadedFileSize(filename);
|
|
||||||
const totalChunks = Math.ceil(size / chunkSize);
|
|
||||||
const decryptedLength = size - totalChunks * ABYTES;
|
|
||||||
const error =
|
|
||||||
size === 0
|
|
||||||
? `File size is 0.`
|
|
||||||
: size === -1
|
|
||||||
? `File verification check failed.`
|
|
||||||
: expectedSize !== decryptedLength
|
|
||||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
|
||||||
: undefined;
|
|
||||||
if (error) throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
import { FlatList, View } from "react-native";
|
||||||
import { useMessageStore } from "../../stores/use-message-store";
|
|
||||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
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 { allowedOnPlatform, renderItem } from "./functions";
|
||||||
import { getContainerBorder } from "../../utils/colors";
|
|
||||||
|
|
||||||
export const Announcement = ({ color }) => {
|
export const Announcement = ({ color }) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const announcements = useMessageStore((state) => state.announcements);
|
const announcements = useMessageStore((state) => state.announcements);
|
||||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||||
|
|
||||||
return !announcement || selectionMode ? null : (
|
return !announcement || selectionMode ? null : (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -47,8 +45,7 @@ export const Announcement = ({ color }) => {
|
|||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: colors.secondary.background,
|
backgroundColor: colors.secondary.background,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12
|
||||||
...getContainerBorder(colors.secondary.background)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Linking, View } from "react-native";
|
import { Linking, View } from "react-native";
|
||||||
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
|
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
|
||||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
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 { SIZE } from "../../utils/size";
|
||||||
import { sleep } from "../../utils/time";
|
import { sleep } from "../../utils/time";
|
||||||
import { PricingPlans } from "../premium/pricing-plans";
|
import { PricingPlans } from "../premium/pricing-plans";
|
||||||
import SheetProvider from "../sheet-provider";
|
import SheetProvider from "../sheet-provider";
|
||||||
|
import { Progress } from "../sheets/progress";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { allowedOnPlatform, getStyle } from "./functions";
|
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 (
|
return (
|
||||||
@@ -94,7 +103,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
|||||||
key={item.title}
|
key={item.title}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
fontSize={SIZE.sm}
|
fontSize={SIZE.sm}
|
||||||
type="plain"
|
type="gray"
|
||||||
onPress={() => onPress(item)}
|
onPress={() => onPress(item)}
|
||||||
width={null}
|
width={null}
|
||||||
height={30}
|
height={30}
|
||||||
@@ -140,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
|||||||
key={item.title}
|
key={item.title}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
fontSize={SIZE.xs}
|
fontSize={SIZE.xs}
|
||||||
type="plain"
|
type="gray"
|
||||||
onPress={() => onPress(item)}
|
onPress={() => onPress(item)}
|
||||||
width={null}
|
width={null}
|
||||||
height={30}
|
height={30}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="plain"
|
type="gray"
|
||||||
icon="close"
|
icon="close"
|
||||||
height={null}
|
height={null}
|
||||||
onPress={() => {
|
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,335 +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";
|
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
|
|
||||||
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: strings.invalid(keyboardType),
|
|
||||||
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"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.unlockNotes()}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Paragraph
|
|
||||||
style={{
|
|
||||||
alignSelf: "center",
|
|
||||||
textAlign: "center",
|
|
||||||
maxWidth: "90%"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.verifyItsYou()}
|
|
||||||
</Paragraph>
|
|
||||||
<Seperator />
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
padding: 12,
|
|
||||||
backgroundColor: colors.primary.background
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user || appLockHasPasswordSecurity ? (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
fwdRef={passwordInputRef}
|
|
||||||
secureTextEntry
|
|
||||||
keyboardType={
|
|
||||||
appLockHasPasswordSecurity ? keyboardType : "default"
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
appLockHasPasswordSecurity
|
|
||||||
? keyboardType === "numeric"
|
|
||||||
? strings.enterApplockPassword()
|
|
||||||
: strings.enterApplockPin()
|
|
||||||
: strings.enterAccountPassword()
|
|
||||||
}
|
|
||||||
onChangeText={(v) => (password.current = v)}
|
|
||||||
onSubmit={() => {
|
|
||||||
onSubmit();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
marginTop: user ? 25 : 25
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user || appLockHasPasswordSecurity ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
title={strings.continue()}
|
|
||||||
type="accent"
|
|
||||||
onPress={onSubmit}
|
|
||||||
width={250}
|
|
||||||
height={45}
|
|
||||||
style={{
|
|
||||||
borderRadius: 150,
|
|
||||||
marginBottom: 10
|
|
||||||
}}
|
|
||||||
fontSize={SIZE.md}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{biometricsAuthEnabled ? (
|
|
||||||
<Button
|
|
||||||
title={strings.unlockWithBiometrics()}
|
|
||||||
width={250}
|
|
||||||
onPress={onUnlockAppRequested}
|
|
||||||
icon={"fingerprint"}
|
|
||||||
type="transparent"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</KeyboardAwareScrollView>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLockedOverlay;
|
|
||||||
@@ -17,88 +17,67 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 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 { View } from "react-native";
|
||||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import filesystem from "../../common/filesystem";
|
import filesystem from "../../common/filesystem";
|
||||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||||
import picker from "../../screens/editor/tiptap/picker";
|
import picker from "../../screens/editor/tiptap/picker";
|
||||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
|
||||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
|
||||||
import {
|
import {
|
||||||
ToastManager,
|
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
presentSheet
|
presentSheet,
|
||||||
|
ToastEvent
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
import PremiumService from "../../services/premium";
|
import PremiumService from "../../services/premium";
|
||||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||||
import {
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
eCloseAttachmentDialog,
|
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
|
||||||
eCloseSheet,
|
|
||||||
eDBItemUpdate,
|
|
||||||
eOnLoadNote
|
|
||||||
} from "../../utils/events";
|
|
||||||
import { SIZE } from "../../utils/size";
|
import { SIZE } from "../../utils/size";
|
||||||
import { sleep } from "../../utils/time";
|
import { sleep } from "../../utils/time";
|
||||||
import { Dialog } from "../dialog";
|
import { Dialog } from "../dialog";
|
||||||
import { presentDialog } from "../dialog/functions";
|
import { presentDialog } from "../dialog/functions";
|
||||||
import { openNote } from "../list-items/note/wrapper";
|
import { openNote } from "../list-items/note/wrapper";
|
||||||
import { DateMeta } from "../properties/date-meta";
|
import { DateMeta } from "../properties/date-meta";
|
||||||
import SheetProvider from "../sheet-provider";
|
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Notice } from "../ui/notice";
|
import { Notice } from "../ui/notice";
|
||||||
import { Pressable } from "../ui/pressable";
|
import { PressableButton } from "../ui/pressable";
|
||||||
import Heading from "../ui/typography/heading";
|
import Heading from "../ui/typography/heading";
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
import { strings } from "@notesnook/intl";
|
import { formatBytes } from "@notesnook/common";
|
||||||
|
|
||||||
const Actions = ({
|
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||||
attachment,
|
|
||||||
close,
|
|
||||||
setAttachments,
|
|
||||||
fwdRef
|
|
||||||
}: {
|
|
||||||
attachment: Attachment;
|
|
||||||
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
|
|
||||||
close?: () => void;
|
|
||||||
fwdRef: RefObject<ActionSheetRef>;
|
|
||||||
}) => {
|
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const contextId = attachment.hash;
|
const contextId = attachment.metadata.hash;
|
||||||
const [filename, setFilename] = useState(attachment.filename);
|
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||||
const [currentProgress] = useAttachmentProgress(attachment);
|
const [currentProgress] = useAttachmentProgress(attachment);
|
||||||
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
|
const [failed, setFailed] = useState(attachment.failed);
|
||||||
const [notes, setNotes] = useState<Note[]>([]);
|
const [notes, setNotes] = useState([]);
|
||||||
const [loading, setLoading] = useState<{
|
const [loading, setLoading] = useState({
|
||||||
name?: string;
|
name: null
|
||||||
}>({});
|
});
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
{
|
{
|
||||||
name: strings.network.download(),
|
name: "Download",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
if (currentProgress) {
|
if (currentProgress) {
|
||||||
await db.fs().cancel(attachment.hash);
|
await db.fs.cancel(attachment.metadata.hash);
|
||||||
useAttachmentStore.getState().remove(attachment.hash);
|
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||||
}
|
}
|
||||||
downloadAttachment(attachment.hash, false);
|
downloadAttachment(attachment.metadata.hash, false);
|
||||||
fwdRef.current?.hide();
|
eSendEvent(eCloseSheet, contextId);
|
||||||
},
|
},
|
||||||
icon: "download"
|
icon: "download"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: strings.network.reupload(),
|
name: "Reupload",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
if (!PremiumService.get()) {
|
if (!PremiumService.get()) {
|
||||||
ToastManager.show({
|
ToastEvent.show({
|
||||||
heading: strings.upgradeToPro(),
|
heading: "Upgrade to pro",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
@@ -106,36 +85,33 @@ const Actions = ({
|
|||||||
}
|
}
|
||||||
await picker.pick({
|
await picker.pick({
|
||||||
reupload: true,
|
reupload: true,
|
||||||
hash: attachment.hash,
|
hash: attachment.metadata.hash,
|
||||||
context: contextId,
|
context: contextId,
|
||||||
type: attachment.mimeType.startsWith("image") ? "image" : "file"
|
type: attachment.metadata.type
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: "upload"
|
icon: "upload"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: strings.fileCheck(),
|
name: "Run file check",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
setLoading({
|
setLoading({
|
||||||
name: strings.fileCheck()
|
name: "Run file check"
|
||||||
});
|
});
|
||||||
const result = await filesystem.checkAttachment(attachment.hash);
|
let res = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||||
if (!result) return;
|
if (res.failed) {
|
||||||
|
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||||
if (result.failed) {
|
setFailed(res.failed);
|
||||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
ToastEvent.show({
|
||||||
setFailed(result.failed);
|
heading: "File check failed with error: " + res.failed,
|
||||||
ToastManager.show({
|
|
||||||
heading: strings.fileCheckFailed(result.failed),
|
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFailed(undefined);
|
setFailed(null);
|
||||||
db.attachments.markAsFailed(attachment.id);
|
db.attachments.markAsFailed(attachment.id, null);
|
||||||
eSendEvent(eDBItemUpdate, attachment.id);
|
ToastEvent.show({
|
||||||
ToastManager.show({
|
heading: "File check passed",
|
||||||
heading: strings.fileCheckPassed(),
|
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
@@ -143,71 +119,66 @@ const Actions = ({
|
|||||||
|
|
||||||
setAttachments();
|
setAttachments();
|
||||||
setLoading({
|
setLoading({
|
||||||
name: undefined
|
name: null
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: "file-check"
|
icon: "file-check"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: strings.rename(),
|
name: "Rename",
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
presentDialog({
|
presentDialog({
|
||||||
|
context: contextId,
|
||||||
input: true,
|
input: true,
|
||||||
title: strings.renameFile(),
|
title: "Rename file",
|
||||||
defaultValue: attachment.filename,
|
paragraph: "Enter a new name for the file",
|
||||||
|
defaultValue: attachment.metadata.filename,
|
||||||
positivePress: async (value) => {
|
positivePress: async (value) => {
|
||||||
if (value && value.length > 0) {
|
if (value && value.length > 0) {
|
||||||
await db.attachments.add({
|
await db.attachments.add({
|
||||||
hash: attachment.hash,
|
hash: attachment.metadata.hash,
|
||||||
filename: value
|
filename: value
|
||||||
});
|
});
|
||||||
setFilename(value);
|
setFilename(value);
|
||||||
setAttachments();
|
setAttachments();
|
||||||
eSendEvent(eDBItemUpdate, attachment.id);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
positiveText: strings.rename()
|
positiveText: "Rename"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: "form-textbox"
|
icon: "form-textbox"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: strings.delete(),
|
name: "Delete",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
const relations = await db.relations.to(attachment, "note").get();
|
await db.attachments.remove(attachment.metadata.hash, false);
|
||||||
await db.attachments.remove(attachment.hash, false);
|
|
||||||
setAttachments();
|
setAttachments();
|
||||||
eSendEvent(eDBItemUpdate, attachment.id);
|
close();
|
||||||
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?.();
|
|
||||||
},
|
},
|
||||||
icon: "delete-outline"
|
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(() => {
|
useEffect(() => {
|
||||||
db.relations
|
setNotes(getNotes());
|
||||||
.to(attachment, "note")
|
}, [attachment, getNotes]);
|
||||||
.selector.items()
|
|
||||||
.then((items) => {
|
|
||||||
setNotes(items);
|
|
||||||
});
|
|
||||||
}, [attachment]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -218,14 +189,12 @@ const Actions = ({
|
|||||||
style={{
|
style={{
|
||||||
maxHeight: "100%"
|
maxHeight: "100%"
|
||||||
}}
|
}}
|
||||||
keyboardShouldPersistTaps="never"
|
|
||||||
>
|
>
|
||||||
<Dialog context={contextId} />
|
<Dialog context={contextId} />
|
||||||
<SheetProvider context={contextId} />
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.primary.border,
|
borderBottomColor: colors.secondary.background,
|
||||||
marginBottom: notes && notes.length > 0 ? 0 : 12
|
marginBottom: notes && notes.length > 0 ? 0 : 12
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -242,36 +211,53 @@ const Actions = ({
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12
|
||||||
marginTop: 6,
|
|
||||||
gap: 10
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
<Paragraph
|
||||||
{attachment.mimeType}
|
size={SIZE.xs}
|
||||||
|
style={{
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
color={colors.secondary.paragraph}
|
||||||
|
>
|
||||||
|
{attachment.metadata.type}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
<Paragraph
|
||||||
{formatBytes(attachment.size)}
|
style={{
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
size={SIZE.xs}
|
||||||
|
color={colors.secondary.paragraph}
|
||||||
|
>
|
||||||
|
{formatBytes(attachment.length)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
{notes.length ? (
|
{attachment.noteIds ? (
|
||||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
<Paragraph
|
||||||
{strings.notes(notes.length)}
|
style={{
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
size={SIZE.xs}
|
||||||
|
color={colors.secondary.paragraph}
|
||||||
|
>
|
||||||
|
{attachment.noteIds.length} note
|
||||||
|
{attachment.noteIds.length > 1 ? "s" : ""}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
) : null}
|
) : null}
|
||||||
<Paragraph
|
<Paragraph
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
Clipboard.setString(attachment.hash);
|
Clipboard.setString(attachment.metadata.hash);
|
||||||
ToastManager.show({
|
ToastEvent.show({
|
||||||
type: "success",
|
type: "success",
|
||||||
heading: strings.hashCopied(),
|
heading: "Attachment hash copied",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
size={SIZE.xs}
|
size={SIZE.xs}
|
||||||
color={colors.secondary.paragraph}
|
color={colors.secondary.paragraph}
|
||||||
>
|
>
|
||||||
{attachment.hash}
|
{attachment.metadata.hash}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -282,7 +268,7 @@ const Actions = ({
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.primary.border,
|
borderBottomColor: colors.secondary.background,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
paddingVertical: 12
|
paddingVertical: 12
|
||||||
}}
|
}}
|
||||||
@@ -294,19 +280,29 @@ const Actions = ({
|
|||||||
}}
|
}}
|
||||||
size={SIZE.sm}
|
size={SIZE.sm}
|
||||||
>
|
>
|
||||||
{strings.listOf()} {strings.dataTypesPlural.note()}:
|
List of notes:
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{notes.map((item) => (
|
{notes.map((item) => (
|
||||||
<Pressable
|
<PressableButton
|
||||||
onPress={async () => {
|
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);
|
eSendEvent(eCloseSheet, contextId);
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
eSendEvent(eCloseAttachmentDialog);
|
eSendEvent(eCloseAttachmentDialog);
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
openNote(item, (item as any).type === "trash");
|
openNote(item, item.type === "trash");
|
||||||
}}
|
}}
|
||||||
style={{
|
customStyle={{
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
|
|
||||||
@@ -315,7 +311,7 @@ const Actions = ({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
>
|
>
|
||||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||||
</Pressable>
|
</PressableButton>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
</View>
|
</View>
|
||||||
@@ -325,16 +321,17 @@ const Actions = ({
|
|||||||
<Button
|
<Button
|
||||||
key={item.name}
|
key={item.name}
|
||||||
buttonType={{
|
buttonType={{
|
||||||
text:
|
text: item.on
|
||||||
item.name === "Delete"
|
? colors.primary.accent
|
||||||
? colors.error.paragraph
|
: item.name === "Delete" || item.name === "PermDelete"
|
||||||
: colors.primary.paragraph
|
? colors.error.paragraph
|
||||||
|
: colors.primary.paragraph
|
||||||
}}
|
}}
|
||||||
onPress={item.onPress}
|
onPress={item.onPress}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
loading={loading?.name === item.name}
|
loading={loading?.name === item.name}
|
||||||
type="plain"
|
type={item.on ? "shade" : "gray"}
|
||||||
fontSize={SIZE.sm}
|
fontSize={SIZE.sm}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
@@ -353,7 +350,7 @@ const Actions = ({
|
|||||||
{failed ? (
|
{failed ? (
|
||||||
<Notice
|
<Notice
|
||||||
type="alert"
|
type="alert"
|
||||||
text={strings.fileCheckFailed(failed)}
|
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -362,11 +359,7 @@ const Actions = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Actions.present = (
|
Actions.present = (attachment, set, context) => {
|
||||||
attachment: Attachment,
|
|
||||||
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
|
|
||||||
context?: string
|
|
||||||
) => {
|
|
||||||
presentSheet({
|
presentSheet({
|
||||||
context: context,
|
context: context,
|
||||||
component: (ref, close) => (
|
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 { 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";
|
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
|
|
||||||
function getFileExtension(filename: string) {
|
|
||||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
|
||||||
return ext == null ? "" : ext[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AttachmentItem = ({
|
|
||||||
id,
|
|
||||||
attachments,
|
|
||||||
encryption,
|
|
||||||
setAttachments,
|
|
||||||
pressable = true,
|
|
||||||
hideWhenNotDownloading,
|
|
||||||
context,
|
|
||||||
errorOnly
|
|
||||||
}: {
|
|
||||||
id: string | number;
|
|
||||||
attachments?: VirtualizedGrouping<Attachment>;
|
|
||||||
encryption?: boolean;
|
|
||||||
setAttachments: (attachments: any) => void;
|
|
||||||
pressable?: boolean;
|
|
||||||
hideWhenNotDownloading?: boolean;
|
|
||||||
context?: string;
|
|
||||||
errorOnly?: boolean;
|
|
||||||
}) => {
|
|
||||||
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 errorOnly && attachment && !attachment?.failed ? null : (
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.9}
|
|
||||||
onPress={onPress}
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
marginVertical: 5,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
padding: 12,
|
|
||||||
paddingVertical: 6,
|
|
||||||
minHeight: 45
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!attachment ? null : (
|
|
||||||
<>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexShrink: 1,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
marginLeft: -5,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.secondary.border,
|
|
||||||
paddingHorizontal: 2,
|
|
||||||
width: 30,
|
|
||||||
height: 30,
|
|
||||||
borderRadius: 5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paragraph
|
|
||||||
adjustsFontSizeToFit
|
|
||||||
size={8}
|
|
||||||
color={colors.secondary.paragraph}
|
|
||||||
style={{
|
|
||||||
maxWidth: 30
|
|
||||||
}}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{getFileExtension(attachment.filename).toUpperCase() ||
|
|
||||||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
|
|
||||||
</Paragraph>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexShrink: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paragraph
|
|
||||||
size={SIZE.sm}
|
|
||||||
style={{
|
|
||||||
flexWrap: "wrap"
|
|
||||||
}}
|
|
||||||
numberOfLines={1}
|
|
||||||
lineBreakMode="middle"
|
|
||||||
color={colors.primary.paragraph}
|
|
||||||
>
|
|
||||||
{attachment.filename}
|
|
||||||
</Paragraph>
|
|
||||||
|
|
||||||
{!hideWhenNotDownloading ? (
|
|
||||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
|
|
||||||
{strings.fileSize()}: {formatBytes(attachment.size)}
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
275
apps/mobile/app/components/attachments/download-attachments.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { Platform, View } from "react-native";
|
||||||
|
import { db } from "../../common/database";
|
||||||
|
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||||
|
import { presentSheet } from "../../services/event-manager";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import Heading from "../ui/typography/heading";
|
||||||
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
|
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||||
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
|
import { FlatList } from "react-native-actions-sheet";
|
||||||
|
import { AttachmentItem } from "./attachment-item";
|
||||||
|
|
||||||
|
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||||
|
const { colors } = useThemeColors();
|
||||||
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
const [progress, setProgress] = useState({
|
||||||
|
value: 0,
|
||||||
|
statusText: "Download started... Please wait"
|
||||||
|
});
|
||||||
|
const [result, setResult] = useState(new Map());
|
||||||
|
const canceled = useRef(false);
|
||||||
|
const groupId = useRef();
|
||||||
|
|
||||||
|
const onDownload = async () => {
|
||||||
|
update({
|
||||||
|
disableClosing: true
|
||||||
|
});
|
||||||
|
setDownloading(true);
|
||||||
|
canceled.current = false;
|
||||||
|
groupId.current = Date.now().toString();
|
||||||
|
const result = await downloadAttachments(
|
||||||
|
attachments,
|
||||||
|
(progress, statusText) => setProgress({ value: progress, statusText }),
|
||||||
|
canceled,
|
||||||
|
groupId.current
|
||||||
|
);
|
||||||
|
if (canceled.current) return;
|
||||||
|
setResult(result || new Map());
|
||||||
|
setDownloading(false);
|
||||||
|
update({
|
||||||
|
disableClosing: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = async () => {
|
||||||
|
update({
|
||||||
|
disableClosing: false
|
||||||
|
});
|
||||||
|
canceled.current = true;
|
||||||
|
console.log(groupId.current, "canceling groupId downloads");
|
||||||
|
await db.fs.cancel(groupId.current);
|
||||||
|
setDownloading(false);
|
||||||
|
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 (let [key, value] of result.entries()) {
|
||||||
|
if (value.status === 0) results.push(db.attachments.attachment(key));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getResultText() {
|
||||||
|
const downloadedAttachmentsCount =
|
||||||
|
attachments.length - failedResults().length;
|
||||||
|
if (downloadedAttachmentsCount === 0)
|
||||||
|
return "Failed to download all attachments";
|
||||||
|
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||||
|
attachments.length
|
||||||
|
} attachments as a zip file at ${
|
||||||
|
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading>
|
||||||
|
{downloading
|
||||||
|
? "Downloading attachments"
|
||||||
|
: result?.size
|
||||||
|
? "Downloaded attachments"
|
||||||
|
: "Download attachments"}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
{downloading ? (
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
textAlign: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{progress.statusText}
|
||||||
|
</Paragraph>
|
||||||
|
) : result?.size ? (
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
textAlign: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getResultText()}
|
||||||
|
</Paragraph>
|
||||||
|
) : (
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
textAlign: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Are you sure you want to download all attachments
|
||||||
|
{isNote ? " of this note?" : "?"}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{downloading ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 200,
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProgressBarComponent
|
||||||
|
height={5}
|
||||||
|
width={null}
|
||||||
|
animated={true}
|
||||||
|
useNativeDriver
|
||||||
|
progress={progress.value ? progress.value / attachments.length : 0}
|
||||||
|
unfilledColor={colors.secondary.background}
|
||||||
|
color={colors.primary.accent}
|
||||||
|
borderWidth={0}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
style={{
|
||||||
|
maxHeight: 300,
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 60,
|
||||||
|
backgroundColor: colors.secondary.background,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginVertical: 12
|
||||||
|
}}
|
||||||
|
data={downloading ? attachments : []}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
height: 60
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paragraph color={colors.secondary.paragraph}>
|
||||||
|
No downloads in progress.
|
||||||
|
</Paragraph>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
renderItem={({ item }) => {
|
||||||
|
return (
|
||||||
|
<AttachmentItem
|
||||||
|
attachment={item}
|
||||||
|
setAttachments={() => {}}
|
||||||
|
pressable={false}
|
||||||
|
hideWhenNotDownloading={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{result?.size ? (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
width: 250,
|
||||||
|
borderRadius: 100,
|
||||||
|
marginTop: 20
|
||||||
|
}}
|
||||||
|
onPress={close}
|
||||||
|
type="accent"
|
||||||
|
title="Done"
|
||||||
|
/>
|
||||||
|
) : !downloading ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
width: "100%",
|
||||||
|
marginTop: 20
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 100,
|
||||||
|
marginRight: 5
|
||||||
|
}}
|
||||||
|
onPress={close}
|
||||||
|
type="grayBg"
|
||||||
|
title="No"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 100,
|
||||||
|
marginLeft: 5
|
||||||
|
}}
|
||||||
|
onPress={onDownload}
|
||||||
|
type="accent"
|
||||||
|
title="Yes"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
width: 250,
|
||||||
|
borderRadius: 100,
|
||||||
|
marginTop: 20
|
||||||
|
}}
|
||||||
|
onPress={cancel}
|
||||||
|
type="error"
|
||||||
|
title="Cancel"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DownloadAttachments.present = (context, attachments, isNote) => {
|
||||||
|
presentSheet({
|
||||||
|
context: context,
|
||||||
|
component: (ref, close, update) => (
|
||||||
|
<DownloadAttachments
|
||||||
|
close={close}
|
||||||
|
attachments={attachments}
|
||||||
|
isNote={isNote}
|
||||||
|
update={update}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadAttachments;
|
||||||
334
apps/mobile/app/components/attachments/index.js
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||||
|
|
||||||
|
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 { useThemeColors } from "@notesnook/theme";
|
||||||
|
import { SIZE } from "../../utils/size";
|
||||||
|
import SheetProvider from "../sheet-provider";
|
||||||
|
import { IconButton } from "../ui/icon-button";
|
||||||
|
import Input from "../ui/input";
|
||||||
|
import Seperator from "../ui/seperator";
|
||||||
|
import Heading from "../ui/typography/heading";
|
||||||
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
|
import { AttachmentItem } from "./attachment-item";
|
||||||
|
import DownloadAttachments from "./download-attachments";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const AttachmentDialog = ({ note }) => {
|
||||||
|
const { colors } = useThemeColors();
|
||||||
|
const { height } = useSettingStore((state) => state.dimensions);
|
||||||
|
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 onChangeText = (text) => {
|
||||||
|
const attachments = note?.id
|
||||||
|
? db.attachments.ofNote(note?.id, "all")
|
||||||
|
: [...(db.attachments.all || [])];
|
||||||
|
|
||||||
|
attachmentSearchValue.current = text;
|
||||||
|
if (
|
||||||
|
!attachmentSearchValue.current ||
|
||||||
|
attachmentSearchValue.current === ""
|
||||||
|
) {
|
||||||
|
setAttachments(filterAttachments(currentFilter));
|
||||||
|
}
|
||||||
|
clearTimeout(searchTimer.current);
|
||||||
|
searchTimer.current = setTimeout(() => {
|
||||||
|
let results = db.lookup.attachments(
|
||||||
|
attachments,
|
||||||
|
attachmentSearchValue.current
|
||||||
|
);
|
||||||
|
if (results.length === 0) return;
|
||||||
|
|
||||||
|
setAttachments(filterAttachments(currentFilter, results));
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItem = ({ item }) => (
|
||||||
|
<AttachmentItem
|
||||||
|
setAttachments={() => {
|
||||||
|
setAttachments(filterAttachments(currentFilter));
|
||||||
|
}}
|
||||||
|
attachment={item}
|
||||||
|
context="attachments-list"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const onCheck = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const checkedAttachments = [];
|
||||||
|
for (let attachment of attachments) {
|
||||||
|
let result = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||||
|
if (result.failed) {
|
||||||
|
await db.attachments.markAsFailed(
|
||||||
|
attachment.metadata.hash,
|
||||||
|
result.failed
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await db.attachments.markAsFailed(attachment.id, null);
|
||||||
|
}
|
||||||
|
checkedAttachments.push(
|
||||||
|
db.attachments.attachment(attachment.metadata.hash)
|
||||||
|
);
|
||||||
|
setAttachments([...checkedAttachments]);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachmentTypes = [
|
||||||
|
{
|
||||||
|
title: "All",
|
||||||
|
filterBy: "all"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Images",
|
||||||
|
filterBy: "images"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Documents",
|
||||||
|
filterBy: "documents"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Video",
|
||||||
|
filterBy: "video"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Audio",
|
||||||
|
filterBy: "audio"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const filterAttachments = (type, _attachments) => {
|
||||||
|
const attachments = _attachments
|
||||||
|
? _attachments
|
||||||
|
: note
|
||||||
|
? db.attachments.ofNote(note?.id, "all")
|
||||||
|
: [...(db.attachments.all || [])];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "all":
|
||||||
|
return attachments;
|
||||||
|
case "images":
|
||||||
|
return attachments.filter((attachment) =>
|
||||||
|
isImage(attachment.metadata.type)
|
||||||
|
);
|
||||||
|
case "video":
|
||||||
|
return attachments.filter((attachment) =>
|
||||||
|
isVideo(attachment.metadata.type)
|
||||||
|
);
|
||||||
|
case "audio":
|
||||||
|
return attachments.filter((attachment) =>
|
||||||
|
isAudio(attachment.metadata.type)
|
||||||
|
);
|
||||||
|
case "documents":
|
||||||
|
return attachments.filter((attachment) =>
|
||||||
|
isDocument(attachment.metadata.type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
alignSelf: "center",
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
height: height * 0.85
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SheetProvider context="attachments-list" />
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading>Attachments</Heading>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
size={SIZE.lg}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
name="check-all"
|
||||||
|
customStyle={{
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
color={colors.primary.paragraph}
|
||||||
|
size={SIZE.lg}
|
||||||
|
onPress={onCheck}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="download"
|
||||||
|
customStyle={{
|
||||||
|
height: 40,
|
||||||
|
width: 40
|
||||||
|
}}
|
||||||
|
color={colors.primary.paragraph}
|
||||||
|
onPress={() => {
|
||||||
|
DownloadAttachments.present(
|
||||||
|
"attachments-list",
|
||||||
|
attachments,
|
||||||
|
!!note
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
size={SIZE.lg}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Seperator />
|
||||||
|
<Input
|
||||||
|
placeholder="Filter attachments by filename, type or hash"
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
onSubmit={() => {
|
||||||
|
onChangeText(attachmentSearchValue.current);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<ScrollView
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 50,
|
||||||
|
flexDirection: "row",
|
||||||
|
backgroundColor: colors.primary.background
|
||||||
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
minWidth: "100%",
|
||||||
|
height: 50
|
||||||
|
}}
|
||||||
|
horizontal
|
||||||
|
>
|
||||||
|
{attachmentTypes.map((item) => (
|
||||||
|
<Button
|
||||||
|
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
||||||
|
key={item.title}
|
||||||
|
title={
|
||||||
|
item.title +
|
||||||
|
` (${filterAttachments(item.filterBy)?.length || 0})`
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderRadius: 0,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
flexGrow: 1,
|
||||||
|
borderBottomColor:
|
||||||
|
currentFilter !== item.filterBy
|
||||||
|
? "transparent"
|
||||||
|
: colors.primary.accent
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
setCurrentFilter(item.filterBy);
|
||||||
|
setAttachments(filterAttachments(item.filterBy));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
<FlashList
|
||||||
|
keyboardDismissMode="none"
|
||||||
|
keyboardShouldPersistTaps="always"
|
||||||
|
ListEmptyComponent={
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
height: 150,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="attachment" size={60} color={colors.secondary.icon} />
|
||||||
|
<Paragraph>
|
||||||
|
{note ? "No attachments on this note" : "No attachments"}
|
||||||
|
</Paragraph>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
ListFooterComponent={
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
height: 350
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
estimatedItemSize={50}
|
||||||
|
data={attachments}
|
||||||
|
renderItem={renderItem}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Paragraph
|
||||||
|
color={colors.secondary.paragraph}
|
||||||
|
size={SIZE.xs}
|
||||||
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="shield-key-outline"
|
||||||
|
size={SIZE.xs}
|
||||||
|
color={colors.primary.icon}
|
||||||
|
/>
|
||||||
|
{" "}All attachments are end-to-end encrypted.
|
||||||
|
</Paragraph>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AttachmentDialog.present = (note) => {
|
||||||
|
presentSheet({
|
||||||
|
component: () => <AttachmentDialog note={note} />
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,618 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of the Notesnook project (https://notesnook.com/)
|
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
Attachment,
|
|
||||||
FilteredSelector,
|
|
||||||
Note,
|
|
||||||
SortOptions,
|
|
||||||
VirtualizedGrouping
|
|
||||||
} from "@notesnook/core";
|
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { ActivityIndicator, View } from "react-native";
|
|
||||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
|
||||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
|
||||||
import create, { State } from "zustand";
|
|
||||||
import { db } from "../../common/database";
|
|
||||||
import filesystem from "../../common/filesystem";
|
|
||||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
|
||||||
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
|
|
||||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
|
||||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
|
||||||
import { SIZE } from "../../utils/size";
|
|
||||||
import { Dialog } from "../dialog";
|
|
||||||
import { presentDialog } from "../dialog/functions";
|
|
||||||
import { Header } from "../header";
|
|
||||||
import SheetProvider from "../sheet-provider";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import { IconButton } from "../ui/icon-button";
|
|
||||||
import Input from "../ui/input";
|
|
||||||
import Seperator from "../ui/seperator";
|
|
||||||
import Heading from "../ui/typography/heading";
|
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
|
||||||
import { AttachmentItem } from "./attachment-item";
|
|
||||||
|
|
||||||
const DEFAULT_SORTING: SortOptions = {
|
|
||||||
sortBy: "dateEdited",
|
|
||||||
sortDirection: "desc"
|
|
||||||
};
|
|
||||||
|
|
||||||
type RecheckerProgress = {
|
|
||||||
failed: number;
|
|
||||||
passed: number;
|
|
||||||
isWorking: boolean;
|
|
||||||
shown: boolean;
|
|
||||||
filter: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RecheckerState extends State {
|
|
||||||
progress: {
|
|
||||||
[key: string]: RecheckerProgress;
|
|
||||||
};
|
|
||||||
setProgress: (key: string, progress: RecheckerProgress) => void;
|
|
||||||
currentFilter: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useRechecker = create<RecheckerState>((set) => ({
|
|
||||||
progress: {
|
|
||||||
all: {
|
|
||||||
failed: 0,
|
|
||||||
passed: 0,
|
|
||||||
isWorking: false,
|
|
||||||
shown: false,
|
|
||||||
filter: "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentFilter: "all",
|
|
||||||
setProgress: (key: string, progress: RecheckerProgress) => {
|
|
||||||
set((state) => ({
|
|
||||||
progress: {
|
|
||||||
...state.progress,
|
|
||||||
[key]: progress
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const attachmentTypes = [
|
|
||||||
{
|
|
||||||
title: strings.mediaTypes.all(),
|
|
||||||
filterBy: "all"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.mediaTypes.image(),
|
|
||||||
filterBy: "images"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.mediaTypes.audio(),
|
|
||||||
filterBy: "audio"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.mediaTypes.video(),
|
|
||||||
filterBy: "video"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.mediaTypes.document(),
|
|
||||||
filterBy: "documents"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.mediaTypes.orphaned(),
|
|
||||||
filterBy: "orphaned"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.mediaTypes.errors(),
|
|
||||||
filterBy: "errors"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AttachmentDialog = ({
|
|
||||||
note,
|
|
||||||
isSheet
|
|
||||||
}: {
|
|
||||||
note?: Note;
|
|
||||||
isSheet: boolean;
|
|
||||||
}) => {
|
|
||||||
const { colors } = useThemeColors();
|
|
||||||
const [attachments, setAttachments] =
|
|
||||||
useState<VirtualizedGrouping<Attachment>>();
|
|
||||||
const attachmentSearchValue = useRef<string>();
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const searchTimer = useRef<NodeJS.Timeout>();
|
|
||||||
const [currentFilter, setCurrentFilter] = useState("all");
|
|
||||||
const rechecker = useRechecker((state) =>
|
|
||||||
note ? state.progress[note.id] || {} : state.progress.all
|
|
||||||
);
|
|
||||||
const currentFilterRef = useRef(currentFilter);
|
|
||||||
currentFilterRef.current = currentFilter;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
useRechecker.setState({
|
|
||||||
currentFilter: currentFilter
|
|
||||||
});
|
|
||||||
}, [currentFilter]);
|
|
||||||
|
|
||||||
const refresh = React.useCallback(() => {
|
|
||||||
if (note) {
|
|
||||||
db.attachments
|
|
||||||
.ofNote(note.id, "all")
|
|
||||||
.sorted({
|
|
||||||
...DEFAULT_SORTING,
|
|
||||||
sortBy: "dateModified"
|
|
||||||
})
|
|
||||||
.then((attachments) => {
|
|
||||||
setLoading(false);
|
|
||||||
setAttachments(attachments);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
db.attachments.all
|
|
||||||
.sorted({
|
|
||||||
...DEFAULT_SORTING,
|
|
||||||
sortBy: "dateModified"
|
|
||||||
})
|
|
||||||
.then((attachments) => {
|
|
||||||
setAttachments(attachments);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [note]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refresh();
|
|
||||||
}, [note, refresh]);
|
|
||||||
|
|
||||||
const onChangeText = async (text: string) => {
|
|
||||||
attachmentSearchValue.current = text;
|
|
||||||
if (
|
|
||||||
!attachmentSearchValue.current ||
|
|
||||||
attachmentSearchValue.current === ""
|
|
||||||
) {
|
|
||||||
setAttachments(await filterAttachments(currentFilter));
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
clearTimeout(searchTimer.current);
|
|
||||||
searchTimer.current = setTimeout(async () => {
|
|
||||||
const results = await db.lookup
|
|
||||||
.attachments(attachmentSearchValue.current as string)
|
|
||||||
.sorted();
|
|
||||||
if (results.length === 0) return;
|
|
||||||
setAttachments(results);
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderItem = ({ index }: { item: boolean; index: number }) => (
|
|
||||||
<AttachmentItem
|
|
||||||
setAttachments={async () => {
|
|
||||||
setAttachments(await filterAttachments(currentFilter));
|
|
||||||
}}
|
|
||||||
errorOnly={currentFilter === "errors"}
|
|
||||||
attachments={attachments}
|
|
||||||
id={index}
|
|
||||||
context="global"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const onCheck = async () => {
|
|
||||||
const getState = () =>
|
|
||||||
useRechecker.getState().progress[note?.id || "all"] || {};
|
|
||||||
const setState = (state: Partial<RecheckerProgress>) =>
|
|
||||||
useRechecker.getState().setProgress(note?.id || "all", {
|
|
||||||
...getState(),
|
|
||||||
...state
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!attachments || getState().isWorking) return;
|
|
||||||
setState({
|
|
||||||
isWorking: true,
|
|
||||||
failed: 0,
|
|
||||||
passed: 0,
|
|
||||||
shown: true,
|
|
||||||
filter: currentFilterRef.current
|
|
||||||
});
|
|
||||||
const filter = currentFilterRef.current;
|
|
||||||
const filteredAttachments = await filterAttachments(
|
|
||||||
currentFilterRef.current
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
|
|
||||||
if (!getState().isWorking) {
|
|
||||||
ToastManager.show({
|
|
||||||
message: strings.attachmentRecheckCancelled(),
|
|
||||||
type: "info",
|
|
||||||
context: isSheet ? "local" : "global"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const attachment = (await attachments.item(i))?.item;
|
|
||||||
if (currentFilter == "errors" && !attachment?.failed) continue;
|
|
||||||
|
|
||||||
if (!attachment) continue;
|
|
||||||
const result = await filesystem.checkAttachment(attachment.hash);
|
|
||||||
if (!result) return;
|
|
||||||
if (result.failed) {
|
|
||||||
setState({
|
|
||||||
failed: getState().failed + 1
|
|
||||||
});
|
|
||||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
|
||||||
} else {
|
|
||||||
setState({
|
|
||||||
passed: getState().passed + 1
|
|
||||||
});
|
|
||||||
await db.attachments.markAsFailed(attachment.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filter === useRechecker.getState().currentFilter) {
|
|
||||||
setAttachments(await filterAttachments(currentFilter));
|
|
||||||
}
|
|
||||||
|
|
||||||
setState({
|
|
||||||
isWorking: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterAttachments = async (type: string) => {
|
|
||||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "all":
|
|
||||||
items = note
|
|
||||||
? db.attachments.ofNote(note.id, "all")
|
|
||||||
: db.attachments.all;
|
|
||||||
break;
|
|
||||||
case "images":
|
|
||||||
items = note
|
|
||||||
? db.attachments.ofNote(note.id, "images")
|
|
||||||
: db.attachments.images;
|
|
||||||
break;
|
|
||||||
case "video":
|
|
||||||
items = items = note
|
|
||||||
? db.attachments.ofNote(note.id, "videos")
|
|
||||||
: db.attachments.videos;
|
|
||||||
break;
|
|
||||||
case "audio":
|
|
||||||
items = note
|
|
||||||
? db.attachments.ofNote(note.id, "audio")
|
|
||||||
: db.attachments.audios;
|
|
||||||
break;
|
|
||||||
case "documents":
|
|
||||||
items = note
|
|
||||||
? db.attachments.ofNote(note.id, "documents")
|
|
||||||
: db.attachments.documents;
|
|
||||||
break;
|
|
||||||
case "orphaned":
|
|
||||||
items = db.attachments.orphaned;
|
|
||||||
break;
|
|
||||||
case "errors":
|
|
||||||
items = items = note
|
|
||||||
? db.attachments.ofNote(note.id, "all")
|
|
||||||
: db.attachments.all;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await items.sorted({
|
|
||||||
...DEFAULT_SORTING,
|
|
||||||
sortBy: "dateModified"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isSheet ? (
|
|
||||||
<>
|
|
||||||
<SheetProvider context="attachments-list" />
|
|
||||||
<Dialog context="local" />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
{!isSheet ? (
|
|
||||||
<Header
|
|
||||||
title={strings.manageAttachments()}
|
|
||||||
renderedInRoute="SettingsGroup"
|
|
||||||
canGoBack
|
|
||||||
headerRightButtons={[
|
|
||||||
{
|
|
||||||
onPress() {
|
|
||||||
onCheck();
|
|
||||||
},
|
|
||||||
title: strings.recheckAll()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onPress() {
|
|
||||||
if (!attachments) return;
|
|
||||||
presentDialog({
|
|
||||||
title: strings.doActions.download.attachment(
|
|
||||||
attachments.placeholders.length
|
|
||||||
),
|
|
||||||
positiveText: strings.network.download(),
|
|
||||||
positivePress: async () => {
|
|
||||||
downloadAttachments(await attachments.ids());
|
|
||||||
},
|
|
||||||
negativeText: strings.cancel()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
title: strings.downloadAllAttachments()
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: 12
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
name="check-all"
|
|
||||||
style={{
|
|
||||||
height: 40,
|
|
||||||
width: 40,
|
|
||||||
marginRight: 10
|
|
||||||
}}
|
|
||||||
color={colors.primary.paragraph}
|
|
||||||
size={SIZE.lg}
|
|
||||||
onPress={onCheck}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
name="download"
|
|
||||||
style={{
|
|
||||||
height: 40,
|
|
||||||
width: 40
|
|
||||||
}}
|
|
||||||
color={colors.primary.paragraph}
|
|
||||||
onPress={() => {
|
|
||||||
if (!attachments) return;
|
|
||||||
presentDialog({
|
|
||||||
title: strings.doActions.download.attachment(
|
|
||||||
attachments.placeholders.length
|
|
||||||
),
|
|
||||||
positiveText: strings.network.download(),
|
|
||||||
positivePress: async () => {
|
|
||||||
downloadAttachments(await attachments.ids());
|
|
||||||
},
|
|
||||||
negativeText: strings.cancel()
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size={SIZE.lg}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
alignSelf: "center",
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
height: "100%"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Seperator />
|
|
||||||
<Input
|
|
||||||
placeholder={strings.filterAttachments()}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
onSubmit={() => {
|
|
||||||
onChangeText(attachmentSearchValue.current as string);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{rechecker.shown ? (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
borderRadius: 10,
|
|
||||||
padding: 10,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.primary.border,
|
|
||||||
gap: 12,
|
|
||||||
justifyContent: "space-between"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: 12,
|
|
||||||
alignItems: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{rechecker.isWorking ? (
|
|
||||||
<ActivityIndicator color={colors.primary.accent} />
|
|
||||||
) : (
|
|
||||||
<Icon
|
|
||||||
name="file-check"
|
|
||||||
size={30}
|
|
||||||
color={colors.primary.accent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<Paragraph>
|
|
||||||
{rechecker.isWorking
|
|
||||||
? note
|
|
||||||
? strings.checkingNoteAttachments()
|
|
||||||
: strings.checkingAllAttachments()
|
|
||||||
: strings.attachmentRecheckComplete()}
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph>
|
|
||||||
{`${
|
|
||||||
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
|
|
||||||
}${strings.passed()}: ${
|
|
||||||
rechecker.passed
|
|
||||||
}, ${strings.failed()}: ${rechecker.failed}`}
|
|
||||||
</Paragraph>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
type={rechecker.isWorking ? "errorShade" : "plain"}
|
|
||||||
name={rechecker.isWorking ? "close" : "check"}
|
|
||||||
size={SIZE.lg}
|
|
||||||
color={
|
|
||||||
rechecker.isWorking ? colors.error.icon : colors.primary.accent
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
useRechecker.getState().setProgress(note?.id || "all", {
|
|
||||||
...useRechecker.getState().progress[note?.id || "all"],
|
|
||||||
shown: false,
|
|
||||||
isWorking: false
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<ScrollView
|
|
||||||
style={{
|
|
||||||
backgroundColor: colors.primary.background,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
flexDirection: "row",
|
|
||||||
paddingVertical: 12
|
|
||||||
}}
|
|
||||||
contentContainerStyle={{
|
|
||||||
alignItems: "center"
|
|
||||||
}}
|
|
||||||
horizontal
|
|
||||||
>
|
|
||||||
{attachmentTypes.map((item) =>
|
|
||||||
item.filterBy === "orphaned" && note ? null : (
|
|
||||||
<Button
|
|
||||||
type={
|
|
||||||
currentFilter === item.filterBy
|
|
||||||
? "secondaryAccented"
|
|
||||||
: "plain"
|
|
||||||
}
|
|
||||||
key={item.title}
|
|
||||||
title={item.title}
|
|
||||||
fontSize={SIZE.sm}
|
|
||||||
style={{
|
|
||||||
borderRadius: 100,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
height: 40,
|
|
||||||
minWidth: 80
|
|
||||||
}}
|
|
||||||
onPress={async () => {
|
|
||||||
const filterBy = item.filterBy;
|
|
||||||
setCurrentFilter(filterBy);
|
|
||||||
setLoading(true);
|
|
||||||
filterAttachments(filterBy)
|
|
||||||
.then((results) => {
|
|
||||||
if (filterBy !== currentFilterRef.current) return;
|
|
||||||
setAttachments(results);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch(console.log);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<FlashList
|
|
||||||
keyboardDismissMode="none"
|
|
||||||
keyboardShouldPersistTaps="always"
|
|
||||||
ListEmptyComponent={
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<ActivityIndicator size={40} color={colors.primary.accent} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon
|
|
||||||
name="attachment"
|
|
||||||
size={60}
|
|
||||||
color={colors.secondary.icon}
|
|
||||||
/>
|
|
||||||
<Paragraph>{strings.noAttachments()}</Paragraph>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
ListHeaderComponent={<AllProgress note={note} />}
|
|
||||||
ListFooterComponent={
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
height: 350
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
estimatedItemSize={50}
|
|
||||||
data={loading ? [] : attachments?.placeholders}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AllProgress = ({ note }: { note?: Note }) => {
|
|
||||||
const progress = useAttachmentStore((state) =>
|
|
||||||
note
|
|
||||||
? {
|
|
||||||
downloading: {
|
|
||||||
[note.id]: state.downloading?.[note.id]
|
|
||||||
},
|
|
||||||
uploading: {
|
|
||||||
[note.id]: state.uploading?.[note.id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: state
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
gap: 10,
|
|
||||||
width: "100%"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.keys(progress.downloading || {}).map((groupId) => (
|
|
||||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{Object.keys(progress.uploading || {}).map((groupId) => (
|
|
||||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AttachmentDialog.present = (note?: Note) => {
|
|
||||||
presentSheet({
|
|
||||||
component: () => <AttachmentDialog note={note} isSheet={true} />,
|
|
||||||
keyboardHandlerDisabled: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -36,7 +36,6 @@ import { IconButton } from "../ui/icon-button";
|
|||||||
import { hideAuth, initialAuthMode } from "./common";
|
import { hideAuth, initialAuthMode } from "./common";
|
||||||
import { Login } from "./login";
|
import { Login } from "./login";
|
||||||
import { Signup } from "./signup";
|
import { Signup } from "./signup";
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
|
|
||||||
export const AuthMode = {
|
export const AuthMode = {
|
||||||
login: 0,
|
login: 0,
|
||||||
@@ -154,12 +153,12 @@ const AuthModal = () => {
|
|||||||
|
|
||||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||||
<Button
|
<Button
|
||||||
title={strings.skip()}
|
title="Skip"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
hideAuth();
|
hideAuth();
|
||||||
}}
|
}}
|
||||||
iconSize={16}
|
iconSize={16}
|
||||||
type="plain"
|
type="gray"
|
||||||
iconPosition="right"
|
iconPosition="right"
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
height={25}
|
height={25}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { db } from "../../common/database";
|
|||||||
import {
|
import {
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
presentSheet,
|
presentSheet,
|
||||||
ToastManager
|
ToastEvent
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
import { useUserStore } from "../../stores/use-user-store";
|
import { useUserStore } from "../../stores/use-user-store";
|
||||||
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||||
@@ -35,7 +35,6 @@ import Seperator from "../ui/seperator";
|
|||||||
import { Dialog } from "../dialog";
|
import { Dialog } from "../dialog";
|
||||||
import BackupService from "../../services/backup";
|
import BackupService from "../../services/backup";
|
||||||
import { sleep } from "../../utils/time";
|
import { sleep } from "../../utils/time";
|
||||||
import { strings } from "@notesnook/intl";
|
|
||||||
|
|
||||||
export const ChangePassword = () => {
|
export const ChangePassword = () => {
|
||||||
const passwordInputRef = useRef();
|
const passwordInputRef = useRef();
|
||||||
@@ -49,18 +48,18 @@ export const ChangePassword = () => {
|
|||||||
|
|
||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
if (!user?.isEmailConfirmed) {
|
if (!user?.isEmailConfirmed) {
|
||||||
ToastManager.show({
|
ToastEvent.show({
|
||||||
heading: strings.emailNotConfirmed(),
|
heading: "Email not confirmed",
|
||||||
message: strings.emailNotConfirmedDesc(),
|
message: "Please confirm your email to change account password",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (error || !oldPassword.current || !password.current) {
|
if (error || !oldPassword.current || !password.current) {
|
||||||
ToastManager.show({
|
ToastEvent.show({
|
||||||
heading: strings.allFieldsRequired(),
|
heading: "All fields required",
|
||||||
message: strings.allFieldsRequiredDesc(),
|
message: "Fill all the fields and try again.",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
@@ -69,13 +68,12 @@ export const ChangePassword = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await BackupService.run(false, "change-password-dialog");
|
const result = await BackupService.run(false, "change-password-dialog");
|
||||||
if (!result.error)
|
if (!result) throw new Error("Failed to create backup");
|
||||||
throw new Error(strings.backupFailed() + `: ${result.error}`);
|
|
||||||
|
|
||||||
await db.user.clearSessions();
|
await db.user.clearSessions();
|
||||||
await db.user.changePassword(oldPassword.current, password.current);
|
await db.user.changePassword(oldPassword.current, password.current);
|
||||||
ToastManager.show({
|
ToastEvent.show({
|
||||||
heading: strings.passwordChangedSuccessfully(),
|
heading: "Account password updated",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "global"
|
context: "global"
|
||||||
});
|
});
|
||||||
@@ -85,8 +83,8 @@ export const ChangePassword = () => {
|
|||||||
eSendEvent(eOpenRecoveryKeyDialog);
|
eSendEvent(eOpenRecoveryKeyDialog);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
ToastManager.show({
|
ToastEvent.show({
|
||||||
heading: strings.passwordChangeFailed(),
|
heading: "Failed to change password",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -103,7 +101,10 @@ export const ChangePassword = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog context="change-password-dialog" />
|
<Dialog context="change-password-dialog" />
|
||||||
<DialogHeader title={strings.changePassword()} />
|
<DialogHeader
|
||||||
|
title="Change password"
|
||||||
|
paragraph="Enter your old and new passwords"
|
||||||
|
/>
|
||||||
<Seperator />
|
<Seperator />
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -117,7 +118,7 @@ export const ChangePassword = () => {
|
|||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
placeholder={strings.oldPassword()}
|
placeholder="Old Password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -126,21 +127,27 @@ export const ChangePassword = () => {
|
|||||||
password.current = value;
|
password.current = value;
|
||||||
}}
|
}}
|
||||||
onErrorCheck={(e) => setError(e)}
|
onErrorCheck={(e) => setError(e)}
|
||||||
returnKeyLabel={strings.next()}
|
returnKeyLabel="Next"
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
validationType="password"
|
validationType="password"
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
placeholder={strings.newPassword()}
|
placeholder="New password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Notice text={strings.changePasswordNotice()} type="alert" />
|
<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 }} />
|
<View style={{ height: 10 }} />
|
||||||
|
|
||||||
<Notice text={strings.changePasswordNotice2()} type="alert" />
|
<Notice
|
||||||
|
text={`Once your password is changed, please make sure to save the new account recovery key.`}
|
||||||
|
type="alert"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
@@ -150,7 +157,7 @@ export const ChangePassword = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
onPress={changePassword}
|
onPress={changePassword}
|
||||||
type="accent"
|
type="accent"
|
||||||
title={loading ? null : strings.changePasswordConfirm()}
|
title={loading ? null : "I understand, change my password"}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||