Compare commits

..

1 Commits

Author SHA1 Message Date
ammarahm-ed
c52ce1302a web: add initial support for purchasing via IAP 2022-09-24 09:19:26 +05:00
1794 changed files with 207275 additions and 186221 deletions

View File

@@ -14,26 +14,18 @@ const SCOPES = [
"mobile",
"web",
"vericrypt",
"desktop",
"crypto",
"editor",
"logger",
"theme",
"server",
"core",
"fs",
"ui",
"clipper",
"config",
"ci",
"setup",
"docs",
"refactor",
"misc",
"common",
"global",
"docs"
"misc"
];
module.exports = {

View File

@@ -18,10 +18,6 @@ apps/web/public/an.js
# editor
packages/editor/styles/
packages/editor/languages/
# editor mobile
packages/editor-mobile/build.bundle
# snapshots
tap-snapshots
packages/editor-mobile/build.bundle

View File

@@ -2,7 +2,7 @@ const LICENSE = [
"",
"This file is part of the Notesnook project (https://notesnook.com/)",
"",
"Copyright (C) 2023 Streetwriters (Private) Limited",
"Copyright (C) 2022 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",

1
.gitattributes vendored
View File

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

View File

@@ -1,27 +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/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

View File

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

View File

@@ -11,20 +11,31 @@ 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
@@ -39,18 +50,7 @@ jobs:
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.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Build apks for Github release
run: yarn release:android
@@ -63,7 +63,7 @@ jobs:
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
- name: Rename apk files
run: |
@@ -92,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/

View File

@@ -8,7 +8,7 @@ on:
paths:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
- ".github/workflows/core.tests.yml"
pull_request:
types:
- "ready_for_review"
@@ -24,18 +24,30 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: 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
echo "USER_PASSWORD=${{ secrets.USER_PASSWORD }}" >> $GITHUB_ENV
echo "USER_TOTP_SECRET=${{ secrets.USER_TOTP_SECRET }}" >> $GITHUB_ENV
echo "USER_HASHED_PASSWORD=${{ secrets.USER_HASHED_PASSWORD }}" >> $GITHUB_ENV
echo "EMAIL=${{ secrets.USER_EMAIL }}" >> $GITHUB_ENV
echo "PASSWORD='${{ secrets.USER_PASSWORD }}'" >> $GITHUB_ENV
echo "HASHED_PASSWORD=${{ secrets.USER_HASHED_PASSWORD }}" >> $GITHUB_ENV
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Run all @notesnook/core tests

View File

@@ -3,21 +3,11 @@ name: Publish @notesnook/desktop
on:
workflow_dispatch:
inputs:
publish-apple:
release:
type: boolean
required: true
default: true
description: "Publish on Mac App Store?"
publish-snap:
type: boolean
required: true
default: true
description: "Publish on Snapcraft?"
publish-github:
type: boolean
required: true
default: true
description: "Publish on GitHub releases?"
default: false
description: "Release after successful build?"
jobs:
build:
@@ -28,12 +18,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: |
@@ -48,208 +52,66 @@ jobs:
name: build
path: apps/web/build/**/*
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/desktop
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
- name: Package desktop build
run: |
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/web/build/
echo "Build folder archived to ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip"
- name: Upload desktop build
uses: softprops/action-gh-release@v1
if: inputs.publish-github
with:
draft: true
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
build-macos:
name: Build for macOS
publish:
name: Publish
needs: build
runs-on: macos-12
runs-on: ${{ matrix.os }}
# Platforms to build on/for
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
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/
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/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
uses: actions/setup-node@v3
with:
app-id: ${{ steps.app_metadata.outputs.apple_app_id }}
key-id: ${{ secrets.api_key_id }}
issuer-id: ${{ secrets.api_key_issuer_id }}
private-key-raw: ${{ secrets.api_key }}
platform: "MAC_OS"
node-version: 16.x
- name: Download build
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
- name: Build app for Mac App Store
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
env:
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npx nx run release --project @notesnook/desktop -- --variant=mas
npx electron-builder --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npx nx run release --project @notesnook/desktop
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
- 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)
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
build-linux:
name: Build for Linux
needs: build
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
path: ./apps/web/desktop/build
- name: Setup Snapcraft Auth
if: inputs.publish-snap
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v1
if: inputs.publish-snap
if: startsWith(matrix.os, 'ubuntu')
- name: Download build
uses: actions/download-artifact@v3
- name: Prepare for app notarization (macOS)
if: startsWith(matrix.os, 'macos')
# Import Apple API key for app notarization on macOS
run: |
mkdir -p ~/private_keys/
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@master
with:
name: build
path: ./apps/web/build
package_root: ./apps/web/desktop
# GitHub token, automatically provided to the action
# (No need to define this secret in the repo settings)
github_token: ${{ secrets.GITHUB_TOKEN }}
build_script_name: "build"
# macOS code signing certificate
mac_certs: ${{ secrets.mac_certs }}
mac_certs_password: ${{ secrets.mac_certs_password }}
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build snap
if: inputs.publish-snap
run: |
npx electron-builder --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ inputs.release }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
else
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
# macOS notarization API key
API_KEY_ID: ${{ secrets.api_key_id }}
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
- name: Publish on Snapcraft
if: inputs.publish-snap
run: |
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
working-directory: ./apps/desktop
build-windows:
name: Build for Windows
needs: build
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build
run: npx nx run release --project @notesnook/desktop
- name: Publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ($${{ inputs.publish-github }} -eq $true) {
yarn electron-builder --win --publish always
} else {
yarn electron-builder --win --publish never
}
working-directory: ./apps/desktop
if: startsWith(matrix.os, 'ubuntu')
run: snapcraft upload --release=stable ./dist/notesnook_linux_amd64.snap
working-directory: ./apps/web/desktop

View File

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

View File

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

View File

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

View File

@@ -1,31 +0,0 @@
name: Publish @notesnook/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 desktop build
run: npm run build:vericrypt
- name: Publish on Cloudflare Pages
run: npx --yes wrangler pages publish --project-name=vericrypt ./apps/vericrypt/build/

View File

@@ -11,12 +11,26 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |

View File

@@ -25,12 +25,26 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |
@@ -49,7 +63,7 @@ jobs:
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
strategy:
matrix:
shard: [1, 2, 3, 4]
shard: [1, 2, 3]
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -62,12 +76,26 @@ jobs:
path: ./apps/web/build
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm install -D @playwright/test otplib
npm i --ignore-scripts --prefer-offline --no-audit
npm install -D @playwright/test
- name: Validate build extraction
run: ls -ld ./apps/web/build/index.html
@@ -79,9 +107,8 @@ jobs:
- name: Add environment variables
run: |
echo "USER_EMAIL=${{ secrets.USER_EMAIL }}" >> $GITHUB_ENV
echo "USER_PASSWORD=${{ secrets.USER_PASSWORD }}" >> $GITHUB_ENV
echo "USER_TOTP_SECRET=${{ secrets.USER_TOTP_SECRET }}" >> $GITHUB_ENV
echo "USER_KEY=${{ secrets.USER_KEY }}" >> $GITHUB_ENV
echo "CURRENT_USER_PASSWORD=${{ secrets.USER_PASSWORD }}" >> $GITHUB_ENV
echo "CURRENT_USER_KEY=${{ secrets.USER_KEY }}" >> $GITHUB_ENV
- name: Run tests
run: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}

8
.gitignore vendored
View File

@@ -3,10 +3,4 @@ coverage
node_modules
dist
.DS_STORE
nx-cloud.env
.idea
.eslintcache
.env.local
.nyc_output
site
node_modules.backup
nx-cloud.env

View File

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

View File

@@ -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
View File

@@ -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"
}

View File

@@ -1,4 +1,3 @@
Notesnook is written & maintained by:
- Abdullah Atta <abdullahatta@streetwriters.co>
- Ammar Ahmed <ammarahmed6506@gmail.com>
- Muhammad Ali <alihamuh@gmail.com>

View File

@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
Ugh! Bugs!
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
@@ -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 youve fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`npm run test`).
5. Format your code with prettier (`npm run prettier`).
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
@@ -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:
![](resources/vscode-signed-off-commit.png)
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
@@ -115,7 +117,6 @@ All commits must include valid scopes in the commit message. **Valid commit scop
3. `setup`: setting up something new in the repo (e.g. eslint, commitlint)
4. `docs`: changes related to documentation (README etc.)
5. `misc`: miscellaneous changes like package-lock.json updates
6. `global`: changes related to the whole repo
**Some example commits would look like this:**
@@ -131,11 +132,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnbs Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
## Git Branch Organization
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We dont use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
1. All tests must be passing at all times
2. There should be as few breaking changes as possible

View File

@@ -10,11 +10,11 @@
## Overview
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
## Developer guide
@@ -25,30 +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/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 |
| Name | Path | Description |
| -------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
| `@notesnook/web` | [/apps/web](/apps/web) | Web/Desktop clients |
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
### Contributing guidelines

View File

@@ -1,4 +0,0 @@
node_modules
build
output
dist

View File

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

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>53CWBG3QUC.org.streetwriters.notesnook</string>
<key>com.apple.developer.team-identifier</key>
<string>53CWBG3QUC</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>53CWBG3QUC.org.streetwriters.notesnook</string>
</array>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.print</key>
<true/>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/Documents/Notesnook/</string>
</array>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,211 +0,0 @@
{
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.5.8",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"homepage": "https://notesnook.com/",
"repository": "https://github.com/streetwriters/notesnook",
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.31.0",
"@trpc/server": "10.31.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.1",
"icojs": "^0.17.1",
"typed-emitter": "^2.1.0",
"yargs": "^17.6.2",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/node": "^18.15.0",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "24.5.1",
"electron-builder": "24.4.0",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"node-fetch": "^3.3.1",
"tree-kill": "^1.2.2"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
},
"scripts": {
"start": "node scripts/dev.mjs",
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "tsc",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
"postinstall": "patch-package"
},
"author": {
"name": "Streetwriters (Private) Limited",
"email": "support@streetwriters.co",
"url": "https://streetwriters.co"
},
"build": {
"appId": "org.streetwriters.notesnook",
"productName": "Notesnook",
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
"artifactName": "notesnook_${os}_${arch}.${ext}",
"npmRebuild": false,
"asar": false,
"files": [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
"build/",
"!build/screenshots${/*}",
"!build/banner.jpg",
"!build/*.ico",
"!build/*.png",
"!node_modules${/*}"
],
"afterSign": "electron-builder-notarize",
"afterPack": "./scripts/removeLocales.js",
"mac": {
"bundleVersion": "240",
"minimumSystemVersion": "10.12.0",
"target": [
{
"target": "dmg",
"arch": [
"arm64",
"x64"
]
},
{
"target": "zip",
"arch": [
"arm64",
"x64"
]
}
],
"category": "public.app-category.productivity",
"darkModeSupport": true,
"type": "distribution",
"hardenedRuntime": true,
"entitlements": "assets/entitlements.mac.plist",
"entitlementsInherit": "assets/entitlements.mac.plist",
"gatekeeperAssess": false,
"icon": "assets/icons/app.icns"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
],
"icon": "assets/icons/app.icns",
"title": "Install Notesnook"
},
"mas": {
"entitlements": "assets/entitlements.mas.plist",
"entitlementsInherit": "assets/entitlements.mas.inherit.plist",
"entitlementsLoginHelper": "assets/entitlements.mas.loginhelper.plist",
"hardenedRuntime": true
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
},
{
"target": "portable",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "assets/icons/app.ico"
},
"portable": {
"artifactName": "notesnook_${os}_${arch}_portable.${ext}"
},
"nsis": {
"oneClick": true,
"createDesktopShortcut": "always"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
},
{
"target": "snap",
"arch": [
"x64",
"arm64"
]
}
],
"category": "Office",
"icon": "assets/icons/app.icns",
"description": "Your private note taking space",
"executableName": "notesnook",
"desktop": {
"actions": [
{
"id": "new-note",
"name": "New note",
"args": "new note"
},
{
"id": "new-notebook",
"name": "New notebook",
"args": "new notebook"
},
{
"id": "new-reminder",
"name": "New reminder",
"args": "new reminder"
}
]
}
},
"snap": {
"autoStart": false,
"confinement": "strict",
"allowNativeWayland": true
},
"extraResources": [
"./assets/**"
],
"extraMetadata": {
"main": "./build/electron.js"
},
"directories": {
"buildResources": "assets",
"output": "./output/"
},
"publish": [
{
"provider": "github",
"repo": "notesnook",
"owner": "streetwriters"
}
]
}
}

View File

@@ -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 3ba5d6a..cf8e147 100644
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
@@ -105,6 +105,7 @@ class LinuxTargetHelper {
StartupWMClass: appInfo.productName,
...extra,
...targetSpecificOptions.desktop,
+ actions: undefined
};
const description = this.getDescription(targetSpecificOptions);
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
@@ -148,6 +149,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);
}
}

View File

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

View File

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

View File

@@ -1,197 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 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;
}

View File

@@ -1,58 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { EventEmitter } from "events";
import TypedEventEmitter from "typed-emitter";
export type AppEvents = {
onCreateItem(name: "note" | "notebook" | "reminder"): void;
};
const emitter = new EventEmitter();
const typedEmitter = emitter as TypedEventEmitter<AppEvents>;
const t = initTRPC.create();
export const bridgeRouter = t.router({
onCreateItem: createSubscription("onCreateItem")
});
export const bridge: AppEvents = new Proxy({} as AppEvents, {
get(_t, name) {
if (typeof name === "symbol") return;
return (...args: unknown[]) => {
emitter.emit(name, ...args);
};
}
});
function createSubscription<TName extends keyof AppEvents>(eventName: TName) {
return t.procedure.subscription(() => {
return observable<Parameters<AppEvents[TName]>[0]>((emit) => {
const listener: AppEvents[TName] = (...args: any[]) => {
emit.next(args[0]);
};
typedEmitter.addListener(eventName, listener);
return () => {
typedEmitter.removeListener(eventName, listener);
};
});
});
}

View File

@@ -1,39 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import zlib from "node:zlib";
import utils from "node:util";
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
const gzipAsync = utils.promisify(zlib.gzip);
const gunzipAsync = utils.promisify(zlib.gunzip);
export const compressionRouter = t.router({
gzip: t.procedure
.input(z.object({ data: z.string(), level: z.number() }))
.query(async ({ input }) => {
const { data, level } = input;
return (await gzipAsync(data, { level })).toString("base64");
}),
gunzip: t.procedure.input(z.string()).query(async ({ input }) => {
return (await gunzipAsync(Buffer.from(input, "base64"))).toString("utf-8");
})
});

View File

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

View File

@@ -1,160 +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";
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({
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.boolean().optional())
.mutation(({ input: privacyMode }) => {
if (!globalThis.window || !["win32", "darwin"].includes(process.platform))
return;
globalThis.window.setContentProtection(!!privacyMode);
if (process.platform === "win32") {
globalThis.window.setThumbnailClip(
privacyMode
? { x: 0, y: 0, width: 1, height: 1 }
: { x: 0, y: 0, width: 0, height: 0 }
);
}
config.privacyMode = !!privacyMode;
}),
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;
}),
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);
}),
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");
}
});
})
)
});

View File

@@ -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.boolean().optional())
.mutation(({ input: enabled }) => {
globalThis.window?.webContents.session.setSpellCheckerEnabled(!!enabled);
config.isSpellCheckerEnabled = !!enabled;
})
});

View File

@@ -1,81 +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.boolean().optional())
.mutation(({ input }) => {
config.automaticUpdates =
input === undefined ? !config.automaticUpdates : input;
}),
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);
};
});
});
}

View File

@@ -1,84 +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(): Promise<CLIOptions> {
const result: CLIOptions = {
note: false,
notebook: false,
reminder: false,
hidden: false
};
const { hidden } = await yargs(hideBin(process.argv))
.boolean("hidden")
.command("new", "Create a new item", (yargs) => {
return yargs
.command("note", "Create a new note", {}, () => {
result.note = true;
})
.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;
}

View File

@@ -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";

View File

@@ -1,205 +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, session, 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 { AutoLaunch } from "./utils/autolaunch";
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";
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"));
}
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
app.exit();
}
if (process.platform === "win32") {
app.setAppUserModelId(app.name);
}
app.commandLine.appendSwitch("lang", "en-US");
async function createWindow() {
const cliOptions = await parseArguments();
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: true,
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"
}
});
globalThis.window = mainWindow;
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(config.privacyMode);
}
await AssetManager.loadIcons();
setupDesktopIntegration();
createIPCHandler({ router, windows: [mainWindow] });
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("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;
}
function setupDesktopIntegration() {
const desktopIntegration = config.desktopSettings;
if (
desktopIntegration.closeToSystemTray ||
desktopIntegration.minimizeToSystemTray
) {
setupTray();
}
// when close to system tray is enabled, it becomes nigh impossible
// to "quit" the app. This is necessary in order to fix that.
if (desktopIntegration.closeToSystemTray) {
app.on("before-quit", () => app.exit(0));
}
globalThis.window?.once("close", (e) => {
if (config.desktopSettings.closeToSystemTray) {
e.preventDefault();
if (process.platform == "darwin") {
// on macOS window cannot be minimized/hidden if it is already fullscreen
// so we just close it.
if (globalThis.window?.isFullScreen()) app.exit(0);
else app.hide();
} else {
globalThis.window?.minimize();
globalThis.window?.hide();
}
}
});
globalThis.window?.on("minimize", () => {
if (config.desktopSettings.minimizeToSystemTray) {
if (process.platform == "darwin") {
app.hide();
} else {
globalThis.window?.hide();
}
}
});
if (desktopIntegration.autoStart) {
AutoLaunch.enable(!!desktopIntegration.startMinimized);
}
}

View File

@@ -1,51 +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 () => {
console.log("HELLO!");
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());

View File

@@ -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>;
}
}

View File

@@ -1,70 +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"
);
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 {
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: hidden,
args: ["--hidden"]
});
}
}
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 });
}
}
}

View File

@@ -1,36 +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 { autoUpdater } from "electron-updater";
import { config } from "./config";
async function configureAutoUpdater() {
autoUpdater.setFeedURL({
provider: "generic",
url: `https://notesnook.com/releases/${process.platform}/`,
useMultipleRangeRequest: false
});
autoUpdater.autoDownload = config.automaticUpdates;
autoUpdater.allowDowngrade = false;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
}
export { configureAutoUpdater };

View File

@@ -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: false,
zoomFactor: 0,
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)
});
}
}

View File

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

View File

@@ -1,160 +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, ProtocolRequest } from "electron";
import { isDevelopment } from "./index";
import { createReadStream } from "fs";
import { extname, normalize } from "path";
import { URL } from "url";
import fetch, { Response } from "node-fetch";
const BASE_PATH = isDevelopment() ? "../public" : "";
const HOSTNAME = `app.notesnook.com`;
const FILE_NOT_FOUND = -6;
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"
};
function registerProtocol() {
const protocolInterceptionResult = protocol.interceptStreamProtocol(
SCHEME,
async (request, callback) => {
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);
callback({ error: FILE_NOT_FOUND });
return;
}
const fileExtension = extname(filePath).replace(".", "");
const data = createReadStream(filePath);
callback({
data,
mimeType: extensionToMimeType[fileExtension]
});
} else {
let response: Response;
try {
const body = await getBody(request);
response = await fetch(request.url, {
...request,
body,
headers: {
...request.headers
// origin: `${PROTOCOL}://${HOSTNAME}/`
},
referrer: request.referrer,
redirect: "manual"
});
} catch (e) {
console.error(e);
console.error(`Error sending request to `, request.url, "Error: ", e);
callback({ statusCode: 400 });
return;
}
callback({
statusCode: response.status,
data: response.body || undefined,
headers: Object.fromEntries(response.headers.entries()),
mimeType: response.headers.get("Content-Type") || undefined
});
}
}
);
console.info(
`${SCHEME} protocol inteception ${
protocolInterceptionResult ? "successful" : "failed"
}.`
);
// protocol.handle(SCHEME, (request) => {
// const url = new URL(request.url);
// if (shouldInterceptRequest(url)) {
// console.info("Intercepting request:", request.url);
// const loadIndex = !extname(url.pathname);
// const absoluteFilePath = normalize(
// `${__dirname}${
// loadIndex ? `${BASE_PATH}/index.html` : `${BASE_PATH}/${url.pathname}`
// }`
// );
// const filePath = getPath(absoluteFilePath);
// 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(".", "");
// const data = createReadStream(filePath);
// return new Response(data, {
// headers: { "Content-Type": extensionToMimeType[fileExtension] }
// });
// } else {
// return net.fetch(request);
// }
// });
// 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 };
async function getBody(request: ProtocolRequest) {
const session = globalThis?.window?.webContents?.session;
const blobParts = [];
if (!request.uploadData || !request.uploadData.length) return null;
for (const data of request.uploadData) {
if (data.bytes) {
blobParts.push(new Uint8Array(data.bytes));
} else if (session && data.blobUUID) {
const buffer = await session.getBlobData(data.blobUUID);
if (!buffer) continue;
blobParts.push(new Uint8Array(buffer));
}
}
const blob = new Blob(blobParts);
return blob;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,7 @@ build_cache/
.yarnrc.yml
.yarn
*.log
.cxx/
*.keystore
!debug.keystore
# Xcode
#

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -16,37 +16,30 @@ 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 {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import Launcher from "./components/launcher";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import { View } from "react-native";
SettingsService.init();
SettingsService.checkOrientation();
const App = () => {
const init = useAppEvents();
useAppEvents();
useEffect(() => {
let { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().lockApp(true);
useUserStore.getState().setVerifyUser(true);
}
init();
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
@@ -55,10 +48,7 @@ const App = () => {
}
TipManager.init();
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<View
style={{
@@ -87,49 +77,10 @@ const App = () => {
}}
>
<ApplicationHolder />
<Launcher />
</GestureHandlerRootView>
<AppLockedOverlay />
</View>
);
};
export const withTheme = (Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,
state.lightTheme
]);
useEffect(() => {
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);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
}, [colorScheme, darkTheme, lightTheme]);
return <Element />;
};
};
export default withTheme(withErrorBoundry(App, "App"));
export default withErrorBoundry(App, "App");

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -21,18 +21,11 @@ import { Platform } from "react-native";
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";
import Sodium from "react-native-sodium";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
},
android: {}
});
@@ -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;

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -16,7 +16,8 @@ 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 Database from "@notesnook/core/api/index";
import { initalize, logger as dbLogger } from "@notesnook/core/logger";
import { Platform } from "react-native";
import { MMKVLoader } from "react-native-mmkv-storage";
@@ -24,49 +25,51 @@ import filesystem from "../filesystem";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import Storage, { KV } from "./storage";
import * as Gzip from "react-native-gzip";
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
database.host(
__DEV__
? {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: "http://192.168.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",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
}
);
database.setup(
Storage,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem,
{
compress: Gzip.deflate,
decompress: Gzip.inflate
}
);
initalize(new KV(LoggerStorage), true);
export const db = database;
initalize(new KV(LoggerStorage));
export const DatabaseLogger = dbLogger;
/**
* @type {import("@notesnook/core/api/index").default}
*/
export var db = new Database(
Storage,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem
);
db.host(
__DEV__
? {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: 'http://192.168.10.29:5264',
// AUTH_HOST: 'http://192.168.10.29:8264',
// SSE_HOST: 'http://192.168.10.29:7264',
// SUBSCRIPTIONS_HOST: 'http://192.168.10.29:9264',
// ISSUES_HOST: 'http://192.168.10.29:2624'
}
: {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
}
);
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();
// }
}

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -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,

View File

@@ -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;
}

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -17,171 +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/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 "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 { cacheDir } from "./utils";
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
) {
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
}
) {
export default async function downloadAttachment(hash, global = true) {
let attachment = db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
@@ -189,23 +37,16 @@ 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 (
@@ -213,11 +54,6 @@ export default async function downloadAttachment(
)
return;
let filename = getFileNameWithExtension(
attachment.metadata.filename,
attachment.metadata.type
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
@@ -227,51 +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,
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, false);
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);
@@ -281,8 +112,5 @@ export default async function downloadAttachment(
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.metadata.hash);
if (options.throwError) {
throw e;
}
}
}

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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";
@@ -46,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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -17,22 +17,23 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
import {
checkAttachment, downloadFile,
getUploadedFileSize
} from "./download";
import {
clearFileStorage,
deleteFile,
exists,
readEncrypted,
writeEncryptedBase64,
hashBase64
writeEncrypted
} from "./io";
import { uploadFile } from "./upload";
import { cancelable } from "./utils";
export default {
readEncrypted,
writeEncryptedBase64,
hashBase64,
writeEncrypted,
uploadFile: cancelable(uploadFile),
downloadFile: cancelable(downloadFile),
deleteFile,

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -18,83 +18,46 @@ 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 Sodium from "react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function readEncrypted(filename, key, cipherData) {
let path = `${cacheDir}/${filename}`;
try {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists =
(await RNFetchBlob.fs.exists(path)) ||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
let exists = await RNFetchBlob.fs.exists(path);
if (!exists) {
return false;
}
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
let output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
hash: filename
},
cipherData.outputType === "base64"
? isPng || isJpeg
? "cache"
: "base64"
: "text"
true
);
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(e);
console.log("error");
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 }) {
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,

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -17,20 +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/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
let { url, headers } = data;
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
console.log("uploading file: ", filename, headers);
try {
let res = await fetch(url, {
method: "PUT",
@@ -38,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",
@@ -61,17 +45,14 @@ 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 = request.cancel;
let response = await request;
@@ -80,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;
}
}

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -17,9 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import * 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 cacheDir = RNFetchBlob.fs.dirs.CacheDir;
@@ -64,22 +62,3 @@ export function cancelable(operation) {
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
console.log(e, r);
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -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>

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -22,8 +22,11 @@ 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 { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
import { useThemeStore } from "../../stores/use-theme-store";
import {
eCloseAnnouncementDialog,
eCloseProgressDialog
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans";
@@ -33,7 +36,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)) || [];
@@ -57,11 +60,11 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
)
});
} else if (item.type === "force-sync") {
eSendEvent(eCloseSheet);
eSendEvent(eCloseProgressDialog);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
eSendEvent(eCloseSheet);
eSendEvent(eCloseProgressDialog);
});
}
};
@@ -92,7 +95,8 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
style={{
height: 30,
alignSelf: "flex-start",
paddingHorizontal: 0
paddingHorizontal: 0,
marginTop: -6
}}
/>
))}
@@ -110,6 +114,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
marginTop: -6,
marginLeft: 12
}}
textStyle={{
@@ -127,9 +132,9 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
title={item.title}
fontSize={SIZE.md}
buttonType={{
color: color ? color : colors.primary.accent,
text: colors.static.white,
selected: color ? color : colors.primary.accent,
color: color ? color : colors.accent,
text: colors.light,
selected: color ? color : colors.accent,
opacity: 1
}}
onPress={() => onPress(item)}
@@ -146,7 +151,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}

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -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
@@ -35,7 +35,7 @@ import { allowedOnPlatform, renderItem } from "./functions";
import { useCallback } from "react";
export const AnnouncementDialog = () => {
const { colors } = useThemeColors();
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [info, setInfo] = useState(null);
const remove = useMessageStore((state) => state.remove);
@@ -73,7 +73,7 @@ export const AnnouncementDialog = () => {
<View
style={{
width: DDS.isTab ? 600 : "100%",
backgroundColor: colors.primary.background,
backgroundColor: colors.bg,
maxHeight: DDS.isTab ? "90%" : "100%",
borderRadius: DDS.isTab ? 10 : 0,
overflow: "hidden",

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
right: 0
}}
iconSize={24}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
borderRadius: 100,
paddingVertical: 0,

View File

@@ -1,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.bg,
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.bg
}}
>
{user ? (
<>
<Input
fwdRef={passwordInputRef}
secureTextEntry
placeholder="Enter account password"
onChangeText={(v) => (password.current = v)}
onSubmit={onSubmit}
/>
</>
) : null}
<View
style={{
marginTop: user ? 25 : 25
}}
>
{user ? (
<>
<Button
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
height={45}
style={{
borderRadius: 150,
marginBottom: 10
}}
fontSize={SIZE.md}
/>
</>
) : null}
<Button
title="Unlock with Biometrics"
width={250}
onPress={onUnlockAppRequested}
icon={"fingerprint"}
type="transparent"
/>
</View>
</View>
</View>
</View>
)
);
};
export default AppLockedOverlay;

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -33,8 +33,12 @@ import {
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
import { useThemeStore } from "../../stores/use-theme-store";
import { formatBytes } from "../../utils";
import {
eCloseAttachmentDialog,
eCloseProgressDialog
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -46,10 +50,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 }) => {
const { colors } = useThemeColors();
const colors = useThemeStore((state) => state.colors);
const contextId = attachment.metadata.hash;
const [filename, setFilename] = useState(attachment.metadata.filename);
const [currentProgress] = useAttachmentProgress(attachment);
@@ -68,7 +71,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
useAttachmentStore.getState().remove(attachment.metadata.hash);
}
downloadAttachment(attachment.metadata.hash, false);
eSendEvent(eCloseSheet, contextId);
eSendEvent(eCloseProgressDialog, contextId);
},
icon: "download"
},
@@ -102,21 +105,15 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
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"
});
}
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
setAttachments([...db.attachments.all]);
setLoading({
name: null
@@ -153,7 +150,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
onPress: async () => {
await db.attachments.remove(attachment.metadata.hash, false);
setAttachments([...db.attachments.all]);
eSendEvent(eCloseSheet, contextId);
eSendEvent(eCloseProgressDialog, contextId);
},
icon: "delete-outline"
}
@@ -194,7 +191,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.secondary.background,
borderBottomColor: colors.nav,
marginBottom: notes && notes.length > 0 ? 0 : 12
}}
>
@@ -215,11 +212,11 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
>
<Paragraph
size={SIZE.xs}
size={SIZE.xs + 1}
style={{
marginRight: 10
}}
color={colors.secondary.paragraph}
color={colors.icon}
>
{attachment.metadata.type}
</Paragraph>
@@ -227,24 +224,22 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
size={SIZE.xs + 1}
color={colors.icon}
>
{formatBytes(attachment.length)}
</Paragraph>
{attachment.noteIds ? (
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.noteIds.length} note
{attachment.noteIds.length > 1 ? "s" : ""}
</Paragraph>
) : null}
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.noteIds.length} note
{attachment.noteIds.length > 1 ? "s" : ""}
</Paragraph>
<Paragraph
onPress={() => {
Clipboard.setString(attachment.metadata.hash);
@@ -254,8 +249,8 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
context: "local"
});
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.metadata.hash}
</Paragraph>
@@ -268,7 +263,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.secondary.background,
borderBottomColor: colors.nav,
marginBottom: 12,
paddingVertical: 12
}}
@@ -296,7 +291,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
});
return;
}
eSendEvent(eCloseSheet, contextId);
eSendEvent(eCloseProgressDialog, contextId);
await sleep(150);
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
@@ -310,7 +305,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
</PressableButton>
))}
</>
@@ -322,10 +317,10 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
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}

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -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}
</>

View File

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

View File

@@ -1,7 +1,7 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
Copyright (C) 2022 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
@@ -17,63 +17,82 @@ 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/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([...attachments]);
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;
@@ -82,248 +101,110 @@ export const AttachmentDialog = ({ note }) => {
};
const renderItem = ({ item }) => (
<AttachmentItem
setAttachments={setAttachments}
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) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
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.bg
}}
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} />
});
};

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