Compare commits
2 Commits
fix-github
...
fix-perman
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da36e69345 | ||
|
|
66bd6b23b5 |
@@ -14,16 +14,12 @@ const SCOPES = [
|
||||
|
||||
"mobile",
|
||||
"web",
|
||||
"vericrypt",
|
||||
"desktop",
|
||||
"crypto",
|
||||
"editor",
|
||||
"logger",
|
||||
"theme",
|
||||
"server",
|
||||
"core",
|
||||
"fs",
|
||||
"ui",
|
||||
"clipper",
|
||||
"config",
|
||||
"ci",
|
||||
@@ -31,10 +27,7 @@ const SCOPES = [
|
||||
"docs",
|
||||
"refactor",
|
||||
"misc",
|
||||
"common",
|
||||
"global",
|
||||
"docs",
|
||||
"themebuilder"
|
||||
"global"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -21,7 +21,4 @@ packages/editor/styles/
|
||||
packages/editor/languages/
|
||||
|
||||
# editor mobile
|
||||
packages/editor-mobile/build.bundle
|
||||
|
||||
# snapshots
|
||||
tap-snapshots
|
||||
packages/editor-mobile/build.bundle
|
||||
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
26
.github/actions/setup-node-with-cache/action.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: "Setup node with cache action"
|
||||
description: "Setups node with cache"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
apps/mobile/package-lock.json
|
||||
apps/desktop/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
extensions/web-clipper/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/sodium/package-lock.json
|
||||
packages/clipper/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
53
.github/workflows/android.publish.internal.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Publish @notesnook/android-internal
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "11"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: internal
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
55
.github/workflows/android.publish.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Publish @notesnook/android
|
||||
name: Publish @notesnook/mobile
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
@@ -11,23 +11,47 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "11"
|
||||
architecture: "x64"
|
||||
java-version: '11'
|
||||
architecture: 'x64'
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
npm ci
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
|
||||
@@ -39,9 +63,7 @@ jobs:
|
||||
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
|
||||
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
@@ -70,3 +92,14 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
21
.github/workflows/core.tests.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
paths:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
- ".github/workflows/core.tests.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- "ready_for_review"
|
||||
@@ -24,12 +24,25 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=core
|
||||
npm ci --prefer-offline --no-audit
|
||||
- name: Add environment variables
|
||||
run: |
|
||||
echo "USER_EMAIL=${{ secrets.USER_EMAIL }}" >> $GITHUB_ENV
|
||||
|
||||
149
.github/workflows/desktop.publish.yml
vendored
@@ -18,21 +18,6 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Publish on GitHub releases?"
|
||||
build-windows:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Windows?"
|
||||
build-linux:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Linux?"
|
||||
build-mac:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -43,12 +28,26 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Collect app metadata
|
||||
id: app_metadata
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
run: |
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
|
||||
@@ -86,16 +85,12 @@ jobs:
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
if: inputs.build-mac
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup notarization
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
@@ -103,17 +98,16 @@ jobs:
|
||||
|
||||
- name: Collect app metadata
|
||||
id: app_metadata
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
run: |
|
||||
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
|
||||
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
echo ::set-output name=bundle_version::$(cat package.json | jq -r .build.mac.bundleVersion)
|
||||
|
||||
|
||||
- name: Get App Store Version
|
||||
id: appstore
|
||||
uses: streetwriters/appstore-connect-app-version@develop
|
||||
if: inputs.publish-apple
|
||||
with:
|
||||
app-id: ${{ steps.app_metadata.outputs.apple_app_id }}
|
||||
key-id: ${{ secrets.api_key_id }}
|
||||
@@ -121,20 +115,24 @@ jobs:
|
||||
private-key-raw: ${{ secrets.api_key }}
|
||||
platform: "MAC_OS"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
path: ./apps/web/desktop/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build app for Mac App Store
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
@@ -142,9 +140,9 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.mac_certs }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
npm run build:mas
|
||||
npx electron-builder --mac mas --universal -p never
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build zip and dmg
|
||||
env:
|
||||
@@ -155,40 +153,36 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop
|
||||
npm run build
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
else
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Deploy to Testflight
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
env:
|
||||
API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
package=$(find ./output/mas-universal/notesnook*.pkg)
|
||||
run: |
|
||||
package=$(find ./dist/mas-universal/notesnook*.pkg)
|
||||
|
||||
echo "Uploading ${package} using altool..."
|
||||
|
||||
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Snapcraft Auth
|
||||
if: inputs.publish-snap
|
||||
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
|
||||
@@ -196,91 +190,84 @@ jobs:
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
if: inputs.publish-snap
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
path: ./apps/web/desktop/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run build
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
npx electron-builder --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
npx electron-builder --linux snap -p never
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
- name: Build AppImage deb and rpm
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
npx electron-builder --linux AppImage deb rpm -p always
|
||||
else
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
npx electron-builder --linux AppImage deb rpm -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Publish on Snapcraft
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/desktop
|
||||
snapcraft upload --release=stable ./dist/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install AzureCodeSigning
|
||||
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
|
||||
shell: pwsh
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
path: ./apps/web/desktop/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
run: npm i
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Build
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
- name: Build Electron wrapper
|
||||
run: npm run build
|
||||
working-directory: ./apps/web/desktop
|
||||
|
||||
- name: Publish
|
||||
- name: Build app
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --win --publish always
|
||||
npx electron-builder --win --publish always
|
||||
} else {
|
||||
yarn electron-builder --win --publish never
|
||||
npx electron-builder --win --publish never
|
||||
}
|
||||
working-directory: ./apps/desktop
|
||||
working-directory: ./apps/web/desktop
|
||||
43
.github/workflows/editor.tests.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Test @notesnook/editor
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/editor/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
- "synchronize"
|
||||
- "reopened"
|
||||
|
||||
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=editor
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build editor
|
||||
run: npx nx build @notesnook/editor
|
||||
|
||||
- name: Run all @notesnook/editor tests
|
||||
run: npx nx test @notesnook/editor
|
||||
35
.github/workflows/help.publish.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Publish Notesnook Help
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "docs/help/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install docgen
|
||||
run: cargo install --git https://github.com/thecodrr/docgen
|
||||
|
||||
- name: Build site
|
||||
run: docgen build --release
|
||||
working-directory: docs/help
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Publish on Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name notesnook-help ./docs/help/site/ --branch main
|
||||
67
.github/workflows/ios.publish.yml
vendored
@@ -1,67 +0,0 @@
|
||||
name: Publish @notesnook/ios
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npx nx run @notesnook/mobile:build
|
||||
|
||||
- name: Install Pods
|
||||
run: npm run prepare:ios
|
||||
|
||||
- name: Build iOS App
|
||||
uses: yukiarrr/ios-build-action@v1.11.2
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
export-options: apps/mobile/native/ios/ExportOptions.plist
|
||||
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
|
||||
update-targets: |
|
||||
Notesnook
|
||||
Make Note
|
||||
NotesWidgetExtension
|
||||
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
|
||||
code-signing-identity: Apple Distribution
|
||||
team-id: ${{ secrets.APPLE_TEAM_ID }}
|
||||
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
||||
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
|
||||
output-path: Notesnook.ipa
|
||||
mobileprovision-base64: |
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_APP }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||
|
||||
- name: 'Upload app to TestFlight'
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
with:
|
||||
app-path: Notesnook.ipa
|
||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.ipa
|
||||
path: Notesnook.ipa
|
||||
2
.github/workflows/nscurl.test.yml
vendored
@@ -11,4 +11,4 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Run ats-diagnostics
|
||||
run: nscurl --ats-diagnostics https://payments.streetwriters.co
|
||||
run: nscurl --ats-diagnostics https://payments.streetwriters.co
|
||||
31
.github/workflows/theme-builder.publish.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Publish @notesnook/theme-builder
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=themebuilder
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build
|
||||
run: npm run build:theme-builder
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-theme-builder ./apps/theme-builder/build/
|
||||
31
.github/workflows/vericrypt.publish.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Publish @notesnook/vericrypt
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=vericrypt
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build
|
||||
run: npm run build:vericrypt
|
||||
|
||||
- name: Publish on Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=vericrypt ./apps/vericrypt/build/
|
||||
69
.github/workflows/web.beta.publish.yml
vendored
@@ -1,69 +0,0 @@
|
||||
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' }}
|
||||
26
.github/workflows/web.publish.yml
vendored
@@ -11,12 +11,26 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -24,8 +38,8 @@ jobs:
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build
|
||||
- name: Generate desktop build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/
|
||||
- name: Publish on Cloudflare Pages
|
||||
run: npx --yes wrangler pages publish --project-name=notesnook-app ./apps/web/build/
|
||||
|
||||
38
.github/workflows/web.tests.yml
vendored
@@ -25,12 +25,26 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
@@ -62,11 +76,25 @@ jobs:
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
apps/mobile/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm i --ignore-scripts --prefer-offline --no-audit
|
||||
npm install -D @playwright/test otplib
|
||||
|
||||
- name: Validate build extraction
|
||||
|
||||
40
.github/workflows/web.v3-beta.publish.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Publish @notesnook/web v3 Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: 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' }}
|
||||
6
.gitignore
vendored
@@ -6,8 +6,4 @@ dist
|
||||
nx-cloud.env
|
||||
.idea
|
||||
.eslintcache
|
||||
.env.local
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
.env.local
|
||||
@@ -5,6 +5,5 @@ module.exports = {
|
||||
printWidth: 80,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
endOfLine: "lf"
|
||||
bracketSpacing: true
|
||||
};
|
||||
|
||||
11
.vscode/extensions.json
vendored
@@ -1,11 +0,0 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-playwright.playwright",
|
||||
"firefox-devtools.vscode-firefox-debug",
|
||||
"msjsdiag.vscode-react-native"
|
||||
]
|
||||
}
|
||||
14
.vscode/settings.json
vendored
@@ -1,15 +1,3 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"eslint.run": "onSave",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"eslint.options": {
|
||||
"cache": true,
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"git.rebaseWhenSync": true
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
|
||||
|
||||
Ugh! Bugs!
|
||||
|
||||
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
|
||||
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
|
||||
|
||||
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
|
||||
|
||||
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
|
||||
|
||||
@@ -28,9 +28,15 @@ If you've thought of a way that Notesnook could be better, we want to hear about
|
||||
|
||||
Before you open a new feature request, please make sure it's not a duplicate. **Duplicate feature requests & bug reports are closed immediately.**
|
||||
|
||||
### Improving documentation
|
||||
|
||||
All app related documentation can be found in the `docs/` directory. All the files in this directory are simple markdown files which you can edit either directly on GitHub, or in your favorite text editor.
|
||||
|
||||
If you find a mistake in the documentation or would like to contribute something new to the documentation, feel free to open a pull request; creating an issue for this is not required.
|
||||
|
||||
### Helping out in the issue tracker
|
||||
|
||||
New issues are always opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
There are always new issues getting opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
|
||||
|
||||
In addition to this, you can help out in the following ways:
|
||||
|
||||
@@ -62,7 +68,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
|
||||
|
||||
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
|
||||
2. Run `npm run bootstrap` in the repository root.
|
||||
3. If you've fixed a bug or added code that should be tested, add tests!
|
||||
3. If you’ve fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`npm run test`).
|
||||
5. Format your code with prettier (`npm run prettier`).
|
||||
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
|
||||
@@ -85,11 +91,7 @@ The DCO is a declaration attached to every contribution made by every developer
|
||||
Signed-off-by: Jane Smith <jane.smith@email.com>
|
||||
```
|
||||
|
||||
On VSCode, it looks like this:
|
||||
|
||||

|
||||
|
||||
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message automatically.
|
||||
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message.
|
||||
|
||||
#### Include `<scope>:` in your commit message
|
||||
|
||||
@@ -103,7 +105,7 @@ All commits must include valid scopes in the commit message. **Valid commit scop
|
||||
|
||||
**Packages:**
|
||||
|
||||
1. `crypto`: changes related to the cryptographic core
|
||||
1. `crypto`: changes related to the cryptographic core (including `@notesnook/crypto-worker`)
|
||||
2. `editor`: changes related to the editor (including `@notesnook/editor-mobile`)
|
||||
3. `logger`: changes related to the logger
|
||||
4. `theme`: changes related to the theme
|
||||
@@ -131,11 +133,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
|
||||
|
||||
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
|
||||
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb’s Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
|
||||
|
||||
## Git Branch Organization
|
||||
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don’t use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
|
||||
|
||||
1. All tests must be passing at all times
|
||||
2. There should be as few breaking changes as possible
|
||||
|
||||
38
README.md
@@ -10,11 +10,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
|
||||
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
|
||||
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
|
||||
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
|
||||
|
||||
## Developer guide
|
||||
|
||||
@@ -25,29 +25,25 @@ Notesnook is built using the following technologies:
|
||||
1. JavaScript/Typescript — this repo is in a hybrid state. A lot of the newer code is being written in Typescript & the old code is slowly being ported over.
|
||||
2. React — the whole front-end across all platforms is built using React.
|
||||
3. React Native — For mobile apps we are using React Native
|
||||
4. Electron — For desktop app
|
||||
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
6. Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
4. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
5. Lerna & Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
|
||||
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
|
||||
|
||||
### Monorepo structure
|
||||
|
||||
| Name | Path | Description |
|
||||
| -------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `@notesnook/web` | [/apps/web](/apps/web) | Web client |
|
||||
| `@notesnook/desktop` | [/apps/desktop](/apps/desktop) | Desktop client |
|
||||
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
|
||||
| `@notesnook/web-clipper` | [/extensions/web-clipper](/extensions/web-clipper) | Web clipper |
|
||||
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
|
||||
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
|
||||
| `@notesnook/clipper` | [/packages/clipper](/packages/clipper) | Web clipper core handling everything related to actual page clipping |
|
||||
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/sodium` | [/packages/sodium](/packages/sodium) | Wrapper around libsodium to support Node.js & Browser |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
| Name | Path | Description |
|
||||
| -------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `@notesnook/web` | [/apps/web](/apps/web) | Web/Desktop clients |
|
||||
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
|
||||
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
|
||||
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
|
||||
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
|
||||
### Contributing guidelines
|
||||
|
||||
|
||||
4
apps/desktop/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
output
|
||||
dist
|
||||
@@ -1,58 +0,0 @@
|
||||
<p align="center">
|
||||
<img style="align:center;" src="/resources/screenshots/web.jpg" alt="Notesnook desktop screenshot" width="600" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Notesnook Desktop</h1>
|
||||
<h3 align="center">The desktop app is built using Electron & Typescript.</h3>
|
||||
<p align="center">
|
||||
<a href="https://notesnook.com/downloads">Downloads</a> | <a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
|
||||
</p>
|
||||
|
||||
## Getting started
|
||||
|
||||
## Build instructions
|
||||
|
||||
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
|
||||
### Setting up the development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/download/)
|
||||
2. [git](https://git-scm.com/downloads)
|
||||
3. NPM (not yarn or pnpm)
|
||||
|
||||
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
|
||||
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
|
||||
# change directory
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
## Developer guide
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible:
|
||||
|
||||
1. Electron
|
||||
2. tRPC: for cross-communication between the web & desktop parts
|
||||
3. zod: for runtime typechecking
|
||||
4. yargs: for CLI argument parsing
|
||||
@@ -1,3 +0,0 @@
|
||||
owner: streetwriters
|
||||
repo: notesnook
|
||||
provider: github
|
||||
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB |
58
apps/desktop/global.d.ts
vendored
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { BrowserWindow } from "electron";
|
||||
import {
|
||||
type FormData as FormDataType,
|
||||
type Headers as HeadersType,
|
||||
type Request as RequestType,
|
||||
type Response as ResponseType
|
||||
} from "undici";
|
||||
|
||||
declare global {
|
||||
var window: BrowserWindow | null;
|
||||
var RELEASE: boolean;
|
||||
var MAC_APP_STORE: boolean;
|
||||
|
||||
// Re-export undici fetch function and various classes to global scope.
|
||||
// These are classes and functions expected to be at global scope according to Node.js v18 API
|
||||
// documentation.
|
||||
// See: https://nodejs.org/dist/latest-v18.x/docs/api/globals.html
|
||||
// eslint-disable-next-line no-var
|
||||
export var {
|
||||
FormData,
|
||||
Headers,
|
||||
Request,
|
||||
Response,
|
||||
fetch
|
||||
}: typeof import("undici");
|
||||
|
||||
type FormData = FormDataType;
|
||||
type Headers = HeadersType;
|
||||
type Request = RequestType;
|
||||
type Response = ResponseType;
|
||||
}
|
||||
|
||||
// NOTE: the import in the global block above needs to be a var for this to work properly.
|
||||
globalThis.fetch = fetch;
|
||||
globalThis.FormData = FormData;
|
||||
globalThis.Headers = Headers;
|
||||
globalThis.Request = Request;
|
||||
globalThis.Response = Response;
|
||||
4138
apps/desktop/package-lock.json
generated
@@ -1,36 +0,0 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index fcb7f54..cb1c7f7 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
+ actions: undefined
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
+
|
||||
+ if (targetSpecificOptions.desktop.actions) {
|
||||
+ let actionsData = "";
|
||||
+ const validActions = [];
|
||||
+ for (const action of targetSpecificOptions.desktop.actions) {
|
||||
+ if (!action.id || !action.name || !action.args) continue;
|
||||
+ actionsData += "\n";
|
||||
+ actionsData += `[Desktop Action ${action.id}]
|
||||
+Name=${action.name}
|
||||
+Exec=${desktopMeta.Exec} ${action.args}`;
|
||||
+ actionsData += "\n";
|
||||
+
|
||||
+ validActions.push(action.id);
|
||||
+ }
|
||||
+
|
||||
+ data += `Actions=${validActions.join(";")};\n${actionsData}`
|
||||
+ }
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 379cf3b..2644e5d 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -221,9 +221,16 @@ class G {
|
||||
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
|
||||
}
|
||||
detachWindow(r) {
|
||||
+
|
||||
y(this, c, i(this, c).filter((n) => n !== r));
|
||||
- for (const [n, t] of i(this, u).entries())
|
||||
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ for (const [n, t] of i(this, u).entries()) {
|
||||
+ try {
|
||||
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ } catch(e) {
|
||||
+ console.error(e);
|
||||
+ // ignore
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import yargs from "yargs-parser";
|
||||
import os from "os";
|
||||
import * as childProcess from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = (arch) =>
|
||||
path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`,
|
||||
`${os.platform()}-${arch}`
|
||||
);
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(`cd ${webAppPath} && npm run build:desktop`);
|
||||
}
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`npm run bundle:mas`);
|
||||
} else {
|
||||
await exec(`npm run bundle`);
|
||||
}
|
||||
|
||||
await exec(`npx tsc`);
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("x64"))) {
|
||||
console.log("copying sodium-native-x64");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("x64"),
|
||||
path.join("build", "prebuilds", `${process.platform}-x64`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("ia32"))) {
|
||||
console.log("copying sodium-native-ia32");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("ia32"),
|
||||
path.join("build", "prebuilds", `${process.platform}-ia32`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(sodiumNativePrebuildPath("arm64"))) {
|
||||
console.log("copying sodium-native-arm64");
|
||||
await fs.cp(
|
||||
sodiumNativePrebuildPath("arm64"),
|
||||
path.join("build", "prebuilds", `${process.platform}-arm64`),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (args.run) {
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
await exec(`./output/darwin-unpacked/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
return childProcess.execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit"
|
||||
});
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import chokidar from "chokidar";
|
||||
import { execSync, spawn } from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import treekill from "tree-kill";
|
||||
import crypto from "crypto";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`
|
||||
);
|
||||
const RUNNING_PROCESSES = [];
|
||||
const RESTARTABLE_PROCESSES = [];
|
||||
let lastBundleHash = null;
|
||||
|
||||
await onChange(true);
|
||||
|
||||
console.log("Watching...");
|
||||
|
||||
const watcher = chokidar.watch("src/", { ignoreInitial: true });
|
||||
watcher.on("all", () => {
|
||||
onChange(false);
|
||||
});
|
||||
|
||||
process.on("SIGINT", async (s) => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
}
|
||||
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
console.log("Restarting...", RESTARTABLE_PROCESSES.length);
|
||||
await killProcesses(RESTARTABLE_PROCESSES);
|
||||
}
|
||||
|
||||
execAsync(
|
||||
"npx",
|
||||
["electron", path.join("build", "electron.js")],
|
||||
true,
|
||||
cleanup
|
||||
);
|
||||
}
|
||||
|
||||
function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(">", ...cmd);
|
||||
|
||||
const s = spawn(cmd[0], cmd.slice(1), {
|
||||
cwd,
|
||||
env: { ...process.env, NO_COLOR: "true" },
|
||||
shell: false
|
||||
});
|
||||
|
||||
RUNNING_PROCESSES.push(s);
|
||||
|
||||
s.stderr.pipe(process.stderr);
|
||||
s.stdout.on("data", (data) => {
|
||||
process.stdout.write(data);
|
||||
if (predicate(data)) resolve(undefined); //
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
try {
|
||||
console.log(">", cmd);
|
||||
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: false
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
function execAsync(cmd, args, restartable, onExit) {
|
||||
try {
|
||||
console.log(">", cmd, ...args);
|
||||
|
||||
const proc = spawn(cmd, args, {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: false
|
||||
});
|
||||
|
||||
const array = restartable ? RESTARTABLE_PROCESSES : RUNNING_PROCESSES;
|
||||
array.push(proc);
|
||||
proc.on("exit", (code, signal) => {
|
||||
console.log(cmd, ...args, "closed with code", code);
|
||||
if (code === 0 && !signal) {
|
||||
array.splice(array.indexOf(proc), 1);
|
||||
onExit && onExit();
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function killProcesses(processes) {
|
||||
for (const process of processes.slice()) {
|
||||
processes.splice(processes.indexOf(process), 1);
|
||||
await new Promise((resolve, reject) =>
|
||||
treekill(process.pid, (err) => (err ? reject(err) : resolve()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
console.log("Cleaning up");
|
||||
|
||||
await killProcesses(RESTARTABLE_PROCESSES);
|
||||
await killProcesses(RUNNING_PROCESSES);
|
||||
await sleep(1000);
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
process.exit();
|
||||
}
|
||||
|
||||
async function isBundleSame() {
|
||||
const bundle = await fs.readFile(
|
||||
path.join(__dirname, "..", "build", "electron.js")
|
||||
);
|
||||
|
||||
const hashSum = crypto.createHash("sha256");
|
||||
hashSum.update(bundle);
|
||||
const hex = hashSum.digest("hex");
|
||||
|
||||
if (!lastBundleHash) {
|
||||
lastBundleHash = hex;
|
||||
return false;
|
||||
}
|
||||
if (hex === lastBundleHash) return true;
|
||||
lastBundleHash = hex;
|
||||
return false;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { writeFileSync, rmSync, readFileSync } = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
const { relative, join } = require("path");
|
||||
|
||||
module.exports = async function (configuration) {
|
||||
const Endpoint = "https://weu.codesigning.azure.net";
|
||||
const CodeSigningAccountName = "Notesnook";
|
||||
const CertificateProfileName = "Notesnook";
|
||||
const FileDigest = configuration.hash.toUpperCase();
|
||||
const TimestampRfc3161 = "http://timestamp.acs.microsoft.com";
|
||||
const TimestampDigest = configuration.hash.toUpperCase();
|
||||
const Description = "The Notesnook app";
|
||||
const DescriptionUrl = "https://notesnook.com/";
|
||||
const FilesCatalog = createCatalog(configuration.path);
|
||||
|
||||
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
|
||||
|
||||
console.debug("Signing", configuration.path, "using command", command);
|
||||
|
||||
psexec(command);
|
||||
|
||||
console.debug("Signed", configuration.path);
|
||||
|
||||
deleteCatalog();
|
||||
};
|
||||
|
||||
function createCatalog(path) {
|
||||
writeFileSync("_catalog", relative(__dirname, path));
|
||||
return join(__dirname, "_catalog");
|
||||
}
|
||||
|
||||
function deleteCatalog() {
|
||||
rmSync("_catalog");
|
||||
}
|
||||
|
||||
function psexec(cmd) {
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: "pwsh"
|
||||
});
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { EventEmitter } from "events";
|
||||
import TypedEventEmitter from "typed-emitter";
|
||||
|
||||
export type AppEvents = {
|
||||
onCreateItem(name: "note" | "notebook" | "reminder"): void;
|
||||
};
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
const typedEmitter = emitter as TypedEventEmitter<AppEvents>;
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const bridgeRouter = t.router({
|
||||
onCreateItem: createSubscription("onCreateItem")
|
||||
});
|
||||
|
||||
export const bridge: AppEvents = new Proxy({} as AppEvents, {
|
||||
get(_t, name) {
|
||||
if (typeof name === "symbol") return;
|
||||
return (...args: unknown[]) => {
|
||||
emitter.emit(name, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function createSubscription<TName extends keyof AppEvents>(eventName: TName) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<Parameters<AppEvents[TName]>[0]>((emit) => {
|
||||
const listener: AppEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
typedEmitter.addListener(eventName, listener);
|
||||
return () => {
|
||||
typedEmitter.removeListener(eventName, listener);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import zlib from "node:zlib";
|
||||
import utils from "node:util";
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const gzipAsync = utils.promisify(zlib.gzip);
|
||||
const gunzipAsync = utils.promisify(zlib.gunzip);
|
||||
|
||||
export const compressionRouter = t.router({
|
||||
gzip: t.procedure
|
||||
.input(z.object({ data: z.string(), level: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { data, level } = input;
|
||||
return (await gzipAsync(data, { level })).toString("base64");
|
||||
}),
|
||||
gunzip: t.procedure.input(z.string()).query(async ({ input }) => {
|
||||
return (await gunzipAsync(Buffer.from(input, "base64"))).toString("utf-8");
|
||||
})
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { compressionRouter } from "./compression";
|
||||
import { osIntegrationRouter } from "./os-integration";
|
||||
import { spellCheckerRouter } from "./spell-checker";
|
||||
import { updaterRouter } from "./updater";
|
||||
import { bridgeRouter } from "./bridge";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const router = t.router({
|
||||
compress: compressionRouter,
|
||||
integration: osIntegrationRouter,
|
||||
spellChecker: spellCheckerRouter,
|
||||
updater: updaterRouter,
|
||||
bridge: bridgeRouter
|
||||
});
|
||||
|
||||
export const api = router.createCaller({});
|
||||
|
||||
// Export type router type signature,
|
||||
// NOT the router itself.
|
||||
export type AppRouter = typeof router;
|
||||
@@ -1,173 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
import { getTheme, setTheme, Theme } from "../utils/theme";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const NotificationOptions = z.object({
|
||||
title: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
silent: z.boolean().optional(),
|
||||
timeoutType: z.union([z.literal("default"), z.literal("never")]).optional(),
|
||||
urgency: z
|
||||
.union([z.literal("normal"), z.literal("critical"), z.literal("low")])
|
||||
.optional(),
|
||||
tag: z.string()
|
||||
});
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
globalThis.window?.webContents.setZoomFactor(factor);
|
||||
config.zoomFactor = factor;
|
||||
}),
|
||||
|
||||
privacyMode: t.procedure.query(() => config.privacyMode),
|
||||
setPrivacyMode: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
if (!globalThis.window || !["win32", "darwin"].includes(process.platform))
|
||||
return;
|
||||
|
||||
globalThis.window.setContentProtection(enabled);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
globalThis.window.setThumbnailClip(
|
||||
enabled
|
||||
? { x: 0, y: 0, width: 1, height: 1 }
|
||||
: { x: 0, y: 0, width: 0, height: 0 }
|
||||
);
|
||||
}
|
||||
config.privacyMode = enabled;
|
||||
}),
|
||||
|
||||
desktopIntegration: t.procedure.query(() => config.desktopSettings),
|
||||
setDesktopIntegration: t.procedure
|
||||
.input(DesktopIntegration)
|
||||
.mutation(({ input: settings }) => {
|
||||
if (settings.autoStart) {
|
||||
AutoLaunch.enable(!!settings.startMinimized);
|
||||
} else {
|
||||
AutoLaunch.disable();
|
||||
}
|
||||
config.desktopSettings = settings;
|
||||
setupDesktopIntegration(settings);
|
||||
}),
|
||||
|
||||
selectDirectory: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
buttonLabel: z.string().optional(),
|
||||
defaultPath: z.string().optional()
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (!globalThis.window) return undefined;
|
||||
|
||||
const { title, buttonLabel, defaultPath } = input;
|
||||
|
||||
const result = await dialog.showOpenDialog(globalThis.window, {
|
||||
title,
|
||||
buttonLabel,
|
||||
properties: ["openDirectory"],
|
||||
defaultPath: defaultPath && resolvePath(defaultPath)
|
||||
});
|
||||
if (result.canceled) return undefined;
|
||||
|
||||
return result.filePaths[0];
|
||||
}),
|
||||
|
||||
saveFile: t.procedure
|
||||
.input(z.object({ data: z.string(), filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { data, filePath } = input;
|
||||
if (!data || !filePath) return;
|
||||
|
||||
const resolvedPath = resolvePath(filePath);
|
||||
|
||||
mkdirSync(dirname(resolvedPath), { recursive: true });
|
||||
writeFileSync(resolvedPath, data);
|
||||
}),
|
||||
|
||||
resolvePath: t.procedure
|
||||
.input(z.object({ filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { filePath } = input;
|
||||
if (!filePath) return;
|
||||
return resolvePath(filePath);
|
||||
}),
|
||||
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
const notification = new Notification({
|
||||
...input,
|
||||
icon: AssetManager.appIcon({
|
||||
size: 64,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
})
|
||||
});
|
||||
notification.show();
|
||||
if (input.urgency === "critical") {
|
||||
shell.beep();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
notification.once("close", () => resolve(undefined));
|
||||
notification.once("click", () => resolve(input.tag));
|
||||
});
|
||||
}),
|
||||
openPath: t.procedure
|
||||
.input(z.object({ type: z.literal("path"), link: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { type, link } = input;
|
||||
if (type === "path") return shell.openPath(resolvePath(link));
|
||||
}),
|
||||
bringToFront: t.procedure.query(() => bringToFront()),
|
||||
changeTheme: t.procedure
|
||||
.input(Theme)
|
||||
.mutation(({ input }) => setTheme(input)),
|
||||
|
||||
onThemeChanged: t.procedure.subscription(() =>
|
||||
observable<"dark" | "light">((emit) => {
|
||||
nativeTheme.on("updated", () => {
|
||||
if (getTheme() === "system") {
|
||||
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
});
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { config } from "../utils/config";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const LANGUAGES: Record<string, string> = {
|
||||
af: "Afrikaans",
|
||||
bg: "Bulgarian",
|
||||
ca: "Catalan",
|
||||
cs: "Czech",
|
||||
cy: "Welsh",
|
||||
da: "Danish",
|
||||
de: "German",
|
||||
"de-DE": "German (Germany)",
|
||||
el: "Greek",
|
||||
en: "English",
|
||||
"en-AU": "English (Australia)",
|
||||
"en-CA": "English (Canada)",
|
||||
"en-GB": "English (UK)",
|
||||
"en-GB-oxendict": "English (UK Oxford)",
|
||||
"en-US": "English (US)",
|
||||
es: "Spanish",
|
||||
"es-419": "Spanish (Latin America)",
|
||||
"es-AR": "Spanish (Argentina)",
|
||||
"es-ES": "Spanish (Spain)",
|
||||
"es-MX": "Spanish (Mexico)",
|
||||
"es-US": "Spanish (US)",
|
||||
et: "Estonian",
|
||||
fa: "Persian",
|
||||
fo: "Faroese",
|
||||
fr: "French",
|
||||
"fr-FR": "French (France)",
|
||||
he: "Hebrew",
|
||||
hi: "Hindi",
|
||||
hr: "Croatian",
|
||||
hu: "Hungarian",
|
||||
hy: "Armenian",
|
||||
id: "Indonesian",
|
||||
it: "Italian",
|
||||
"it-IT": "Italian (Italy)",
|
||||
ko: "Korean",
|
||||
lt: "Lithuanian",
|
||||
lv: "Latvian",
|
||||
nb: "Norwegian Bokmål",
|
||||
nl: "Dutch",
|
||||
pl: "Polish",
|
||||
pt: "Portuguese",
|
||||
"pt-BR": "Portuguese (Brazil)",
|
||||
"pt-PT": "Portuguese (Portugal)",
|
||||
ro: "Romanian",
|
||||
ru: "Russian",
|
||||
sh: "Serbo-Croatian",
|
||||
sk: "Slovak",
|
||||
sl: "Slovenian",
|
||||
sq: "Albanian",
|
||||
sr: "Serbian",
|
||||
sv: "Swedish",
|
||||
ta: "Tamil",
|
||||
tg: "Tajik",
|
||||
tr: "Turkish",
|
||||
uk: "Ukrainian",
|
||||
vi: "Vietnamese"
|
||||
};
|
||||
|
||||
type Language = { code: string; name: string };
|
||||
|
||||
export const spellCheckerRouter = t.router({
|
||||
isEnabled: t.procedure.query(() => config.isSpellCheckerEnabled),
|
||||
languages: t.procedure.query(
|
||||
() =>
|
||||
<Language[]>(
|
||||
globalThis.window?.webContents.session.availableSpellCheckerLanguages.map(
|
||||
(code) => ({
|
||||
code,
|
||||
name: LANGUAGES[code]
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
enabledLanguages: t.procedure.query(
|
||||
() =>
|
||||
<Language[]>(
|
||||
globalThis.window?.webContents.session
|
||||
.getSpellCheckerLanguages()
|
||||
.map((code) => ({
|
||||
code,
|
||||
name: LANGUAGES[code]
|
||||
}))
|
||||
)
|
||||
),
|
||||
setLanguages: t.procedure
|
||||
.input(z.array(z.string()))
|
||||
.mutation(({ input: languages }) =>
|
||||
globalThis.window?.webContents.session.setSpellCheckerLanguages(languages)
|
||||
),
|
||||
toggle: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
|
||||
config.isSpellCheckerEnabled = enabled;
|
||||
})
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { CancellationToken, autoUpdater } from "electron-updater";
|
||||
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
|
||||
import { z } from "zod";
|
||||
import { config } from "../utils/config";
|
||||
|
||||
type UpdateInfo = { version: string };
|
||||
type Progress = { percent: number };
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
const cancellationToken = new CancellationToken();
|
||||
await autoUpdater.downloadUpdate(cancellationToken);
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
await autoUpdater.checkForUpdates();
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
config.automaticUpdates = enabled;
|
||||
}),
|
||||
|
||||
onChecking: createSubscription("checking-for-update"),
|
||||
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
|
||||
"update-downloaded"
|
||||
),
|
||||
onDownloadProgress: createSubscription<"download-progress", Progress>(
|
||||
"download-progress"
|
||||
),
|
||||
onNotAvailable: createSubscription<"update-not-available", UpdateInfo>(
|
||||
"update-not-available"
|
||||
),
|
||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||
"update-available"
|
||||
),
|
||||
onError: createSubscription("error")
|
||||
});
|
||||
|
||||
function createSubscription<
|
||||
TName extends keyof AppUpdaterEvents,
|
||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||
>(eventName: TName) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<TReturnType>((emit) => {
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.addListener(eventName, listener);
|
||||
return () => {
|
||||
autoUpdater.removeListener(eventName, listener);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,88 +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 yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
export type CLIOptions = {
|
||||
note: boolean | string;
|
||||
notebook: boolean | string;
|
||||
reminder: boolean | string;
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
export async function parseArguments(argv: string[]): Promise<CLIOptions> {
|
||||
const result: CLIOptions = {
|
||||
note: false,
|
||||
notebook: false,
|
||||
reminder: false,
|
||||
hidden: false
|
||||
};
|
||||
const { hidden } = await yargs(hideBin(argv))
|
||||
.boolean("hidden")
|
||||
// have to account for this flag added on Windows when launching
|
||||
// via Jumplist
|
||||
.boolean("allow-file-access-from-files")
|
||||
.command("new", "Create a new item", (yargs) => {
|
||||
return yargs
|
||||
.command("note", "Create a new note", {}, () => {
|
||||
result.note = true;
|
||||
console.log("HERE!");
|
||||
})
|
||||
.command("notebook", "Create a new notebook", {}, () => {
|
||||
result.notebook = true;
|
||||
})
|
||||
.command("reminder", "Add a new reminder", {}, () => {
|
||||
result.reminder = true;
|
||||
});
|
||||
})
|
||||
.command("open", "Open a specific item", (yargs) => {
|
||||
return yargs
|
||||
.command(
|
||||
"note",
|
||||
"Open a note",
|
||||
{ id: { string: true, description: "Id of the note" } },
|
||||
(args) => {
|
||||
result.note = args.id || false;
|
||||
}
|
||||
)
|
||||
.command(
|
||||
"notebook",
|
||||
"Open a notebook",
|
||||
{ id: { string: true, description: "Id of the notebook" } },
|
||||
(args) => {
|
||||
result.notebook = args.id || false;
|
||||
}
|
||||
)
|
||||
.command(
|
||||
"topic",
|
||||
"Open a topic",
|
||||
{
|
||||
id: { string: true, description: "Id of the topic" },
|
||||
notebookId: { string: true, description: "Id of the notebook" }
|
||||
},
|
||||
(args) => {
|
||||
result.notebook = `${args.notebookId}/${args.id}`;
|
||||
}
|
||||
);
|
||||
})
|
||||
.parse();
|
||||
result.hidden = hidden || false;
|
||||
return result;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
@@ -1,179 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, nativeTheme, shell } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
|
||||
import { setupMenu } from "./utils/menu";
|
||||
import { WindowState } from "./utils/window-state";
|
||||
import { setupJumplist } from "./utils/jumplist";
|
||||
import { setupTray } from "./utils/tray";
|
||||
import { CLIOptions, parseArguments } from "./cli";
|
||||
import { AssetManager } from "./utils/asset-manager";
|
||||
import { createIPCHandler } from "electron-trpc/main";
|
||||
import { router, api } from "./api";
|
||||
import { config } from "./utils/config";
|
||||
import path from "path";
|
||||
import { bringToFront } from "./utils/bring-to-front";
|
||||
import { bridge } from "./api/bridge";
|
||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
console.log("Another instance is already running!");
|
||||
app.exit();
|
||||
}
|
||||
|
||||
if (process.platform == "win32" && process.env.PORTABLE_EXECUTABLE_DIR) {
|
||||
console.log("Portable app: true");
|
||||
const root = path.join(process.env.PORTABLE_EXECUTABLE_DIR, "Notesnook");
|
||||
app.setPath("appData", path.join(root, "AppData"));
|
||||
app.setPath("documents", path.join(root, "Documents"));
|
||||
app.setPath("userData", path.join(root, "UserData"));
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId(app.name);
|
||||
}
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("uncaughtException:", error);
|
||||
});
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("unhandledRejection:", reason);
|
||||
});
|
||||
|
||||
app.commandLine.appendSwitch("lang", "en-US");
|
||||
|
||||
async function createWindow() {
|
||||
const cliOptions = await parseArguments(process.argv);
|
||||
setTheme(getTheme());
|
||||
|
||||
const mainWindowState = new WindowState({});
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: !cliOptions.hidden,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
darkTheme: getTheme() === "dark",
|
||||
backgroundColor: getBackgroundColor(),
|
||||
opacity: 0,
|
||||
autoHideMenuBar: false,
|
||||
icon: AssetManager.appIcon({
|
||||
size: 512,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
}),
|
||||
webPreferences: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
});
|
||||
|
||||
createIPCHandler({ router, windows: [mainWindow] });
|
||||
globalThis.window = mainWindow;
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
|
||||
mainWindow.minimize();
|
||||
|
||||
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
|
||||
mainWindow.setOpacity(1);
|
||||
|
||||
if (config.privacyMode) {
|
||||
await api.integration.setPrivacyMode({ enabled: config.privacyMode });
|
||||
}
|
||||
|
||||
await AssetManager.loadIcons();
|
||||
setupDesktopIntegration(config.desktopSettings);
|
||||
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
});
|
||||
|
||||
setupMenu();
|
||||
setupJumplist();
|
||||
|
||||
if (isDevelopment())
|
||||
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
nativeTheme.on("updated", () => {
|
||||
setupTray();
|
||||
setupJumplist();
|
||||
});
|
||||
}
|
||||
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
if (!isDevelopment()) registerProtocol();
|
||||
await createWindow();
|
||||
configureAutoUpdater();
|
||||
});
|
||||
|
||||
app.once("window-all-closed", () => {
|
||||
if (process.platform !== "darwin" || MAC_APP_STORE) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("second-instance", async (_ev, argv) => {
|
||||
if (!globalThis.window) return;
|
||||
const cliOptions = await parseArguments(argv);
|
||||
if (cliOptions.note) bridge.onCreateItem("note");
|
||||
if (cliOptions.notebook) bridge.onCreateItem("notebook");
|
||||
if (cliOptions.reminder) bridge.onCreateItem("reminder");
|
||||
bringToFront();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (globalThis.window === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
function createURL(options: CLIOptions, path = "/") {
|
||||
const url = new URL(isDevelopment() ? "http://localhost:3000" : PROTOCOL_URL);
|
||||
|
||||
url.pathname = path;
|
||||
if (options.note === true) url.hash = "/notes/create/1";
|
||||
else if (options.notebook === true) url.hash = "/notebooks/create";
|
||||
else if (options.reminder === true) url.hash = "/reminders/create";
|
||||
else if (typeof options.note === "string")
|
||||
url.hash = `/notes/${options.note}/edit`;
|
||||
else if (typeof options.notebook === "string")
|
||||
url.hash = `/notebooks/${options.notebook}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
onMessage: (callback) =>
|
||||
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
|
||||
};
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto =
|
||||
process.platform === "win32" &&
|
||||
process.arch !== "x64" &&
|
||||
process.arch !== "ia32"
|
||||
? undefined
|
||||
: require("@notesnook/crypto").NNCrypto;
|
||||
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
@@ -1,117 +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 { NativeImage, nativeImage } from "electron";
|
||||
import path from "path";
|
||||
import { isDevelopment } from "./index";
|
||||
import { parse, ParsedImage } from "icojs";
|
||||
import { getSystemTheme } from "./theme";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
type Formats = "ico" | "png" | "icns";
|
||||
type IconOptions<TFormat extends Formats> = {
|
||||
size?: 16 | 22 | 24 | 32 | 48 | 64 | 128 | 256 | 512 | 1024;
|
||||
format?: TFormat;
|
||||
};
|
||||
|
||||
type IconNames = (typeof icons)[number];
|
||||
type Prefixes = (typeof prefixes)[number];
|
||||
|
||||
type FlexibleIcon<TFormat extends Formats> = TFormat extends "ico"
|
||||
? string
|
||||
: NativeImage;
|
||||
|
||||
const RESOURCES_DIR = isDevelopment()
|
||||
? process.cwd()
|
||||
: process.platform === "darwin"
|
||||
? path.normalize(path.join(path.dirname(process.execPath), "..", "Resources"))
|
||||
: path.join(path.dirname(process.execPath), "resources");
|
||||
|
||||
const prefixes = ["", ".dark"];
|
||||
const icons = [
|
||||
"note-add",
|
||||
"notebook-add",
|
||||
"reminder-add",
|
||||
"quit",
|
||||
"tray-icon"
|
||||
] as const;
|
||||
|
||||
const ALL_ICONS: {
|
||||
id: IconNames;
|
||||
prefix: Prefixes;
|
||||
images: ParsedImage[];
|
||||
}[] = [];
|
||||
|
||||
export class AssetManager {
|
||||
static async loadIcons() {
|
||||
if (ALL_ICONS.length) return;
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
for (const icon of icons) {
|
||||
const icoPath = path.join(
|
||||
RESOURCES_DIR,
|
||||
"assets",
|
||||
"icons",
|
||||
`${icon}${prefix}.ico`
|
||||
);
|
||||
const icoBuffer = await readFile(icoPath);
|
||||
const images = await parse(icoBuffer, "image/png");
|
||||
ALL_ICONS.push({ id: icon, images, prefix });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static appIcon(options: IconOptions<Formats>) {
|
||||
const { size = 32, format = "png" } = options;
|
||||
|
||||
if (format === "ico") return "assets\\icons\\app.ico";
|
||||
if (format === "icns") return "assets/icons/app.icns";
|
||||
|
||||
return `assets/icons/${size}x${size}.png`;
|
||||
}
|
||||
|
||||
static icon<TFormat extends Formats>(
|
||||
name: IconNames,
|
||||
options: IconOptions<TFormat>
|
||||
): FlexibleIcon<TFormat> {
|
||||
const { size = 16, format = "png" } = options;
|
||||
|
||||
const prefix: Prefixes = getSystemTheme() === "dark" ? ".dark" : "";
|
||||
|
||||
const icoPath = path.join(
|
||||
RESOURCES_DIR,
|
||||
"assets",
|
||||
"icons",
|
||||
`${name}${prefix}.ico`
|
||||
);
|
||||
if (format === "ico") return icoPath as FlexibleIcon<TFormat>;
|
||||
|
||||
const icon = ALL_ICONS.find((a) => a.id === name && a.prefix === prefix);
|
||||
if (!icon)
|
||||
return nativeImage.createFromPath(
|
||||
AssetManager.appIcon(options)
|
||||
) as FlexibleIcon<TFormat>;
|
||||
|
||||
return nativeImage.createFromBuffer(
|
||||
Buffer.from(
|
||||
(icon.images.find((i) => i.height === size) || icon.images[0]).buffer
|
||||
)
|
||||
) as FlexibleIcon<TFormat>;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { app } from "electron";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const LINUX_DESKTOP_ENTRY = (hidden: boolean) => `[Desktop Entry]
|
||||
Type=Application
|
||||
Version=${app.getVersion()}
|
||||
Name=${app.getName()}
|
||||
Comment=${app.getName()} startup script
|
||||
Exec=${process.execPath}${hidden ? " --hidden" : ""}
|
||||
StartupNotify=false
|
||||
Terminal=false`;
|
||||
|
||||
const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
|
||||
app.getPath("home"),
|
||||
".config",
|
||||
"autostart"
|
||||
);
|
||||
|
||||
const STARTUP_ARGS = ["--hidden"];
|
||||
|
||||
export class AutoLaunch {
|
||||
static enable(hidden: boolean) {
|
||||
if (process.platform === "linux") {
|
||||
mkdirSync(LINUX_AUTOSTART_DIRECTORY_PATH, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(
|
||||
LINUX_AUTOSTART_DIRECTORY_PATH,
|
||||
`${app.getName().toLowerCase()}.desktop`
|
||||
),
|
||||
LINUX_DESKTOP_ENTRY(hidden)
|
||||
);
|
||||
} else {
|
||||
const loginItemSettings = app.getLoginItemSettings({
|
||||
args: STARTUP_ARGS
|
||||
});
|
||||
if (loginItemSettings.openAtLogin) return;
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
openAsHidden: hidden,
|
||||
args: STARTUP_ARGS
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static disable() {
|
||||
if (process.platform === "linux") {
|
||||
const desktopFilePath = path.join(
|
||||
LINUX_AUTOSTART_DIRECTORY_PATH,
|
||||
`${app.getName().toLowerCase()}.desktop`
|
||||
);
|
||||
if (!existsSync(desktopFilePath)) return;
|
||||
rmSync(desktopFilePath);
|
||||
} else {
|
||||
app.setLoginItemSettings({ openAtLogin: false, openAsHidden: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { nativeTheme } from "electron";
|
||||
import { JSONStorage } from "./json-storage";
|
||||
import { z } from "zod";
|
||||
|
||||
export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
startMinimized: z.boolean().optional(),
|
||||
minimizeToSystemTray: z.boolean().optional(),
|
||||
closeToSystemTray: z.boolean().optional()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
|
||||
export const config = {
|
||||
desktopSettings: <DesktopIntegration>{
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: true,
|
||||
zoomFactor: 1,
|
||||
theme: nativeTheme.themeSource,
|
||||
automaticUpdates: true
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
for (const key in config) {
|
||||
const defaultValue = config[<ConfigKey>key];
|
||||
if (Object.hasOwn(config, key)) {
|
||||
Object.defineProperty(config, key, {
|
||||
get: () => JSONStorage.get(key, defaultValue),
|
||||
set: (value) => JSONStorage.set(key, value)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { app } from "electron";
|
||||
import { DesktopIntegration, config } from "./config";
|
||||
import { setupTray, destroyTray } from "./tray";
|
||||
import { AutoLaunch } from "./autolaunch";
|
||||
|
||||
export function setupDesktopIntegration(
|
||||
desktopIntegration: DesktopIntegration
|
||||
) {
|
||||
if (
|
||||
desktopIntegration.closeToSystemTray ||
|
||||
desktopIntegration.minimizeToSystemTray
|
||||
) {
|
||||
setupTray();
|
||||
} else {
|
||||
destroyTray();
|
||||
}
|
||||
|
||||
// when close to system tray is enabled, it becomes nigh impossible
|
||||
// to "quit" the app. This is necessary in order to fix that.
|
||||
app.on("before-quit", () =>
|
||||
desktopIntegration.closeToSystemTray ? app.exit(0) : null
|
||||
);
|
||||
|
||||
globalThis.window?.on("close", (e) => {
|
||||
if (config.desktopSettings.closeToSystemTray) {
|
||||
e.preventDefault();
|
||||
if (process.platform == "darwin") {
|
||||
// on macOS window cannot be minimized/hidden if it is already fullscreen
|
||||
// so we just close it.
|
||||
if (globalThis.window?.isFullScreen()) app.exit(0);
|
||||
else app.hide();
|
||||
} else {
|
||||
try {
|
||||
globalThis.window?.minimize();
|
||||
globalThis.window?.hide();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.window?.on("minimize", () => {
|
||||
if (config.desktopSettings.minimizeToSystemTray) {
|
||||
if (process.platform == "darwin") {
|
||||
app.hide();
|
||||
} else {
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (desktopIntegration.autoStart) {
|
||||
AutoLaunch.enable(!!desktopIntegration.startMinimized);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +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, Menu } from "electron";
|
||||
import { AssetManager } from "./asset-manager";
|
||||
import { bringToFront } from "./bring-to-front";
|
||||
import { bridge } from "../api/bridge";
|
||||
|
||||
export function setupJumplist() {
|
||||
if (process.platform === "win32") {
|
||||
setJumplistOnWindows();
|
||||
} else if (process.platform === "darwin") {
|
||||
setDockMenuOnMacOs();
|
||||
}
|
||||
}
|
||||
|
||||
function setJumplistOnWindows() {
|
||||
app.setJumpList([
|
||||
{
|
||||
type: "custom",
|
||||
name: "Quick actions",
|
||||
items: [
|
||||
{
|
||||
program: process.execPath,
|
||||
iconIndex: 0,
|
||||
iconPath: AssetManager.icon("note-add", { format: "ico" }),
|
||||
args: "new note",
|
||||
description: "Create a new note",
|
||||
title: "New note",
|
||||
type: "task"
|
||||
},
|
||||
{
|
||||
program: process.execPath,
|
||||
iconIndex: 0,
|
||||
iconPath: AssetManager.icon("notebook-add", { format: "ico" }),
|
||||
args: "new notebook",
|
||||
description: "Create a new notebook",
|
||||
title: "New notebook",
|
||||
type: "task"
|
||||
},
|
||||
{
|
||||
program: process.execPath,
|
||||
iconIndex: 0,
|
||||
iconPath: AssetManager.icon("reminder-add", { format: "ico" }),
|
||||
args: "new reminder",
|
||||
description: "Add a new reminder",
|
||||
title: "New reminder",
|
||||
type: "task"
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
function setDockMenuOnMacOs() {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "New note",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("note");
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "New notebook",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("notebook");
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "New reminder",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("reminder");
|
||||
}
|
||||
}
|
||||
]);
|
||||
app.dock.setMenu(contextMenu);
|
||||
}
|
||||
@@ -1,97 +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 { protocol } from "electron";
|
||||
import { isDevelopment } from "./index";
|
||||
import { createReadStream } from "fs";
|
||||
import { extname, normalize } from "path";
|
||||
import { URL } from "url";
|
||||
|
||||
const BASE_PATH = isDevelopment() ? "../public" : "";
|
||||
const HOSTNAME = `app.notesnook.com`;
|
||||
const SCHEME = "https";
|
||||
const extensionToMimeType: Record<string, string> = {
|
||||
html: "text/html",
|
||||
json: "application/json",
|
||||
js: "application/javascript",
|
||||
css: "text/css",
|
||||
svg: "image/svg+xml",
|
||||
png: "image/png",
|
||||
jpg: "image/jpg",
|
||||
ttf: "font/ttf",
|
||||
woff: "font/woff",
|
||||
woff2: "font/woff2"
|
||||
};
|
||||
|
||||
function registerProtocol() {
|
||||
protocol.handle(SCHEME, async (request) => {
|
||||
const url = new URL(request.url);
|
||||
if (shouldInterceptRequest(url)) {
|
||||
console.info("Intercepting request:", request.url);
|
||||
const loadIndex = !extname(url.pathname);
|
||||
const filePath = normalize(
|
||||
`${__dirname}${
|
||||
loadIndex ? `${BASE_PATH}/index.html` : `${BASE_PATH}/${url.pathname}`
|
||||
}`
|
||||
);
|
||||
if (!filePath) {
|
||||
console.error("Local asset file not found at", filePath);
|
||||
return new Response(undefined, {
|
||||
status: 404,
|
||||
statusText: "FILE_NOT_FOUND"
|
||||
});
|
||||
}
|
||||
const fileExtension = extname(filePath).replace(".", "");
|
||||
return new Response(createReadStream(filePath), {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
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"`);
|
||||
}
|
||||
|
||||
const bypassedRoutes: string[] = [];
|
||||
function shouldInterceptRequest(url: URL) {
|
||||
const shouldIntercept = url.hostname === HOSTNAME;
|
||||
return shouldIntercept && !bypassedRoutes.includes(url.pathname);
|
||||
}
|
||||
|
||||
const PROTOCOL_URL = `${SCHEME}://${HOSTNAME}/`;
|
||||
export { registerProtocol, PROTOCOL_URL };
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { nativeTheme } from "electron";
|
||||
import { config } from "./config";
|
||||
import { z } from "zod";
|
||||
|
||||
export const Theme = z.union([
|
||||
z.literal("system"),
|
||||
z.literal("light"),
|
||||
z.literal("dark")
|
||||
]);
|
||||
|
||||
export type Theme = z.infer<typeof Theme>;
|
||||
|
||||
function getTheme() {
|
||||
return config.theme;
|
||||
}
|
||||
|
||||
function setTheme(theme: Theme) {
|
||||
changeTheme(theme);
|
||||
if (globalThis.window)
|
||||
globalThis.window.setBackgroundColor(getBackgroundColor());
|
||||
config.theme = theme;
|
||||
}
|
||||
|
||||
function getBackgroundColor() {
|
||||
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
const listeners = nativeTheme.rawListeners("updated");
|
||||
nativeTheme.removeAllListeners("updated");
|
||||
|
||||
const oldThemeSource = nativeTheme.themeSource;
|
||||
nativeTheme.themeSource = "system";
|
||||
const currentTheme = nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
||||
nativeTheme.themeSource = oldThemeSource;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
1000
|
||||
);
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
export { getTheme, setTheme, getBackgroundColor, getSystemTheme };
|
||||
|
||||
function changeTheme(theme: Theme) {
|
||||
const listeners = nativeTheme.rawListeners("updated");
|
||||
nativeTheme.removeAllListeners("updated");
|
||||
|
||||
nativeTheme.themeSource = theme;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
1000
|
||||
);
|
||||
}
|
||||
@@ -1,91 +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, Menu, Tray } from "electron";
|
||||
import { AssetManager } from "./asset-manager";
|
||||
import { isFlatpak } from "./index";
|
||||
import { bringToFront } from "./bring-to-front";
|
||||
import { bridge } from "../api/bridge";
|
||||
|
||||
let tray: Tray | undefined = undefined;
|
||||
export function destroyTray() {
|
||||
if (tray) tray.destroy();
|
||||
}
|
||||
|
||||
export function setupTray() {
|
||||
if (tray) tray.destroy();
|
||||
|
||||
const trayIconSize =
|
||||
process.platform === "win32" || process.platform === "darwin" ? 16 : 32;
|
||||
|
||||
tray = new Tray(
|
||||
AssetManager.icon("tray-icon", {
|
||||
size: process.platform === "darwin" ? 22 : 32
|
||||
})
|
||||
);
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show app",
|
||||
type: "normal",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("tray-icon", { size: trayIconSize }),
|
||||
click: bringToFront
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "New note",
|
||||
type: "normal",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("note-add", { size: trayIconSize }),
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("note");
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "New notebook",
|
||||
type: "normal",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("notebook-add", { size: trayIconSize }),
|
||||
click: () => {
|
||||
bringToFront();
|
||||
bridge.onCreateItem("notebook");
|
||||
}
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Quit",
|
||||
icon: isFlatpak()
|
||||
? undefined
|
||||
: AssetManager.icon("quit", { size: trayIconSize }),
|
||||
type: "normal",
|
||||
click: () => {
|
||||
app.exit(0);
|
||||
}
|
||||
}
|
||||
]);
|
||||
tray.on("double-click", bringToFront);
|
||||
if (process.platform !== "darwin") tray.on("click", bringToFront);
|
||||
tray.setToolTip("Notesnook");
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"lib": ["ESNext"]
|
||||
},
|
||||
"include": ["src", "global.d.ts"]
|
||||
}
|
||||
@@ -17,6 +17,15 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup npmrc
|
||||
run: |
|
||||
echo "registry=https://npm.pkg.github.com" > .npmrc
|
||||
echo "@streetwriters:registry=https://npm.pkg.github.com" >> .npmrc
|
||||
echo "//npm.pkg.github.com/:_authToken=${{secrets.PACKAGES_TOKEN}}" >> .npmrc
|
||||
|
||||
- name: Setup yarnrc
|
||||
run: echo "\"@streetwriters:registry\" \"https://npm.pkg.github.com\"" > .yarnrc
|
||||
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
@@ -17,6 +17,15 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup npmrc
|
||||
run: |
|
||||
echo "registry=https://npm.pkg.github.com" > .npmrc
|
||||
echo "@streetwriters:registry=https://npm.pkg.github.com" >> .npmrc
|
||||
echo "//npm.pkg.github.com/:_authToken=${{secrets.PACKAGES_TOKEN}}" >> .npmrc
|
||||
|
||||
- name: Setup yarnrc
|
||||
run: echo "\"@streetwriters:registry\" \"https://npm.pkg.github.com\"" > .yarnrc
|
||||
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
5
apps/mobile/.gitignore
vendored
@@ -5,6 +5,7 @@ artifacts/
|
||||
.DS_Store
|
||||
|
||||
native/android/app/src/main/assets/
|
||||
|
||||
*Issues.md
|
||||
build_cache/
|
||||
#
|
||||
@@ -13,9 +14,7 @@ build_cache/
|
||||
.yarnrc.yml
|
||||
.yarn
|
||||
*.log
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
|
||||
|
||||
# Xcode
|
||||
#
|
||||
|
||||
1
apps/mobile/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
## Build instructions
|
||||
|
||||
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||
|
||||
### Setting up the development environment
|
||||
### Setting up development environment
|
||||
|
||||
Requirements:
|
||||
|
||||
@@ -57,7 +57,7 @@ npm install
|
||||
|
||||
### Running the app on Android
|
||||
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
|
||||
|
||||
```bash
|
||||
npm run start:android
|
||||
@@ -78,11 +78,11 @@ npm run start:ios
|
||||
|
||||
## Developer guide
|
||||
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible:
|
||||
We try to keep the stack as lean as possible
|
||||
|
||||
1. React Native
|
||||
2. Typescript/Javascript
|
||||
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible:
|
||||
|
||||
The app codebase is distributed over two primary directories. `native/` and `app/`.
|
||||
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
|
||||
|
||||
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
|
||||
- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
|
||||
- `hooks/`: Hooks for different app logic
|
||||
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `screens`: Navigator screens.
|
||||
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions, etc.
|
||||
- `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions etc.
|
||||
|
||||
There are several other folders at the root:
|
||||
|
||||
@@ -115,11 +115,11 @@ There are several other folders at the root:
|
||||
|
||||
### Running the tests
|
||||
|
||||
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
|
||||
### Android
|
||||
|
||||
To run the tests on Android, you will need to create an emulator device on your system:
|
||||
To run the tests on android, you will need to create an emulator device on your system:
|
||||
|
||||
```
|
||||
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
|
||||
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
|
||||
|
||||
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
|
||||
|
||||
Once you have created an emulator device, build the Android apks:
|
||||
Once you have created an emulator device, build the android apks
|
||||
|
||||
```
|
||||
npm run build:android
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
Finally run the tests
|
||||
|
||||
```
|
||||
npm run test:android
|
||||
@@ -141,9 +141,9 @@ npm run test:android
|
||||
|
||||
### iOS
|
||||
|
||||
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
|
||||
To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
|
||||
|
||||
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
|
||||
First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
|
||||
|
||||
```
|
||||
brew tap wix/brew
|
||||
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
|
||||
npm run build:ios
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
Finally run the tests:
|
||||
|
||||
```
|
||||
npm run test:ios
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
|
||||
import "react-native";
|
||||
import { it } from "@jest/globals";
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from "react-test-renderer";
|
||||
import Heading from "../app/components/ui/typography/heading";
|
||||
|
||||
@@ -16,42 +16,42 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "@azure/core-asynciterator-polyfill";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import Launcher from "./components/launcher";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { View } from "react-native";
|
||||
import { useState } from "react";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
NetInfo.configure({
|
||||
reachabilityUrl: "https://notesnook.com",
|
||||
reachabilityTest: (response) => {
|
||||
if (!response) return false;
|
||||
return response?.status >= 200 && response?.status < 300;
|
||||
}
|
||||
});
|
||||
|
||||
SettingsService.init();
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useAppEvents();
|
||||
const [init, setInit] = useState(false);
|
||||
useEffect(() => {
|
||||
let { appLockMode } = SettingsService.get();
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
useUserStore.getState().setVerifyUser(true);
|
||||
}
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setInit(true);
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
@@ -60,10 +60,7 @@ const App = () => {
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -92,59 +89,10 @@ const App = () => {
|
||||
}}
|
||||
>
|
||||
<ApplicationHolder />
|
||||
{init && <Launcher />}
|
||||
</GestureHandlerRootView>
|
||||
<AppLockedOverlay />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
let currTheme =
|
||||
useThemeStore.getState().colorScheme === "dark"
|
||||
? SettingsService.getProperty("darkTheme")
|
||||
: SettingsService.getProperty("lightTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
state.darkTheme,
|
||||
state.lightTheme
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (!currentTheme) return;
|
||||
themeTrpcClient.updateTheme
|
||||
.query({
|
||||
version: currentTheme.version,
|
||||
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
id: currentTheme.id
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 1000);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
|
||||
return <Element />;
|
||||
};
|
||||
};
|
||||
|
||||
export default withTheme(withErrorBoundry(App, "App"));
|
||||
export default withErrorBoundry(App, "App");
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB |
@@ -22,17 +22,10 @@ import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
@@ -46,27 +39,12 @@ export async function deriveCryptoKey(name, data) {
|
||||
credentials.key,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeIOSKeychain(username, password) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
username,
|
||||
password,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
|
||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
@@ -74,9 +52,6 @@ export async function getCryptoKey(_name) {
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// upgrades ios keychain to use accessGroups
|
||||
// so we have access to keychain in share extension.
|
||||
await upgradeIOSKeychain(credentials.username, credentials.password);
|
||||
return credentials.password;
|
||||
} else {
|
||||
return null;
|
||||
@@ -126,18 +101,6 @@ export async function decrypt(password, data) {
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
@@ -166,24 +129,3 @@ export async function encrypt(password, data) {
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let results = await Sodium.encryptMulti(
|
||||
password,
|
||||
data.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -16,17 +16,37 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
|
||||
import Database from "@notesnook/core/api/index";
|
||||
import { initalize, logger as dbLogger } from "@notesnook/core/logger";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import filesystem from "../filesystem";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import filesystem from "../filesystem";
|
||||
import "./logger";
|
||||
import Storage from "./storage";
|
||||
import Storage, { KV } from "./storage";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
|
||||
database.host(
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
initalize(new KV(LoggerStorage), true);
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
/**
|
||||
* @type {import("@notesnook/core/api/index").default}
|
||||
*/
|
||||
export var db = new Database(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
db.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
@@ -34,11 +54,11 @@ database.host(
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.43.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: "http://192.168.8.101:5264",
|
||||
// AUTH_HOST: "http://192.168.8.101:8264",
|
||||
// SSE_HOST: "http://192.168.8.101:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.8.101:9264",
|
||||
// ISSUES_HOST: "http://192.168.8.101:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
@@ -49,15 +69,13 @@ database.host(
|
||||
}
|
||||
);
|
||||
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
export async function loadDatabase() {
|
||||
// if (!DB) {
|
||||
// let module = await import(/* webpackChunkName: "notes-core" */ 'notes-core/api/index');
|
||||
// DB = module.default;
|
||||
// }
|
||||
// db = new DB(Storage, Platform.OS === 'ios' ? EventSource : AndroidEventSource, filesystem);
|
||||
// if (DOMParser) {
|
||||
// await DOMParser.prepare();
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import { ProcessingModes, MMKVLoader } from "react-native-mmkv-storage";
|
||||
import MMKVStorage, { ProcessingModes } from "react-native-mmkv-storage";
|
||||
|
||||
export const MMKV = new MMKVLoader()
|
||||
export const MMKV = new MMKVStorage.Loader()
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import {
|
||||
decrypt,
|
||||
deriveCryptoKey,
|
||||
@@ -27,16 +27,11 @@ import {
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey,
|
||||
decryptMulti,
|
||||
encryptMulti
|
||||
removeCryptoKey
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
export class KV {
|
||||
/**
|
||||
* @type {typeof MMKV}
|
||||
*/
|
||||
storage = null;
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
@@ -58,7 +53,6 @@ export class KV {
|
||||
key,
|
||||
typeof data === "string" ? data : JSON.stringify(data)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -66,36 +60,27 @@ export class KV {
|
||||
if (keys.length <= 0) {
|
||||
return [];
|
||||
} else {
|
||||
try {
|
||||
let data = await this.storage.getMultipleItemsAsync(
|
||||
keys.slice(),
|
||||
"string"
|
||||
);
|
||||
return data.map(([key, value]) => {
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(value);
|
||||
} catch (e) {
|
||||
obj = value;
|
||||
}
|
||||
return [key, obj];
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
let data = await this.storage.getMultipleItemsAsync(keys.slice());
|
||||
|
||||
return data.map(([key, value]) => {
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(value);
|
||||
} catch (e) {
|
||||
obj = value;
|
||||
}
|
||||
|
||||
return [key, obj];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
return this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
return this.storage.removeItems(...keys);
|
||||
return await this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return this.storage.clearStore();
|
||||
return await this.storage.clearStore();
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
@@ -111,10 +96,6 @@ export class KV {
|
||||
);
|
||||
return keys;
|
||||
}
|
||||
|
||||
async writeMulti(items) {
|
||||
return this.storage.setMultipleItemsAsync(items, "object");
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultStorage = new KV(MMKV);
|
||||
@@ -148,11 +129,8 @@ export default {
|
||||
remove: (key) => DefaultStorage.remove(key),
|
||||
clear: () => DefaultStorage.clear(),
|
||||
getAllKeys: () => DefaultStorage.getAllKeys(),
|
||||
writeMulti: (items) => DefaultStorage.writeMulti(items),
|
||||
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
|
||||
encrypt,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
getRandomBytes,
|
||||
checkAndCreateDir,
|
||||
requestPermission,
|
||||
@@ -160,6 +138,5 @@ export default {
|
||||
getCryptoKey,
|
||||
removeCryptoKey,
|
||||
hash,
|
||||
generateCryptoKey,
|
||||
encryptMulti
|
||||
generateCryptoKey
|
||||
};
|
||||
|
||||
@@ -1,50 +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 { Dimensions, Platform } from "react-native";
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
* @returns
|
||||
*/
|
||||
export async function compressToBase64(path, type) {
|
||||
const { width, scale } = Dimensions.get("window");
|
||||
const response = await ImageResizer.createResizedImage(
|
||||
path,
|
||||
width * scale,
|
||||
9999,
|
||||
type,
|
||||
80,
|
||||
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);
|
||||
return base64;
|
||||
}
|
||||
@@ -17,178 +17,19 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
||||
import { presentSheet, ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
Fail: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export function downloadAllAttachments() {
|
||||
const attachments = db.attachments.all;
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||
: `${cacheDir}/notesnook-attachments.zip`;
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
let attachment = attachments[i];
|
||||
const hash = attachment.metadata.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Success
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
console.log("Error downloading attachment", e);
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (result?.size) {
|
||||
let sub;
|
||||
try {
|
||||
onProgress?.(0, `Zipping... Please wait`);
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||
);
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
onProgress(1, `Saving zip file... Please wait`);
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
|
||||
onProgress?.(1, `Done`);
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
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(
|
||||
hash,
|
||||
global = true,
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
import { cacheDir } from "./utils";
|
||||
|
||||
export default async function downloadAttachment(hash, global = true) {
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
@@ -196,41 +37,22 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
let folder = {};
|
||||
if (!options.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db.fs.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
|
||||
if (!(await exists(attachment.metadata.hash))) {
|
||||
if (
|
||||
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
return await db.attachments.read(
|
||||
attachment.metadata.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.metadata.filename,
|
||||
attachment.metadata.type
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
let info = {
|
||||
@@ -241,52 +63,46 @@ export default async function downloadAttachment(
|
||||
hash: attachment.metadata.hash,
|
||||
hashType: attachment.metadata.hashType,
|
||||
mime: attachment.metadata.type,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
fileName: attachment.metadata.filename,
|
||||
uri: folder.uri,
|
||||
chunkSize: attachment.chunkSize
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
options.cache ? "cache" : "file"
|
||||
);
|
||||
let fileUri = await Sodium.decryptFile(key, info, "file");
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: attachment.metadata.filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (!options.silent) {
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!isImage(attachment.metadata?.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
if (!options.silent) {
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
fileUri = folder.uri + `/${attachment.metadata.filename}`;
|
||||
}
|
||||
console.log("saved file uri: ", fileUri);
|
||||
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${attachment.metadata.filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
component: (
|
||||
<ShareComponent
|
||||
uri={fileUri}
|
||||
name={attachment.metadata.filename}
|
||||
padding={12}
|
||||
/>
|
||||
)
|
||||
});
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
console.log("download attachment error: ", e);
|
||||
@@ -296,8 +112,5 @@ export default async function downloadAttachment(
|
||||
.catch(console.log);
|
||||
}
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,22 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import hosts from "@notesnook/core/dist/utils/constants";
|
||||
import hosts from "@notesnook/core/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
|
||||
await createCacheDir();
|
||||
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
if (exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -49,7 +46,6 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
|
||||
if (!downloadUrl) throw new Error("Unable to resolve download url");
|
||||
let totalSize = 0;
|
||||
console.log("Download starting");
|
||||
let request = RNFetchBlob.config({
|
||||
path: path,
|
||||
IOSBackgroundTask: true
|
||||
@@ -63,55 +59,44 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
console.log("downloading: ", recieved, total);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
};
|
||||
cancelToken.cancel = request.cancel;
|
||||
let response = await request;
|
||||
await fileCheck(response, totalSize);
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled") {
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log("Download file error:", e, url, headers);
|
||||
console.log("download file error: ", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.user.tokenManager.getAccessToken();
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.user.tokenManager.getAccessToken();
|
||||
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
const contentLength = parseInt(attachmentInfo.headers?.get("content-length"));
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
|
||||
@@ -23,16 +23,14 @@ import {
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64
|
||||
writeEncrypted
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
writeEncrypted,
|
||||
uploadFile: cancelable(uploadFile),
|
||||
downloadFile: cancelable(downloadFile),
|
||||
deleteFile,
|
||||
|
||||
@@ -19,81 +19,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { cacheDir, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
console.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = !attachment.metadata.type
|
||||
? false
|
||||
: /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = !attachment.metadata.type
|
||||
? false
|
||||
: /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
hash: filename
|
||||
},
|
||||
cipherData.outputType === "base64"
|
||||
? isPng || isJpeg
|
||||
? "cache"
|
||||
: "base64"
|
||||
: "text"
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
);
|
||||
console.log("file decrypted...");
|
||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||
const dCachePath = `${cacheDir}/${output}`;
|
||||
output = await compressToBase64(
|
||||
`file://${dCachePath}`,
|
||||
isPng ? "PNG" : "JPEG"
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log("readEncrypted", e);
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
uri: ""
|
||||
});
|
||||
return {
|
||||
hash: hash,
|
||||
type: "xxh64"
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64({ data, key }) {
|
||||
await createCacheDir();
|
||||
export async function writeEncrypted(filename, { data, type, key }) {
|
||||
console.log("file input: ", { type, key });
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
console.log(filepath);
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
|
||||
console.log("encrypted file output: ", output);
|
||||
return {
|
||||
...output,
|
||||
@@ -102,7 +65,6 @@ export async function writeEncryptedBase64({ data, key }) {
|
||||
}
|
||||
|
||||
export async function deleteFile(filename, data) {
|
||||
await createCacheDir();
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
@@ -128,89 +90,19 @@ export async function deleteFile(filename, data) {
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (let file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
}
|
||||
for (let file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
try {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("clearFileStorage", e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCacheDir() {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
console.log("Cache directory created");
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateFilesFromCache() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
console.log("Files migrated already");
|
||||
return;
|
||||
}
|
||||
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(console.log);
|
||||
console.log("Moved", file);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
console.log("migrateFilesFromCache", e);
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.length + totalAbytes;
|
||||
|
||||
const stat = await RNFetchBlob.fs.stat(
|
||||
existsInAppGroup ? appGroupPath : path
|
||||
);
|
||||
|
||||
if (stat.size !== expectedFileSize) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(console.log);
|
||||
return false;
|
||||
}
|
||||
|
||||
exists = true;
|
||||
}
|
||||
let exists = await RNFetchBlob.fs.exists(`${cacheDir}/${filename}`);
|
||||
return exists;
|
||||
}
|
||||
|
||||
@@ -17,21 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { createCacheDir } from "./io";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
console.log("uploading file: ", filename, headers);
|
||||
try {
|
||||
let res = await fetch(url, {
|
||||
method: "PUT",
|
||||
@@ -39,22 +34,10 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||
const uploadUrl = await res.text();
|
||||
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
uploadFilePath = appGroupPath;
|
||||
}
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
if (!uploadUrl) throw new Error("Unable to resolve upload url");
|
||||
|
||||
let request = RNFetchBlob.config({
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
IOSBackgroundTask: true
|
||||
})
|
||||
.fetch(
|
||||
"PUT",
|
||||
@@ -62,21 +45,15 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
{
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(uploadFilePath)
|
||||
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
|
||||
)
|
||||
.uploadProgress((sent, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(sent, total, filename, 0, "upload");
|
||||
DatabaseLogger.info(
|
||||
`File upload progress: ${filename}, ${sent}/${total}`
|
||||
);
|
||||
console.log("uploading: ", sent, total);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
};
|
||||
cancelToken.cancel = request.cancel;
|
||||
let response = await request;
|
||||
|
||||
let status = response.info().status;
|
||||
@@ -84,31 +61,17 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
let result = status >= 200 && status < 300 && text.length === 0;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
if (result) {
|
||||
DatabaseLogger.info(
|
||||
`File upload status: ${filename}, ${status}, ${text}`
|
||||
);
|
||||
let attachment = db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (
|
||||
!isImage(attachment.metadata.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
if (!attachment.metadata.type.startsWith("image/")) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
throw new Error(
|
||||
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(response.info())}`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
|
||||
DatabaseLogger.error(e);
|
||||
console.log("upload file: ", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export const cacheDir =
|
||||
Platform.OS == "ios"
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export function getRandomId(prefix) {
|
||||
return Math.random()
|
||||
@@ -69,25 +62,3 @@ export function cancelable(operation) {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (let uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,18 +21,18 @@ import React from "react";
|
||||
import { FlatList, View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
|
||||
export const Announcement = ({ color }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
return !announcement || selectionMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
backgroundColor: colors.bg,
|
||||
width: "100%",
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12,
|
||||
@@ -44,26 +44,28 @@ export const Announcement = ({ color }) => {
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.secondary.background,
|
||||
backgroundColor: colors.nav,
|
||||
paddingBottom: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 12
|
||||
}}
|
||||
>
|
||||
{announcement?.body
|
||||
.filter((item) => allowedOnPlatform(item.platforms))
|
||||
.map((item, index) =>
|
||||
<View>
|
||||
<FlatList
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 12
|
||||
}}
|
||||
data={announcement?.body.filter((item) =>
|
||||
allowedOnPlatform(item.platforms)
|
||||
)}
|
||||
renderItem={({ item, index }) =>
|
||||
renderItem({
|
||||
item: item,
|
||||
index: index,
|
||||
color: colors[color],
|
||||
inline: true
|
||||
})
|
||||
)}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Linking, View } from "react-native";
|
||||
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
@@ -33,7 +33,7 @@ import { Button } from "../ui/button";
|
||||
import { allowedOnPlatform, getStyle } from "./functions";
|
||||
|
||||
export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
let buttons =
|
||||
actions.filter((item) => allowedOnPlatform(item.platforms)) || [];
|
||||
|
||||
@@ -92,7 +92,8 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
style={{
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
paddingHorizontal: 0,
|
||||
marginTop: -6
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -110,6 +111,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
marginTop: -6,
|
||||
marginLeft: 12
|
||||
}}
|
||||
textStyle={{
|
||||
@@ -127,11 +129,9 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
title={item.title}
|
||||
fontSize={SIZE.md}
|
||||
buttonType={{
|
||||
color: color ? color : colors.primary.accent,
|
||||
text: color
|
||||
? colors.static.white
|
||||
: colors.primary.accentForeground,
|
||||
selected: color ? color : colors.primary.accent,
|
||||
color: color ? color : colors.accent,
|
||||
text: colors.light,
|
||||
selected: color ? color : colors.accent,
|
||||
opacity: 1
|
||||
}}
|
||||
onPress={() => onPress(item)}
|
||||
@@ -148,7 +148,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
type="gray"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import {
|
||||
eCloseAnnouncementDialog,
|
||||
eOpenAnnouncementDialog
|
||||
@@ -34,69 +34,10 @@ import BaseDialog from "../dialog/base-dialog";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { useCallback } from "react";
|
||||
|
||||
/**
|
||||
* Test announcement
|
||||
* {
|
||||
id: "some-announcement",
|
||||
type: "dialog",
|
||||
body: [
|
||||
{
|
||||
type: "title",
|
||||
text: "This is a title",
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "Most of you are too busy to keep up to date with what's happening in Notesnook. That is unfortunate because Notesnook has come a looooong way.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "To solve this, we are launching the Notesnook Digest — a newsletter to help you stay updated about Notesnook development. And to keep things interesting I'll also sprinkle this newsletter with other interesting stuff like privacy tips & news, interesting books, things I am looking forward to etc.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "So be sure to subscribe. There won't be a proper schedule to this (yet) maybe once or twice a month. I promise no spam — only more awesomeness.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "— May privacy reign.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "callToActions",
|
||||
actions: [
|
||||
{
|
||||
type: "promo",
|
||||
title: "15% Off",
|
||||
platforms: ["android"],
|
||||
data: "com.streetwriters.notesnook.sub.yr.15"
|
||||
}
|
||||
],
|
||||
platforms: ["all"]
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
export const AnnouncementDialog = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [info, setInfo] = useState();
|
||||
const [info, setInfo] = useState(null);
|
||||
const remove = useMessageStore((state) => state.remove);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -110,9 +51,7 @@ export const AnnouncementDialog = () => {
|
||||
|
||||
const open = (data) => {
|
||||
setInfo(data);
|
||||
setImmediate(() => {
|
||||
setVisible(true);
|
||||
});
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const close = useCallback(() => {
|
||||
@@ -134,7 +73,7 @@ export const AnnouncementDialog = () => {
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? 600 : "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
backgroundColor: colors.bg,
|
||||
maxHeight: DDS.isTab ? "90%" : "100%",
|
||||
borderRadius: DDS.isTab ? 10 : 0,
|
||||
overflow: "hidden",
|
||||
|
||||
@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
right: 0
|
||||
}}
|
||||
iconSize={24}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={SIZE.xs + 1}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingVertical: 0,
|
||||
|
||||
@@ -1,215 +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 { 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;
|
||||
@@ -33,7 +33,8 @@ import {
|
||||
} from "../../services/event-manager";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { formatBytes } from "../../utils";
|
||||
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
@@ -46,10 +47,9 @@ import { Notice } from "../ui/notice";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
|
||||
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const contextId = attachment.metadata.hash;
|
||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||
const [currentProgress] = useAttachmentProgress(attachment);
|
||||
@@ -64,7 +64,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs.cancel(attachment.metadata.hash);
|
||||
await db.fs.cancel(attachment.metadata.hash, "download");
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
}
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
@@ -102,22 +102,16 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
ToastEvent.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
setFailed(null);
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
|
||||
setAttachments();
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
setAttachments([...db.attachments.all]);
|
||||
setLoading({
|
||||
name: null
|
||||
});
|
||||
@@ -140,7 +134,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
@@ -152,8 +146,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
await db.attachments.remove(attachment.metadata.hash, false);
|
||||
setAttachments();
|
||||
close();
|
||||
setAttachments([...db.attachments.all]);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
@@ -194,7 +188,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
borderBottomColor: colors.nav,
|
||||
marginBottom: notes && notes.length > 0 ? 0 : 12
|
||||
}}
|
||||
>
|
||||
@@ -215,11 +209,11 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
size={SIZE.xs + 1}
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
color={colors.icon}
|
||||
>
|
||||
{attachment.metadata.type}
|
||||
</Paragraph>
|
||||
@@ -227,8 +221,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
>
|
||||
{formatBytes(attachment.length)}
|
||||
</Paragraph>
|
||||
@@ -238,8 +232,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
>
|
||||
{attachment.noteIds.length} note
|
||||
{attachment.noteIds.length > 1 ? "s" : ""}
|
||||
@@ -254,8 +248,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.icon}
|
||||
>
|
||||
{attachment.metadata.hash}
|
||||
</Paragraph>
|
||||
@@ -268,7 +262,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
borderBottomColor: colors.nav,
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
@@ -310,7 +304,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
}}
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
|
||||
</PressableButton>
|
||||
))}
|
||||
</>
|
||||
@@ -322,10 +316,10 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
key={item.name}
|
||||
buttonType={{
|
||||
text: item.on
|
||||
? colors.primary.accent
|
||||
? colors.accent
|
||||
: item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
? colors.errorText
|
||||
: colors.pri
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
title={item.name}
|
||||
@@ -362,13 +356,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
Actions.present = (attachment, set, context) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close) => (
|
||||
<Actions
|
||||
fwdRef={ref}
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
/>
|
||||
component: (ref) => (
|
||||
<Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,14 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { 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 { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { formatBytes } from "../../utils";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -35,26 +37,20 @@ function getFileExtension(filename) {
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
|
||||
export const AttachmentItem = ({
|
||||
attachment,
|
||||
encryption,
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
encryption
|
||||
);
|
||||
const onPress = () => {
|
||||
if (!pressable) return;
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
const encryptionProgress = useAttachmentStore(
|
||||
(state) => state.encryptionProgress
|
||||
);
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
const onPress = () => {
|
||||
Actions.present(attachment, setAttachments, attachment.metadata.hash);
|
||||
};
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -65,10 +61,11 @@ export const AttachmentItem = ({
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background
|
||||
backgroundColor: colors.nav
|
||||
}}
|
||||
type="grayBg"
|
||||
>
|
||||
<SheetProvider context={attachment.metadata.hash} />
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
@@ -83,12 +80,12 @@ export const AttachmentItem = ({
|
||||
marginLeft: -5
|
||||
}}
|
||||
>
|
||||
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
||||
<Icon name="file" size={SIZE.xxxl} color={colors.icon} />
|
||||
|
||||
<Paragraph
|
||||
adjustsFontSizeToFit
|
||||
size={6}
|
||||
color={colors.static.white}
|
||||
color={colors.light}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
@@ -111,27 +108,25 @@ export const AttachmentItem = ({
|
||||
}}
|
||||
numberOfLines={1}
|
||||
lineBreakMode="middle"
|
||||
color={colors.primary.paragraph}
|
||||
color={colors.pri}
|
||||
>
|
||||
{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}
|
||||
<Paragraph color={colors.icon} size={SIZE.xs}>
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentProgress ? (
|
||||
{currentProgress || encryptionProgress || encryption ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
if (encryption) return;
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
@@ -144,12 +139,18 @@ export const AttachmentItem = ({
|
||||
>
|
||||
<ProgressCircleComponent
|
||||
size={SIZE.xxl}
|
||||
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
|
||||
progress={
|
||||
encryptionProgress
|
||||
? encryptionProgress
|
||||
: currentProgress?.value
|
||||
? currentProgress?.value / 100
|
||||
: 0
|
||||
}
|
||||
showsText
|
||||
textStyle={{
|
||||
fontSize: 10
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
color={colors.accent}
|
||||
formatText={(progress) => (progress * 100).toFixed(0)}
|
||||
borderWidth={0}
|
||||
thickness={2}
|
||||
@@ -161,7 +162,7 @@ export const AttachmentItem = ({
|
||||
<IconButton
|
||||
onPress={onPress}
|
||||
name="alert-circle-outline"
|
||||
color={colors.error.paragraph}
|
||||
color={colors.errorText}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { 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;
|
||||
@@ -17,317 +17,194 @@ 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 React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import {
|
||||
eCloseAttachmentDialog,
|
||||
eOpenAttachmentsDialog
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Toast } from "../toast";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import SheetWrapper from "../ui/sheet";
|
||||
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 || [])]
|
||||
);
|
||||
|
||||
export const AttachmentDialog = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
const actionSheetRef = useRef();
|
||||
const [attachments, setAttachments] = useState([]);
|
||||
const attachmentSearchValue = useRef();
|
||||
const searchTimer = useRef();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenAttachmentsDialog, open);
|
||||
eSubscribeEvent(eCloseAttachmentDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
|
||||
eUnSubscribeEvent(eCloseAttachmentDialog, close);
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
const open = (data) => {
|
||||
if (data?.id) {
|
||||
setNote(data);
|
||||
let _attachments = db.attachments.ofNote(data.id, "all");
|
||||
setAttachments(_attachments);
|
||||
} else {
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
actionSheetRef.current?.show();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const close = () => {
|
||||
actionSheetRef.current?.hide();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onChangeText = (text) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => {
|
||||
let results = db.lookup.attachments(
|
||||
attachments,
|
||||
db.attachments.all,
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
|
||||
setAttachments(filterAttachments(currentFilter, results));
|
||||
setAttachments(results);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={() => {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}}
|
||||
attachment={item}
|
||||
context="attachments-list"
|
||||
/>
|
||||
<AttachmentItem setAttachments={setAttachments} attachment={item} />
|
||||
);
|
||||
|
||||
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 || 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
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
onClose={async () => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<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})`
|
||||
<DialogHeader
|
||||
title={note ? "Attachments" : "Manage attachments"}
|
||||
paragraph="Tap on an attachment to view properties"
|
||||
button={{
|
||||
title: "Check all",
|
||||
type: "grayAccent",
|
||||
loading: loading,
|
||||
onPress: async () => {
|
||||
setLoading(true);
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
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
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
{!note ? (
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<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}
|
||||
<FlatList
|
||||
nestedScrollEnabled
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
data={attachments}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,16 +17,14 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
@@ -45,7 +43,7 @@ export const AuthMode = {
|
||||
};
|
||||
|
||||
const AuthModal = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
@@ -69,9 +67,6 @@ const AuthModal = () => {
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
actionSheetRef.current?.hide();
|
||||
setCurrentAuthMode(AuthMode.login);
|
||||
setVisible(false);
|
||||
@@ -81,96 +76,73 @@ const AuthModal = () => {
|
||||
<BaseDialog
|
||||
overlayOpacity={0}
|
||||
statusBarTranslucent={false}
|
||||
onShow={() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}}
|
||||
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
|
||||
visible={true}
|
||||
onClose={close}
|
||||
useSafeArea={false}
|
||||
bounce={false}
|
||||
background={colors.primary.background}
|
||||
background={colors.bg}
|
||||
transparent={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
top: 0,
|
||||
top: Platform.OS === "ios" ? insets.top : 0,
|
||||
zIndex: 999,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.pri}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="gray"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title="Skip for now"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={20}
|
||||
type="gray"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Toast context="local" />
|
||||
|
||||
@@ -24,7 +24,7 @@ import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
@@ -34,7 +34,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
const [error, setError] = useState(false);
|
||||
@@ -117,7 +117,7 @@ export const ForgotPassword = () => {
|
||||
width: null,
|
||||
height: null
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
color={colors.accent}
|
||||
name="email"
|
||||
size={50}
|
||||
/>
|
||||
@@ -134,7 +134,7 @@ export const ForgotPassword = () => {
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
padding: 12
|
||||
|
||||
@@ -65,7 +65,7 @@ const Auth = ({ navigation, route }) => {
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
color={colors.pri}
|
||||
customStyle={{
|
||||
position: 'absolute',
|
||||
zIndex: 999,
|
||||
|
||||
@@ -18,26 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SVG } from "./background";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import Sync from "../../services/sync";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -46,7 +46,7 @@ const LoginSteps = {
|
||||
};
|
||||
|
||||
export const Login = ({ changeMode }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const {
|
||||
step,
|
||||
@@ -61,17 +61,11 @@ export const Login = ({ changeMode }) => {
|
||||
login
|
||||
} = useLogin(async () => {
|
||||
hideAuth();
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
eSendEvent("userLoggedIn", true);
|
||||
await sleep(500);
|
||||
Progress.present();
|
||||
setTimeout(() => {
|
||||
if (!useUserStore.getState().syncing) {
|
||||
Sync.run("global", false, true);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { width, height } = useWindowDimensions();
|
||||
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
setStep(LoginSteps.emailAuth);
|
||||
@@ -89,68 +83,64 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
{loading ? (
|
||||
<BaseDialog transparent={true} visible={true} animation="fade" />
|
||||
) : null}
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 1,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
src={SVG(colors.night ? colors.icon : "black")}
|
||||
height={700}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
textAlign: "center"
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
size={30}
|
||||
color={colors.heading}
|
||||
>
|
||||
Login to your {"\n"}account
|
||||
Welcome back!
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
textAlign: "center",
|
||||
marginTop: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
changeMode(1);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{"Don't have an account? Sign up"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
@@ -160,9 +150,10 @@ export const Login = ({ changeMode }) => {
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
padding: 12,
|
||||
backgroundColor: colors.bg,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -181,7 +172,7 @@ export const Login = ({ changeMode }) => {
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
editable={step === LoginSteps.emailAuth}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
@@ -203,7 +194,6 @@ export const Login = ({ changeMode }) => {
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
@@ -215,7 +205,6 @@ export const Login = ({ changeMode }) => {
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
@@ -229,22 +218,25 @@ export const Login = ({ changeMode }) => {
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
marginTop: 25,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
loading={loading}
|
||||
onPress={login}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
title={
|
||||
loading
|
||||
? null
|
||||
: step === LoginSteps.emailAuth
|
||||
? "Login"
|
||||
: "Continue"
|
||||
}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
@@ -256,7 +248,6 @@ export const Login = ({ changeMode }) => {
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
@@ -267,37 +258,9 @@ export const Login = ({ changeMode }) => {
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,23 +17,19 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastEvent,
|
||||
eSendEvent,
|
||||
eSubscribeEvent
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import { setLoginMessage } from "../../services/message";
|
||||
import SettingsService from "../../services/settings";
|
||||
import Sync from "../../services/sync";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -46,13 +42,10 @@ import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
const [username, provider] = email.split("@");
|
||||
if (username.length === 1) return `****@${provider}`;
|
||||
return email.replace(/(.{1})(.*)(?=@)/, function (gp1, gp2, gp3) {
|
||||
if (!email) return null;
|
||||
return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) {
|
||||
for (let i = 0; i < gp3.length; i++) {
|
||||
gp2 += "*";
|
||||
}
|
||||
@@ -61,28 +54,23 @@ function getObfuscatedEmail(email) {
|
||||
}
|
||||
|
||||
export const SessionExpired = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const { step, password, email, passwordInputRef, loading, login } = useLogin(
|
||||
() => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
eSendEvent("userLoggedIn", true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
},
|
||||
true
|
||||
}
|
||||
);
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await db.user.logout();
|
||||
await BiometricService.resetCredentials();
|
||||
setLoginMessage();
|
||||
SettingsService.resetSettings();
|
||||
useUserStore.getState().setUser(null);
|
||||
useUserStore.getState().setSyncing(false);
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
setVisible(false);
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
@@ -94,9 +82,10 @@ export const SessionExpired = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const sub = eSubscribeEvent(eLoginSessionExpired, open);
|
||||
eSubscribeEvent("session_expired", open);
|
||||
return () => {
|
||||
sub.unsubscribe?.();
|
||||
eUnSubscribeEvent("session_expired", open);
|
||||
setFocused(false);
|
||||
};
|
||||
}, [visible, open]);
|
||||
|
||||
@@ -112,7 +101,6 @@ export const SessionExpired = () => {
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
setVisible(true);
|
||||
setFocused(false);
|
||||
return;
|
||||
}
|
||||
SettingsService.set({
|
||||
@@ -125,34 +113,28 @@ export const SessionExpired = () => {
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
setFocused(false);
|
||||
setVisible(true);
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<BaseDialog
|
||||
transparent={false}
|
||||
background={colors.primary.background}
|
||||
bounce={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
<Modal
|
||||
onShow={async () => {
|
||||
await sleep(300);
|
||||
passwordInputRef.current?.focus();
|
||||
setFocused(true);
|
||||
}}
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
>
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
style={{
|
||||
width: focused ? "100%" : "99.9%",
|
||||
padding: 12,
|
||||
justifyContent: "center",
|
||||
flex: 1,
|
||||
backgroundColor: colors.primary.background
|
||||
backgroundColor: colors.bg
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -171,10 +153,10 @@ export const SessionExpired = () => {
|
||||
height: 60
|
||||
}}
|
||||
name="alert"
|
||||
color={colors.error.icon}
|
||||
color={colors.errorText}
|
||||
size={50}
|
||||
/>
|
||||
<Heading size={SIZE.xxxl} color={colors.primary.heading}>
|
||||
<Heading size={SIZE.xxxl} color={colors.heading}>
|
||||
Session expired
|
||||
</Heading>
|
||||
<Paragraph
|
||||
@@ -238,9 +220,7 @@ export const SessionExpired = () => {
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
<Dialog context="session_expiry" />
|
||||
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
</BaseDialog>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,26 +18,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import umami from "../../common/analytics";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SVG } from "./background";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
const passwordInputRef = useRef();
|
||||
@@ -48,8 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { width, height } = useWindowDimensions();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastEvent.show({
|
||||
@@ -67,7 +70,6 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await db.user.signup(email.current.toLowerCase(), password.current);
|
||||
@@ -77,6 +79,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
hideAuth();
|
||||
umami.pageView("/account-created", "/welcome/signup");
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
@@ -96,73 +99,70 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
{loading ? (
|
||||
<BaseDialog transparent={true} visible={true} animation="fade" />
|
||||
) : null}
|
||||
<Animated.View
|
||||
entering={FadeInDown}
|
||||
exiting={FadeOutUp}
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
height: 250,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Create your {"\n"}account
|
||||
</Heading>
|
||||
<SvgView
|
||||
src={SVG(colors.night ? colors.icon : "black")}
|
||||
height={700}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: Dimensions.get("window").height < 700 ? -75 : 15
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={30}
|
||||
color={colors.heading}
|
||||
>
|
||||
Create your account
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
textAlign: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
changeMode(0);
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
Already have an account? Log in
|
||||
</Paragraph>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.primary.background,
|
||||
padding: 12,
|
||||
backgroundColor: colors.bg,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
@@ -222,16 +222,37 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={12}
|
||||
marginBottom={5}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
type="accent"
|
||||
title={loading ? null : "Agree and continue"}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
textAlign: "center",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
alignSelf: "center",
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
color={colors.icon}
|
||||
>
|
||||
By signing up, you agree to our{" "}
|
||||
<Paragraph
|
||||
@@ -242,9 +263,9 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
color={colors.accent}
|
||||
>
|
||||
Terms of Service{" "}
|
||||
terms of service{" "}
|
||||
</Paragraph>
|
||||
and{" "}
|
||||
<Paragraph
|
||||
@@ -255,51 +276,13 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
color={colors.accent}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Paragraph>{" "}
|
||||
You also agree to recieve marketing emails from us which you can
|
||||
opt-out of from app settings.
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
|
||||
Already have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Login
|
||||
</Paragraph>
|
||||
privacy policy.
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
@@ -39,7 +39,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const code = useRef();
|
||||
const [currentMethod, setCurrentMethod] = useState({
|
||||
method: mfaInfo?.primaryMethod,
|
||||
@@ -54,7 +54,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
app: "Enter the 6 digit code from your authenticator app to continue logging in",
|
||||
sms: "Enter the 6 digit code sent to your phone number to continue logging in",
|
||||
email: "Enter the 6 digit code sent to your email to continue logging in",
|
||||
recoveryCode: "Enter the recovery code to continue logging in"
|
||||
recoveryCode: "Enter the 8 digit recovery code to continue logging in"
|
||||
};
|
||||
|
||||
const secondaryMethodsText = {
|
||||
@@ -65,7 +65,9 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
};
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6) return;
|
||||
const length = currentMethod.method === "recoveryCode" ? 8 : 6;
|
||||
|
||||
if (!code.current || code.current.length !== length) return;
|
||||
setLoading(true);
|
||||
inputRef.current?.blur();
|
||||
await onMfaLogin(
|
||||
@@ -157,7 +159,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}}
|
||||
size={50}
|
||||
name="key"
|
||||
color={colors.primary.accent}
|
||||
color={colors.accent}
|
||||
/>
|
||||
<Heading
|
||||
style={{
|
||||
@@ -200,27 +202,22 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
<>
|
||||
<Input
|
||||
placeholder={
|
||||
currentMethod.method === "recoveryCode"
|
||||
? "xxxxx-xxxxx"
|
||||
: "xxxxxx"
|
||||
currentMethod.method === "recoveryCode" ? "xxxxxxxx" : "xxxxxx"
|
||||
}
|
||||
testID={"input.totp"}
|
||||
maxLength={
|
||||
currentMethod.method === "recoveryCode" ? undefined : 6
|
||||
}
|
||||
maxLength={currentMethod.method === "recoveryCode" ? 8 : 6}
|
||||
fwdRef={inputRef}
|
||||
textAlign="center"
|
||||
onChangeText={(value) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
onNext();
|
||||
}}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
height: 60,
|
||||
textAlign: "center",
|
||||
letterSpacing: 10,
|
||||
width: 250
|
||||
width: null
|
||||
}}
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
@@ -282,7 +279,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
marginRight: 10
|
||||
}}
|
||||
size={15}
|
||||
color={colors.primary.accent}
|
||||
color={colors.accent}
|
||||
name={item.icon}
|
||||
/>
|
||||
<View
|
||||
@@ -303,9 +300,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
|
||||
TwoFactorVerification.present = (onMfaLogin, data, context) => {
|
||||
presentSheet({
|
||||
component: () => (
|
||||
<TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />
|
||||
),
|
||||
component: <TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />,
|
||||
context: context || "two_factor_verify",
|
||||
onClose: () => {
|
||||
onMfaLogin();
|
||||
|
||||
@@ -33,7 +33,7 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
export const useLogin = (onFinishLogin) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
@@ -64,12 +64,10 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const login = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
try {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
console.log("email auth", mfaInfo);
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(async (mfa, callback) => {
|
||||
try {
|
||||
@@ -77,7 +75,6 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
@@ -96,18 +93,11 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
}
|
||||
}
|
||||
}, mfaInfo);
|
||||
} else {
|
||||
finishWithError(new Error("Unable to send 2FA code"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
null,
|
||||
sessionExpired
|
||||
);
|
||||
await db.user.authenticatePassword(email.current, password.current);
|
||||
finishLogin();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -17,24 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
|
||||
export const ContainerHeader = (props: PropsWithChildren) => {
|
||||
const { colors } = useThemeColors();
|
||||
export const ContainerHeader = ({ children }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return !selectionMode ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
backgroundColor: colors.bg,
|
||||
width: "100%",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||