Compare commits

..

1 Commits

Author SHA1 Message Date
Abdullah Atta
f751db673e web: benchmark different embedding models 2024-06-01 11:14:11 +05:00
1874 changed files with 419032 additions and 1006561 deletions

View File

@@ -10,12 +10,11 @@ const authors = readFileSync("AUTHORS", "utf-8");
const isAuthor = authors.includes(`<${authorEmail}>`);
const SCOPES = [
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
"mobile",
"web",
"vericrypt",
"monograph",
"desktop",
"crypto",
"editor",
@@ -35,9 +34,7 @@ const SCOPES = [
"common",
"global",
"docs",
"themebuilder",
"intl",
"webclipper"
"themebuilder"
];
module.exports = {

View File

@@ -1,6 +1,8 @@
name: Bug Report
description: Are you facing a bug or a crash in Notesnook?
type: Bug
labels: ["Type: Bug", "Status: Pending"]
assignees:
- thecodrr
body:
- type: markdown
attributes:

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Notesnook Discord Community
url: https://go.notesnook.com/discord
url: https://discord.gg/6mHHyncHJE
about: Reach out to us directly & discuss your issues, suggestions & other feedback!

View File

@@ -1,6 +1,6 @@
name: Feature request
description: Suggest a feature you are missing in Notesnook
type: Feature
labels: ["Type: Feature Request", "Status: Pending"]
body:
- type: markdown
attributes:

View File

@@ -7,15 +7,13 @@ runs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 20.x
cache: "npm"
cache-dependency-path: |
package-lock.json
apps/mobile/package-lock.json
apps/desktop/package-lock.json
apps/web/package-lock.json
apps/monograph/package-lock.json
apps/theme-builder/package-lock.json
extensions/web-clipper/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
@@ -26,4 +24,3 @@ runs:
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
packages/intl/package-lock.json

View File

@@ -1,105 +0,0 @@
name: e2e-android
on: workflow_dispatch
jobs:
e2e-android:
runs-on: ubuntu-latest
env:
API_LEVEL: 34
ARCH: x86_64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: wrapper
cache-read-only: false
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Install Detox CLI
run: npm install detox-cli --global
- name: Check for typescript errors
run: |
cd apps/mobile
npx tsc --noEmit
- name: Detox build
run: |
yarn build:android
ls apps/mobile/android/app/build/outputs/apk
ls apps/mobile/android/app/build/outputs/apk/release
- name: Get device name
id: device
run: |
AVD_NAME=$(node -p "require('./apps/mobile/.detoxrc.js').devices.emulator.device.avdName")
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ steps.device.outputs.AVD_NAME }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Detox test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
disable-animations: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation: false
script: yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: detox-artifacts
path: artifacts
retention-days: 14

View File

@@ -4,7 +4,8 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
@@ -21,7 +22,7 @@ jobs:
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
java-version: "11"
architecture: "x64"
- name: Install node modules
@@ -30,23 +31,7 @@ jobs:
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/android && chmod +x ./gradlew
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
@@ -73,7 +58,7 @@ jobs:
ccache -p
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
@@ -84,19 +69,12 @@ jobs:
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Remove debug keystore
run: rm -rf apps/mobile/android/app/debug.keystore
- name: Build unsigned app bundle
run: yarn release:android:bundle
@@ -107,7 +85,7 @@ jobs:
id: sign_app
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/bundle/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
@@ -122,7 +100,7 @@ jobs:
id: sign_apk
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/apk/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
@@ -132,7 +110,7 @@ jobs:
- name: Rename apk files
run: |
cd apps/mobile/android/app/build/outputs/apk/release/
cd apps/mobile/native/android/app/build/outputs/apk/release/
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
@@ -140,7 +118,7 @@ jobs:
- name: Get app version
id: package-version
uses: saionaro/extract-package-version@master
uses: martinbeentjes/npm-get-version-action@master
with:
path: apps/mobile
@@ -151,29 +129,28 @@ jobs:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: beta
track: alpha
status: completed
whatsNewDirectory: apps/mobile/android/releasenotes/
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ steps.package-version.outputs.version}}-beta-android
name: Notesnook Android v${{ steps.package-version.outputs.version}}
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
repository: streetwriters/notesnook
token: ${{ secrets.GITHUB_TOKEN }}
files: |
apps/mobile/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86_64.apk
${{steps.sign_app.outputs.signedReleaseFile}}
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/**/*.map
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -4,7 +4,8 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
@@ -21,32 +22,16 @@ jobs:
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
java-version: "11"
architecture: "x64"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- 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/android && chmod +x ./gradlew
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
@@ -73,7 +58,7 @@ jobs:
ccache -p
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
@@ -84,19 +69,12 @@ jobs:
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Remove debug keystore
run: rm -rf apps/mobile/android/app/debug.keystore
- name: Build unsigned app bundle
run: yarn release:android:bundle
@@ -107,7 +85,7 @@ jobs:
id: sign_app
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/bundle/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
@@ -122,7 +100,7 @@ jobs:
id: sign_apk
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/apk/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
@@ -132,7 +110,7 @@ jobs:
- name: Rename apk files
run: |
cd apps/mobile/android/app/build/outputs/apk/release/
cd apps/mobile/native/android/app/build/outputs/apk/release/
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
@@ -140,7 +118,7 @@ jobs:
- name: Get app version
id: package-version
uses: saionaro/extract-package-version@master
uses: martinbeentjes/npm-get-version-action@master
with:
path: apps/mobile
@@ -153,26 +131,26 @@ jobs:
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/android/releasenotes/
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ steps.package-version.outputs.version}}-android
name: Notesnook Android v${{ steps.package-version.outputs.version}}
tag_name: ${{ steps.package-version.outputs.current-version}}-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
repository: streetwriters/notesnook
token: ${{ secrets.GITHUB_TOKEN }}
files: |
apps/mobile/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86_64.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/**/*.map
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -9,13 +9,7 @@ on:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
pull_request_target:
branches:
- "master"
paths:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
pull_request:
types:
- "ready_for_review"
- "opened"
@@ -23,21 +17,11 @@ on:
- "reopened"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
test:
needs: authorize
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -52,6 +36,7 @@ jobs:
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 "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Run all @notesnook/core tests
run: npm run tx @notesnook/core:test:e2e
run: npx nx test:e2e @notesnook/core

View File

@@ -33,22 +33,14 @@ on:
required: true
default: true
description: "Build for macOS?"
release-track:
type: choice
required: true
default: stable
description: "Select the release track"
options:
- stable
- beta
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -57,25 +49,13 @@ jobs:
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
- name: Setup environment
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build (stable)
if: ${{ inputs.release-track == 'stable' }}
run: npm run tx @notesnook/web:build:desktop
- name: Generate desktop build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: BETA=true npm run tx @notesnook/web:build:desktop
- name: Build desktop bundle
working-directory: ./apps/desktop
run: npm run bundle
- name: Generate desktop build
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v4
@@ -91,8 +71,7 @@ jobs:
- name: Package desktop build
run: |
cp -r ./apps/web/build ./apps/desktop/
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/desktop/build/
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
@@ -103,28 +82,6 @@ jobs:
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
prerelease: ${{ inputs.release-track == 'beta' }}
target_commitish: ${{ github.ref }}
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
sudo apt-get update
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
cd /tmp/flatpak-builder-tools/node
pipx install .
cd -
node scripts/generate-sources.mjs
- name: Upload flatpak sources
uses: softprops/action-gh-release@v1
if: inputs.publish-github && inputs.build-linux
with:
draft: true
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./generated-sources.json
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
build-macos:
@@ -135,7 +92,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -179,12 +136,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
@@ -195,9 +146,9 @@ jobs:
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npm run tx @notesnook/desktop:release -- --variant=mas
cd apps/desktop
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
npx nx run release --project @notesnook/desktop -- --variant=mas
yarn electron-builder --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
env:
@@ -208,13 +159,13 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npm run tx @notesnook/desktop:release
cd apps/desktop
npx nx run release --project @notesnook/desktop
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p always
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
else
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p never
yarn 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
@@ -229,15 +180,15 @@ jobs:
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-x64:
name: Build for Linux x64
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -247,7 +198,7 @@ jobs:
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v3
uses: samuelmeuli/action-snapcraft@v1
if: inputs.publish-snap
- name: Download build
@@ -261,18 +212,14 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
yarn electron-builder --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
@@ -280,85 +227,18 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p always
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
else
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
- name: Publish on Snapcraft
if: inputs.publish-snap
run: |
if [ ${{ inputs.release-track }} == 'beta' ]; then
snapcraft upload --release=beta ./output/notesnook_linux_amd64.snap
else
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
fi
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
working-directory: ./apps/desktop
build-linux-arm64:
name: Build for Linux arm64
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04-arm
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
# - name: Setup Snapcraft Auth
# if: inputs.publish-snap
# run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
# - name: Install Snapcraft
# uses: samuelmeuli/action-snapcraft@v1
# if: inputs.publish-snap
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
# - name: Build snap
# if: inputs.publish-snap
# run: |
# yarn electron-builder --config=electron-builder.config.js --linux snap:arm64 -p never
# working-directory: ./apps/desktop
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p always
else
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
# - name: Publish on Snapcraft
# if: inputs.publish-snap
# run: |
# snapcraft upload --release=stable ./output/notesnook_linux_arm64.snap
# working-directory: ./apps/desktop
build-windows:
name: Build for Windows
needs: build
@@ -367,7 +247,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
@@ -392,17 +272,8 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build
run: node scripts/execute.mjs @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
- name: Publish
env:
@@ -412,8 +283,8 @@ jobs:
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
if ($${{ inputs.publish-github }} -eq $true) {
yarn electron-builder --config=electron-builder.config.js --win --publish always
yarn electron-builder --win --publish always
} else {
yarn electron-builder --config=electron-builder.config.js --win --publish never
yarn electron-builder --win --publish never
}
working-directory: ./apps/desktop

View File

@@ -1,230 +0,0 @@
name: Test @notesnook/desktop
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "apps/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
branches:
- "master"
paths:
- "apps/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
jobs:
build:
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Generate desktop build
run: npm run tx @notesnook/web:build:desktop
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
test-macos-x64:
name: Test macOS x64
needs: build
runs-on: macos-15-intel
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
working-directory: ./apps/desktop
- name: Run tests x64
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos-x64
path: apps/desktop/test-results
retention-days: 5
test-macos:
name: Test macOS
needs: build
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
working-directory: ./apps/desktop
- name: Run tests arm64
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos
path: apps/desktop/test-results
retention-days: 5
test-linux:
name: Test for Linux
needs: build
runs-on: ubuntu-22.04
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-linux
path: apps/desktop/test-results
retention-days: 5
test-windows:
name: Test for Windows
needs: build
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: node scripts/execute.mjs @notesnook/desktop:release
- name: Build app
run: |
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-win
path: apps/desktop/test-results
retention-days: 5

View File

@@ -10,12 +10,6 @@ on:
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
pull_request:
branches:
- "master"
paths:
- "packages/editor/**"
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
types:
- "ready_for_review"
- "opened"
@@ -25,10 +19,10 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -38,8 +32,12 @@ jobs:
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: npm run tx @notesnook/editor:build
run: npx nx build @notesnook/editor
- name: Run all @notesnook/editor tests
run: npm run tx @notesnook/editor:test
run: npx nx test @notesnook/editor

View File

@@ -1,6 +1,6 @@
name: Publish Notesnook Help
on:
on:
workflow_dispatch:
push:
branches:
@@ -11,10 +11,10 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

View File

@@ -4,33 +4,28 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-26
runs-on: macos-13
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npm run tx @notesnook/mobile:build
run: npx nx run @notesnook/mobile:build
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/ios/Podfile.lock')) }}
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/native/ios/Podfile.lock')) }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
@@ -52,22 +47,14 @@ jobs:
ccache -p
- name: Cache Pods
uses: actions/cache@v3
uses: actions/cache@v2
id: pods-cache
with:
path: apps/mobile/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
path: apps/mobile/native/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/ios
pod install
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
run: npm run prepare:ios
- name: CCache Stats Before Build
run: ccache -sv
@@ -78,9 +65,9 @@ jobs:
bundle-identifier: org.streetwriters.notesnook
scheme: Notesnook
configuration: "Release"
export-options: apps/mobile/ios/ExportOptions.plist
project-path: apps/mobile/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
export-options: apps/mobile/native/ios/ExportOptions.plist
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
update-targets: |
Notesnook
Make Note
@@ -116,5 +103,5 @@ jobs:
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/ios/**/*.map
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -1,89 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Publish @notesnook/monograph
on: workflow_dispatch
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-22.04
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=monograph
- name: Collect package metadata
id: package_metadata
working-directory: ./apps/monograph
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
- name: Generate build
run: npm run tx @notesnook/monograph:build
# Setup Buildx
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
ecr: auto
logout: true
# Pull previous image from docker hub to use it as cache to improve the image build time.
- name: docker pull cache image
continue-on-error: true
run: docker pull streetwriters/monograph:latest
# Setup QEMU
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: streetwriters/monograph
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: apps/monograph
file: apps/monograph/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
cache-from: streetwriters/monograph:latest
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: index.docker.io/streetwriters/monograph
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@@ -5,10 +5,10 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -20,6 +20,7 @@ jobs:
- 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

View File

@@ -5,10 +5,10 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -20,6 +20,7 @@ jobs:
- 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

View File

@@ -1,40 +0,0 @@
name: Benchmark @notesnook/web
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.benchmarks.yml"
pull_request:
jobs:
bench:
name: Bench
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Build
run: npm run build:web
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: apps/web
- name: Run benchmarks
id: benchmark
run: node __bench__/app.bench.mjs
working-directory: apps/web

View File

@@ -1,24 +1,14 @@
name: Publish @notesnook/web
on:
workflow_dispatch:
inputs:
release-track:
type: choice
required: true
default: stable
description: "Select the release track"
options:
- stable
- beta
on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -30,23 +20,12 @@ jobs:
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate build (stable)
if: ${{ inputs.release-track == 'stable' }}
- name: Generate build
run: npm run build:web
- name: Publish to Cloudflare Pages (stable)
if: ${{ inputs.release-track == 'stable' }}
working-directory: ./apps/web
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./build/
- name: Generate build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: npm run build:beta:web
- name: Publish to Cloudflare Pages (beta)
if: ${{ inputs.release-track == 'beta' }}
working-directory: ./apps/web
run: npx --yes wrangler pages deploy --branch=beta --project-name=notesnook-app-beta ./build/
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/

View File

@@ -9,13 +9,7 @@ on:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml"
pull_request_target:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml"
pull_request:
types:
- "ready_for_review"
- "opened"
@@ -23,23 +17,12 @@ on:
- "reopened"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
build:
name: Build
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -49,6 +32,10 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate test build
run: npm run build:test:web
@@ -62,13 +49,11 @@ jobs:
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
strategy:
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v4

2
.gitignore vendored
View File

@@ -11,5 +11,3 @@ nx-cloud.env
site
node_modules.backup
.nx
/*.patch
.taskcache

View File

@@ -11,5 +11,5 @@
"cache": true,
"cacheStrategy": "content"
},
"git.rebaseWhenSync": false
"git.rebaseWhenSync": true
}

View File

@@ -27,6 +27,7 @@ Notesnook is built using the following technologies:
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.
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
@@ -65,7 +66,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
## Additional Resources
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
- [Migrating & Importing your data from other apps — Importer](https://importer.notesnook.com/)
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)

View File

@@ -2,6 +2,4 @@ node_modules
build
output
dist
_catalog
test-results
test-artifacts
_catalog

View File

@@ -24,7 +24,7 @@ Requirements:
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
1. `clone` the monorepo:
Once you have completed the setup, the first step is to `clone` the monorepo:
```bash
git clone https://github.com/streetwriters/notesnook.git
@@ -33,51 +33,19 @@ git clone https://github.com/streetwriters/notesnook.git
cd notesnook
```
2. Install dependencies:
Once you are inside the `./notesnook` directory, run the preparation step:
```bash
# this might take a while to complete
npm install
```
3. Run the webapp for desktop environment:
```bash
cd apps/web
npm run start:desktop
```
4. In a separate terminal session, run the desktop app from the root of the project:
Now you can finally start the desktop app:
```bash
npm run start:desktop
```
### Release mode
To run the app in release mode:
```bash
npm run staging -- --rebuild
```
This will compile and run the app in production mode but it won't generate any packages. To create the final packages, you'll have to run the following commands:
```bash
npm run release -- --rebuild
# For macOS
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 --publish never
# For Linux (AppImage)
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 --publish never
# For Windows
npx electron-builder --config=electron-builder.config.js --win --publish never
```
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
## Developer guide
### The tech stack

View File

@@ -1,242 +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 { testCleanup, test } from "./test-override.js";
import { writeFile } from "fs/promises";
import { Page } from "playwright";
import { gt, lt } from "semver";
import { describe } from "vitest";
test("update starts downloading if version is outdated", async ({
ctx: { page },
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /updating/i })
.waitFor({ state: "attached" });
});
test("update is only shown if version is outdated and auto updates are disabled", async ({
ctx,
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.waitFor({ state: "attached" });
});
describe("update to stable if it is newer", () => {
test.scoped({ options: { version: "3.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
expect(gt(version, "3.0.0-beta.0")).toBe(true);
});
});
describe("update is not available if it latest stable version is older", () => {
test.scoped({ options: { version: "99.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
expect(
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.isHidden()
).toBe(true);
});
});
describe("downgrade to stable on switching to stable release track", () => {
test.scoped({ options: { version: "99.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
expect(lt(version, "99.0.0-beta.0")).toBe(true);
});
});
async function skipDialog(page: Page) {
try {
const dialog = page.locator(".ReactModal__Content");
const positiveButton = dialog.locator(
"button[data-role='positive-button']"
);
const negativeButton = dialog.locator(
"button[data-role='negative-button']"
);
if (await positiveButton.isVisible())
await positiveButton.click({ timeout: 1000 });
else if (await negativeButton.isVisible())
await negativeButton.click({ timeout: 1000 });
} catch (e) {
// ignore error
}
}

View File

@@ -1,40 +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 { testCleanup, test } from "./test-override.js";
test("make sure app loads", async ({
ctx: { page },
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
});

View File

@@ -1,64 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test as vitestTest, TestContext } from "vitest";
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
import { mkdir, rm } from "fs/promises";
import path from "path";
import slugify from "slugify";
export const test = vitestTest.extend<Fixtures>({
options: { version: "3.0.0" } as TestOptions,
ctx: async ({ options }, use) => {
const ctx = await buildAndLaunchApp(options);
await use(ctx);
}
});
export async function testCleanup(context: TestContext) {
const ctx = (context.task.context as unknown as Fixtures).ctx;
if (context.task.result?.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(context.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
await rm(ctx.userDataDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
await rm(ctx.outputDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
}

View File

@@ -1,226 +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 { execSync } from "child_process";
import { cp } from "fs/promises";
import { fileURLToPath } from "node:url";
import path, { join, resolve } from "path";
import { _electron as electron } from "playwright";
import { existsSync } from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
const productName = `NotesnookTestHarness`;
const SOURCE_DIR = resolve("output", productName);
export interface AppContext {
app: import("playwright").ElectronApplication;
page: import("playwright").Page;
configPath: string;
userDataDir: string;
outputDir: string;
relaunch: () => Promise<void>;
}
export interface TestOptions {
version: string;
}
export interface Fixtures {
options: TestOptions;
ctx: AppContext;
}
export async function buildAndLaunchApp(
options?: TestOptions
): Promise<AppContext> {
const productName = `notesnooktest${makeid(10)}`;
const outputDir = path.join("test-artifacts", `${productName}-output`);
const executablePath = await copyBuild({
...options,
outputDir
});
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName,
options?.version
);
const ctx: AppContext = {
app,
page,
configPath,
userDataDir,
outputDir,
relaunch: async () => {
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName,
options?.version
);
ctx.app = app;
ctx.page = page;
ctx.userDataDir = userDataDir;
ctx.configPath = configPath;
}
};
return ctx;
}
async function launchApp(
executablePath: string,
packageName: string,
version?: string
) {
const userDataDir = resolve(
__dirname,
"..",
"test-artifacts",
"user_data_dirs",
packageName
);
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
env: {
...(process.platform === "linux"
? {
...(process.env as Record<string, string>),
APPIMAGE: "true"
}
: (process.env as Record<string, string>)),
CUSTOM_USER_DATA_DIR: userDataDir,
...(version
? {
CUSTOM_APP_VERSION: version
}
: {})
}
});
const page = await app.firstWindow();
const configPath = path.join(userDataDir, "UserData", "config.json");
return {
app,
page,
configPath,
userDataDir
};
}
let MAX_RETRIES = 3;
export async function buildApp(version?: string) {
if (!existsSync(SOURCE_DIR)) {
const args = [
"electron-builder",
"--dir",
`--${process.arch}`,
`--config electron-builder.config.js`,
`--c.extraMetadata.productName=${productName}`,
`--c.compression=store`,
"--publish=never"
];
if (version) args.push(`--c.extraMetadata.version=${version}`);
try {
execSync(`npx ${args.join(" ")}`, {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: SOURCE_DIR
}
});
} catch (e) {
if (--MAX_RETRIES) {
console.log("retrying...");
return await buildApp(version);
} else throw e;
}
}
}
async function copyBuild({ outputDir }: { outputDir: string }) {
return process.platform === "win32"
? await makeBuildCopyWindows(outputDir, productName)
: process.platform === "darwin"
? await makeBuildCopyMacOS(outputDir, productName)
: await makeBuildCopyLinux(outputDir, productName);
}
async function makeBuildCopyLinux(outputDir: string, productName: string) {
const platformDir =
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(
__dirname,
"..",
appDir,
productName.toLowerCase().replace(/\s+/g, "-")
);
}
async function makeBuildCopyWindows(outputDir: string, productName: string) {
const platformDir =
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(__dirname, "..", appDir, `${productName}.exe`);
}
async function makeBuildCopyMacOS(outputDir: string, productName: string) {
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(
__dirname,
"..",
appDir,
`${productName}.app`,
"Contents",
"MacOS",
productName
);
}
async function makeBuildCopy(outputDir: string, platformDir: string) {
const appDir = outputDir;
await cp(join(SOURCE_DIR, platformDir), outputDir, {
recursive: true,
preserveTimestamps: true,
verbatimSymlinks: true,
dereference: false,
force: true
});
return appDir;
}
function makeid(length: number) {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result.toLowerCase();
}

View File

@@ -1,4 +1,3 @@
owner: streetwriters
repo: notesnook
provider: github
updaterCacheDirName: Notesnook

Binary file not shown.

View File

@@ -1,223 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const path = require("path");
const pkg = require("./package.json");
const buildRoot = process.env.NN_BUILD_ROOT || ".";
const buildFiles = [
`${buildRoot}/build/`,
`!${buildRoot}/build/screenshots\${/*}`,
`!${buildRoot}/build/banner.jpg`,
`!${buildRoot}/build/*.ico`,
`!${buildRoot}/build/*.png`
];
const productName = process.env.NN_PRODUCT_NAME || "Notesnook";
const appId = process.env.NN_APP_ID || "org.streetwriters.notesnook";
const outputDir = process.env.NN_OUTPUT_DIR || "output";
const linuxExecutableName = process.env.NN_PRODUCT_NAME
? process.env.NN_PRODUCT_NAME.toLowerCase().replace(/\s+/g, "-")
: "notesnook";
const year = new Date().getFullYear();
const isBeta = pkg.version.includes("-beta");
/**
* @type {import("app-builder-lib").Configuration}
*/
module.exports = {
appId: appId,
productName: productName,
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
artifactName: "notesnook_${os}_${arch}.${ext}",
generateUpdatesFilesForAllChannels: true,
asar: true,
asarUnpack: [
"node_modules/sqlite-better-trigram-@(linux|darwin|windows)-${arch}/**/*",
"node_modules/sqlite3-fts5-html-@(linux|darwin|windows)-${arch}/**/*"
],
files: [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
...buildFiles,
"!node_modules${/*}",
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
"node_modules/better-sqlite3-multiple-ciphers/lib",
"node_modules/better-sqlite3-multiple-ciphers/package.json",
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sqlite-better-trigram",
"node_modules/sqlite3-fts5-html",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
{
from: "node_modules/sqlite-better-trigram-linux-${arch}",
to: "node_modules/sqlite-better-trigram-linux-${arch}"
},
{
from: "node_modules/sqlite-better-trigram-darwin-${arch}",
to: "node_modules/sqlite-better-trigram-darwin-${arch}"
},
{
from: "node_modules/sqlite-better-trigram-windows-${arch}",
to: "node_modules/sqlite-better-trigram-windows-${arch}"
},
{
from: "node_modules/sqlite3-fts5-html-linux-${arch}",
to: "node_modules/sqlite3-fts5-html-linux-${arch}"
},
{
from: "node_modules/sqlite3-fts5-html-darwin-${arch}",
to: "node_modules/sqlite3-fts5-html-darwin-${arch}"
},
{
from: "node_modules/sqlite3-fts5-html-windows-${arch}",
to: "node_modules/sqlite3-fts5-html-windows-${arch}"
},
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
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",
notarize: true
},
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"]
}
],
signtoolOptions: {
signingHashAlgorithms: ["sha256"],
sign: "./scripts/sign.js"
},
icon: "assets/icons/app.ico"
},
portable: {
artifactName: "notesnook_${os}_${arch}_portable.${ext}"
},
nsis: {
oneClick: true,
createDesktopShortcut: "always",
deleteAppDataOnUninstall: true
},
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: linuxExecutableName,
desktop: {
desktopActions: {
"new-note": {
Name: "New note",
Exec: `${linuxExecutableName} new note`
},
"new-notebook": {
Name: "New notebook",
Exec: `${linuxExecutableName} new notebook`
},
"new-reminder": {
Name: "New reminder",
Exec: `${linuxExecutableName} new reminder`
}
}
}
},
snap: {
autoStart: false,
confinement: "strict",
allowNativeWayland: true,
base: "core22"
},
extraResources: ["app-update.yml", "./assets/**"],
extraMetadata: {
main: path.join(buildRoot, "build", "electron.js")
},
directories: {
buildResources: "assets",
output: outputDir
},
publish: [
{
provider: "github",
repo: "notesnook",
owner: "streetwriters",
channel: isBeta ? "beta" : "latest"
}
]
};

File diff suppressed because it is too large Load Diff

View File

@@ -2,82 +2,228 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.7",
"version": "3.0.6",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": [
"src/overrides.ts"
],
"exports": {
".": {
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
}
}
},
"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": {
"@lingui/core": "5.1.2",
"@notesnook/intl": "file:../../packages/intl",
"@notesnook/ui": "file:../../packages/ui",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "^12.4.1",
"electron-trpc": "0.7.1",
"electron-updater": "^6.6.2",
"icojs": "^0.19.5",
"sqlite-better-trigram": "0.0.5",
"sqlite3-fts5-html": "^0.0.6",
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.38.3",
"@trpc/server": "10.38.3",
"better-sqlite3-multiple-ciphers": "^9.5.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.4",
"icojs": "^0.17.1",
"sodium-native": "^4.1.1",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "3.24.3"
"yargs": "^17.6.2",
"zod": "^3.21.4"
},
"devDependencies": {
"@streetwriters/kysely": "^0.27.4",
"@types/node": "22.15.3",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^37.0.0",
"electron-builder": "^26.0.12",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
"node-gyp-build": "^4.8.4",
"playwright": "1.48.2",
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "^29.3.1",
"electron-builder": "^24.13.3",
"esbuild": "^0.20.0",
"kysely": "^0.27.3",
"node-gyp-build": "^4.8.0",
"prebuildify": "^6.0.1",
"slugify": "1.6.6",
"tree-kill": "^1.2.2",
"undici": "^7.8.0",
"vitest": "^3.2.4"
"undici": "^6.14.1"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
},
"overrides": {
"@electron/node-gyp": "^10.2.0-electron.2"
},
"scripts": {
"start": "node scripts/dev.mjs",
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "node ../../scripts/build.mjs",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"build": "tsc",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle: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",
"test": "vitest run"
"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}",
"generateUpdatesFilesForAllChannels": true,
"asar": false,
"files": [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
"build/",
"!build/screenshots${/*}",
"!build/banner.jpg",
"!build/*.ico",
"!build/*.png",
"!node_modules${/*}",
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
"node_modules/better-sqlite3-multiple-ciphers/lib",
"node_modules/better-sqlite3-multiple-ciphers/package.json",
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
"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",
"notarize": true
},
"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"
]
}
],
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"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": [
"app-update.yml",
"./assets/**"
],
"extraMetadata": {
"main": "./build/electron.js"
},
"directories": {
"buildResources": "assets",
"output": "./output/"
},
"publish": [
{
"provider": "github",
"repo": "notesnook",
"owner": "streetwriters"
}
]
}
}

View File

@@ -0,0 +1,36 @@
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
index fcb7f54..cb1c7f7 100644
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
StartupWMClass: appInfo.productName,
...extra,
...targetSpecificOptions.desktop,
+ actions: undefined
};
const description = this.getDescription(targetSpecificOptions);
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
data += `\n${name}=${desktopMeta[name]}`;
}
data += "\n";
+
+ if (targetSpecificOptions.desktop.actions) {
+ let actionsData = "";
+ const validActions = [];
+ for (const action of targetSpecificOptions.desktop.actions) {
+ if (!action.id || !action.name || !action.args) continue;
+ actionsData += "\n";
+ actionsData += `[Desktop Action ${action.id}]
+Name=${action.name}
+Exec=${desktopMeta.Exec} ${action.args}`;
+ actionsData += "\n";
+
+ validActions.push(action.id);
+ }
+
+ data += `Actions=${validActions.join(";")};\n${actionsData}`
+ }
return Promise.resolve(data);
}
}

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/app-builder-lib/out/targets/snap.js b/node_modules/app-builder-lib/out/targets/snap.js
index 0b5405e..753f6c6 100644
--- a/node_modules/app-builder-lib/out/targets/snap.js
+++ b/node_modules/app-builder-lib/out/targets/snap.js
@@ -115,7 +115,7 @@ class SnapTarget extends core_1.Target {
else {
const archTriplet = archNameToTriplet(arch);
appDescriptor.environment = {
- DISABLE_WAYLAND: options.allowNativeWayland ? "" : "1",
+ DISABLE_WAYLAND: options.allowNativeWayland ? "0" : "1",
PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH",
SNAP_DESKTOP_RUNTIME: "$SNAP/gnome-platform",
LD_LIBRARY_PATH: [

View File

@@ -1,11 +0,0 @@
diff --git a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
index 1a14ecd..ff938f1 100644
--- a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
+++ b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
@@ -38,5 +38,6 @@
'SQLITE_TRACE_SIZE_LIMIT=32',
'SQLITE_USER_AUTHENTICATION=0',
'SQLITE_USE_URI=0',
+ 'SQLITE_ENABLE_REGEXP',
],
}

View File

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

View File

@@ -0,0 +1,46 @@
diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js
index 3fbcdf0..7ca3ab5 100644
--- a/node_modules/node-gyp-build/bin.js
+++ b/node_modules/node-gyp-build/bin.js
@@ -16,7 +16,8 @@ if (!buildFromSource()) {
}
function build () {
- var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
+ var win32 = os.platform() === 'win32'
+ var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
try {
var pkg = require('node-gyp/package.json')
@@ -27,7 +28,7 @@ function build () {
]
} catch (_) {}
- proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) {
+ proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell: win32, windowsHide: true }).on('exit', function (code) {
if (code || !process.argv[3]) process.exit(code)
exec(process.argv[3]).on('exit', function (code) {
process.exit(code)
@@ -45,15 +46,18 @@ function preinstall () {
function exec (cmd) {
if (process.platform !== 'win32') {
- var shell = os.platform() === 'android' ? 'sh' : '/bin/sh'
- return proc.spawn(shell, ['-c', '--', cmd], {
+ var shell = os.platform() === 'android' ? 'sh' : true
+ return proc.spawn(cmd, [], {
+ shell,
stdio: 'inherit'
})
}
- return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], {
+ return proc.spawn(cmd, [], {
windowsVerbatimArguments: true,
- stdio: 'inherit'
+ stdio: 'inherit',
+ shell: true,
+ windowsHide: true
})
}

View File

@@ -17,72 +17,60 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
console.log("starting build...");
import path from "path";
import fs from "fs/promises";
import { existsSync } from "fs";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
import { fileURLToPath } from "url";
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
console.log("imports done...");
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = args.root || path.join(__dirname, "..");
const skipTscBuild = args.skipTscBuild || false;
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
);
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
console.log("loaded args", {
args,
__filename,
__dirname,
root,
skipTscBuild,
webAppPath
});
await fs.rm(path.join(root, "build"), { force: true, recursive: true });
console.log("removed build folder");
await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
console.log("rebuilding...");
await exec(
"node scripts/execute.mjs @notesnook/web:build:desktop",
"yarn nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
}
await patchBetterSQLite3();
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
await fs.cp(path.join(webAppPath, "build"), path.join(root, "build"), {
if (os.platform() === "win32")
await exec(
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
path.join(__dirname, "..", "node_modules", "sodium-native")
);
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
});
if (args.variant === "mas") {
await exec(`yarn run bundle:mas --outdir=${path.join(root, "build")}`);
await exec(`yarn run bundle:mas`);
} else {
await exec(`yarn run bundle --outdir=${path.join(root, "build")}`);
await exec(`yarn run bundle`);
}
if (!skipTscBuild) {
await exec(`yarn run build`);
}
await exec(`yarn tsc`);
if (args.run) {
await exec(
`yarn electron-builder --dir --${process.arch} --config=electron-builder.config.js`
);
await exec(`yarn electron-builder --dir --x64`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {
if (process.arch === "arm64")
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
} else {
await exec(`./output/linux-unpacked/Notesnook`);
}
@@ -95,3 +83,21 @@ async function exec(cmd, cwd) {
cwd: cwd || process.cwd()
});
}
async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "9.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}

View File

@@ -36,8 +36,6 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
await onChange(true);
console.log("Watching...");
@@ -55,13 +53,11 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec("yarn electron-builder install-app-deps");
}
await exec(`yarn run bundle`);
await exec(`yarn run build`);
execAsync(`yarn`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -70,8 +66,8 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["npm", "run", "start:desktop"],
path.join(__dirname, "..", "..", "web"),
["yarn", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
}

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 { readFile, rm, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
console.log("Patching better-sqlite3");
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
delete json.homepage;
delete json.repository;
await writeFile(jsonPath, JSON.stringify(json));
await rm(
path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"build"
),
{ force: true, recursive: true }
);
}
if (process.argv[1] === __filename) {
patchBetterSQLite3();
}

View File

@@ -19,16 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { initTRPC } from "@trpc/server";
import { z } from "zod";
import {
app,
dialog,
Menu,
MenuItem,
nativeImage,
nativeTheme,
Notification,
shell
} from "electron";
import { dialog, nativeTheme, Notification, shell } from "electron";
import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
@@ -38,11 +29,10 @@ import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak, isSnap } from "../utils";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
import type { MenuItem as NNMenuItem } from "@notesnook/ui";
const t = initTRPC.create();
@@ -59,7 +49,6 @@ const NotificationOptions = z.object({
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
isSnap: t.procedure.query(() => isSnap()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
@@ -163,10 +152,6 @@ export const osIntegrationRouter = t.router({
await rm(input);
}),
restart: t.procedure.query(() => {
app.relaunch();
app.exit();
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {
@@ -206,10 +191,7 @@ export const osIntegrationRouter = t.router({
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor;
if (
process.platform === "win32" &&
!config.desktopSettings.nativeTitlebar
)
if (process.platform === "win32")
globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor
});
@@ -235,63 +217,5 @@ export const osIntegrationRouter = t.router({
nativeTheme.off("updated", updated);
};
})
),
showMenu: t.procedure
.input(
z.object({
menuItems: z.array(z.any())
})
)
.subscription(({ input: { menuItems } }) =>
observable<string[]>((emit) => {
const items = menuItems as NNMenuItem[];
const menu = new Menu();
for (const item of items) {
const menuItem = toMenuItem(item, (id) => emit.next(id));
if (menuItem) menu.append(menuItem);
}
if (menu.items.length > 0) menu.popup();
return () => {
menu.removeAllListeners();
menu.closePopup();
};
})
)
)
});
function toMenuItem(
item: NNMenuItem,
onClick: (id: string[]) => void,
parentKey?: string
): MenuItem | undefined {
switch (item.type) {
case "lazy-loader":
return undefined;
case "separator":
return new MenuItem({ type: "separator" });
case "button": {
const submenu = item.menu ? new Menu() : undefined;
if (submenu && item.menu) {
for (const subitem of item.menu.items) {
const subMenuItem = toMenuItem(subitem, onClick, item.key);
if (subMenuItem) submenu.append(subMenuItem);
}
}
return new MenuItem({
label: item.title,
enabled: !item.isDisabled,
visible: !item.isHidden,
toolTip: item.tooltip,
sublabel: item.tooltip,
checked: item.isChecked,
type: submenu ? "submenu" : item.isChecked ? "checkbox" : "normal",
id: item.key,
submenu,
click: () => onClick(parentKey ? [parentKey, item.key] : [item.key]),
accelerator: item.modifier?.replace("Mod", "CommandOrControl")
});
}
}
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { Database, Statement } from "better-sqlite3-multiple-ciphers";
import type { QueryResult } from "@streetwriters/kysely";
import type { QueryResult } from "kysely";
type SQLiteCompatibleType =
| number
@@ -33,8 +33,6 @@ export class SQLite {
initialized = false;
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
retryCounter: Record<string, number> = {};
extensionsLoaded = false;
constructor() {
console.log("new sqlite worker");
}
@@ -115,29 +113,9 @@ export class SQLite {
} catch (e) {
if (e instanceof Error) e.message += ` (query: ${sql})`;
throw e;
} finally {
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
// extensions before database has been decrypting. This is because
// executing a `SELECT` now accesses the underlying databases resulting in
// an error. Since FTS5 extensions depend on `SELECT fts5` to load the
// fts5 API, we must wait decrypt the database before we can load
// the extensions.
if (!this.extensionsLoaded && (await this.isDatabaseReady())) {
this.loadExtensions();
}
}
}
private loadExtensions() {
this.sqlite?.loadExtension(
getExtensionPath("sqlite-better-trigram", "better-trigram")
);
this.sqlite?.loadExtension(
getExtensionPath("sqlite3-fts5-html", "fts5-html")
);
this.extensionsLoaded = true;
}
async run<R>(
sql: string,
parameters?: SQLiteCompatibleType[]
@@ -162,53 +140,4 @@ export class SQLite {
retryDelay: 500
});
}
/**
* This just executes `SELECT 1` on the database to make sure its ready.
* On an encrypted database, this will fail until `PRAGMA key` has been
* called.
*/
private async isDatabaseReady() {
// return this.exec(`SELECT 1;`)
// .then(() => true)
// .catch(() => false);
if (!this.sqlite) return false;
try {
this.sqlite.prepare(`SELECT 1;`).run();
return true;
} catch {
return false;
}
}
}
function getExtensionPath(extensionName: string, entryPoint: string) {
const path = require("path");
const { statSync } = require("fs");
const os = process.platform === "win32" ? "windows" : process.platform;
const packageName = `${extensionName}-${os}-${process.arch}`;
const extensionSuffix =
process.platform === "win32"
? "dll"
: process.platform === "darwin"
? "dylib"
: "so";
let loadablePath = path.join(
require.resolve(extensionName),
"..",
"..",
packageName,
`${entryPoint}.${extensionSuffix}`
);
if (loadablePath.includes(".asar"))
loadablePath = loadablePath
.replace("electron.asar", "app.asar")
.replace(".asar", ".asar.unpacked");
if (!statSync(loadablePath, { throwIfNoEntry: false })) {
throw new Error(`${extensionName} not found at ${loadablePath}.`);
}
return loadablePath;
}

View File

@@ -23,45 +23,25 @@ import { CancellationToken, autoUpdater } from "electron-updater";
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
import { z } from "zod";
import { config } from "../utils/config";
import { app } from "electron";
type UpdateInfo = { version: string };
type Progress = { percent: number };
const t = initTRPC.create();
let cancellationToken: CancellationToken | undefined = undefined;
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
export const updaterRouter = t.router({
autoUpdates: t.procedure.query(() => config.automaticUpdates),
releaseTrack: t.procedure.query(() => config.releaseTrack),
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
if (cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<string[]>((resolve, reject) => {
downloadTimeout = setTimeout(async () => {
cancellationToken = new CancellationToken();
autoUpdater.isUpdaterActive();
await autoUpdater
.downloadUpdate(cancellationToken)
.then(resolve)
.catch(reject)
.finally(() => (cancellationToken = undefined));
}, 1000);
});
cancellationToken = new CancellationToken();
await autoUpdater
.downloadUpdate(cancellationToken)
.finally(() => (cancellationToken = undefined));
}),
check: t.procedure.query(async () => {
if (cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<void>((resolve) => {
downloadTimeout = setTimeout(async () => {
await autoUpdater
.checkForUpdates()
.catch(console.error)
.finally(resolve);
}, 1000);
});
await autoUpdater.checkForUpdates().catch(console.error);
}),
toggleAutoUpdates: t.procedure
@@ -69,13 +49,7 @@ export const updaterRouter = t.router({
.mutation(({ input: { enabled } }) => {
config.automaticUpdates = enabled;
}),
changeReleaseTrack: t.procedure
.input(z.object({ track: z.string() }))
.mutation(({ input: { track } }) => {
config.releaseTrack = track;
app.relaunch();
app.exit();
}),
onChecking: createSubscription("checking-for-update"),
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
"update-downloaded"
@@ -87,18 +61,7 @@ export const updaterRouter = t.router({
"update-not-available"
),
onAvailable: createSubscription<"update-available", UpdateInfo>(
"update-available",
() => {
if (!config.automaticUpdates) return false;
autoUpdater.emit("download-progress", {
bytesPerSecond: 0,
delta: 0,
percent: 0,
total: 100,
transferred: 0
});
return true;
}
"update-available"
),
onError: createSubscription("error")
});
@@ -106,11 +69,10 @@ export const updaterRouter = t.router({
function createSubscription<
TName extends keyof AppUpdaterEvents,
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
>(eventName: TName) {
return t.procedure.subscription(() => {
return observable<TReturnType>((emit) => {
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
if (handler?.(args[0])) return;
emit.next(args[0]);
};
autoUpdater.removeAllListeners(eventName);

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export { PATHS } from "./constants";
export * from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";

View File

@@ -17,8 +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 "./overrides";
import { app, BrowserWindow, nativeTheme, shell, dialog } from "electron";
import { app, BrowserWindow, nativeTheme, shell } from "electron";
import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
import { configureAutoUpdater } from "./utils/autoupdater";
@@ -37,20 +36,6 @@ import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
import { Messages, setI18nGlobal } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const locale =
process.env.NODE_ENV === "development"
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
: import("@notesnook/intl/locales/$en.json");
locale.then(({ default: locale }) => {
i18n.load({
en: locale.messages as unknown as Messages
});
i18n.activate("en");
});
setI18nGlobal(i18n);
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -86,8 +71,6 @@ async function createWindow() {
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
paintWhenInitiallyHidden: cliOptions.hidden,
skipTaskbar: cliOptions.hidden,
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
@@ -101,24 +84,17 @@ async function createWindow() {
format: process.platform === "win32" ? "ico" : "png"
}),
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle:
process.platform === "win32" || process.platform === "darwin"
? "hidden"
: "default",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
}
}),
titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
},
webPreferences: {
zoomFactor: config.zoomFactor,
@@ -148,11 +124,6 @@ async function createWindow() {
await AssetManager.loadIcons();
setupDesktopIntegration(config.desktopSettings);
mainWindow.webContents.session.setPermissionRequestHandler(
(webContents, permission, callback) => {
callback(permission === "geolocation" ? false : true);
}
);
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);
@@ -182,17 +153,6 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
if (app.runningUnderARM64Translation) {
console.log("App is running under ARM64 translation");
dialog.showMessageBoxSync({
message:
"Notesnook detected that it is running under ARM64 translation. For the best performance, please download the ARM64 build of Notesnook from our website.",
type: "warning",
buttons: ["Okay"],
title: "Degraded Performance Warning"
});
}
if (config.customDns) enableCustomDns();
else disableCustomDns();

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 { app } from "electron";
import path from "path";
const customVersion = process.env.CUSTOM_APP_VERSION;
if (customVersion) {
app.getVersion = () => customVersion;
console.log("setting custom version:", customVersion);
}
if (process.env.CUSTOM_USER_DATA_DIR) {
app.setPath(
"appData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
);
app.setPath(
"userData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
);
app.setPath(
"documents",
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
);
}

View File

@@ -19,25 +19,26 @@ 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 { NNCrypto } from "@notesnook/crypto";
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: any;
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
var electronTRPC: RendererGlobalElectronTRPC;
var NativeNNCrypto: (new () => NNCrypto) | undefined;
}
process.once("loaded", async () => {
const electronTRPC = {
sendMessage: (operation: any) =>
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
onMessage: (callback: any) =>
onMessage: (callback) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};
globalThis.electronTRPC = electronTRPC;
});
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -20,7 +20,7 @@ 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 { ParsedImage, parseICO } from "icojs";
import { parse, ParsedImage } from "icojs";
import { getSystemTheme } from "./theme";
import { readFile } from "fs/promises";
@@ -71,7 +71,7 @@ export class AssetManager {
`${icon}${prefix}.ico`
);
const icoBuffer = await readFile(icoPath);
const images = await parseICO(icoBuffer, "image/png");
const images = await parse(icoBuffer, "image/png");
ALL_ICONS.push({ id: icon, images, prefix });
}
}

View File

@@ -20,25 +20,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { autoUpdater } from "electron-updater";
import { config } from "./config";
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
? "beta"
: "latest";
async function configureAutoUpdater() {
const releaseTrack =
config.releaseTrack === "stable" ? "latest" : config.releaseTrack;
autoUpdater.setFeedURL({
provider: "generic",
url: `https://notesnook.com/api/v1/releases/${process.platform}/${releaseTrack}`,
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
useMultipleRangeRequest: false,
channel: releaseTrack
channel: CHANNEL
});
autoUpdater.autoDownload = config.automaticUpdates;
autoUpdater.allowDowngrade =
// only allow downgrade if the current version is a prerelease
// and the user has changed the release track to stable
config.releaseTrack === "stable" &&
autoUpdater.currentVersion.prerelease.length > 0;
autoUpdater.allowDowngrade = false;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.disableWebInstaller = true;
}
export { configureAutoUpdater };

View File

@@ -20,14 +20,12 @@ 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";
import { autoUpdater } from "electron-updater";
export const DesktopIntegration = z.object({
autoStart: z.boolean().optional(),
startMinimized: z.boolean().optional(),
minimizeToSystemTray: z.boolean().optional(),
closeToSystemTray: z.boolean().optional(),
nativeTitlebar: z.boolean().optional()
closeToSystemTray: z.boolean().optional()
});
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
@@ -37,8 +35,7 @@ export const config = {
autoStart: false,
startMinimized: false,
minimizeToSystemTray: false,
closeToSystemTray: false,
nativeTitlebar: false
closeToSystemTray: false
},
privacyMode: false,
isSpellCheckerEnabled: true,
@@ -47,9 +44,6 @@ export const config = {
automaticUpdates: true,
proxyRules: "",
customDns: true,
releaseTrack: autoUpdater.currentVersion.raw.includes("-beta")
? "beta"
: "stable",
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:

View File

@@ -29,7 +29,3 @@ export function isDevelopment() {
export function isFlatpak() {
return existsSync("/.flatpak-info");
}
export function isSnap() {
return process.env.SNAP !== undefined;
}

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { Menu, MenuItem, clipboard, shell } from "electron";
function setupMenu() {
@@ -41,7 +40,7 @@ function setupMenu() {
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: strings.addToDictionary(),
label: "Add to dictionary",
click: () =>
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
@@ -60,7 +59,7 @@ function setupMenu() {
if (params.linkURL.length) {
menu.append(
new MenuItem({
label: strings.openInBrowser(),
label: "Open in browser",
click: () => shell.openExternal(params.linkURL)
})
);
@@ -69,7 +68,7 @@ function setupMenu() {
if (params.isEditable) {
menu.append(
new MenuItem({
label: strings.undo(),
label: "Undo",
role: "undo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Z"
@@ -78,7 +77,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: strings.redo(),
label: "Redo",
role: "redo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Y"
@@ -95,7 +94,7 @@ function setupMenu() {
if (params.isEditable)
menu.append(
new MenuItem({
label: strings.cut(),
label: "Cut",
role: "cut",
enabled: params.selectionText.length > 0,
accelerator: "CommandOrControl+X"
@@ -105,7 +104,7 @@ function setupMenu() {
if (params.linkURL?.length) {
menu.append(
new MenuItem({
label: strings.copyLink(),
label: "Copy link",
click() {
clipboard.writeText(params.linkURL);
}
@@ -114,7 +113,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: strings.copyLinkText(),
label: "Copy link text",
click() {
clipboard.writeText(params.linkText);
}
@@ -125,7 +124,7 @@ function setupMenu() {
if (params.selectionText.length) {
menu.append(
new MenuItem({
label: strings.copy(),
label: "Copy",
role: "copy",
accelerator: "CommandOrControl+C"
})
@@ -136,51 +135,23 @@ function setupMenu() {
menu.append(
new MenuItem({
id: "copy-image",
label: strings.copyImage(),
label: "Copy Image",
click() {
globalThis.window?.webContents.copyImageAt(params.x, params.y);
}
})
);
if (params.isEditable) {
if (params.isEditable)
menu.append(
new MenuItem({
label: strings.paste(),
role: "paste",
label: "Paste",
role: "pasteAndMatchStyle",
enabled: clipboard.readText("clipboard").length > 0,
accelerator: "CommandOrControl+V"
})
);
menu.append(
new MenuItem({
label:
process.platform === "darwin"
? strings.pasteAndMatchStyle()
: strings.pasteWithoutFormatting(),
role: "pasteAndMatchStyle",
enabled: clipboard.readText("clipboard").length > 0,
accelerator:
process.platform === "darwin"
? "Option+Shift+Command+V"
: "Shift+CommandOrControl+V"
})
);
menu.append(
new MenuItem({
type: "separator"
})
);
menu.append(
new MenuItem({
label: strings.spellCheck(),
role: "toggleSpellChecker"
})
);
}
if (menu.items.length > 0) menu.popup();
});
}

View File

@@ -57,8 +57,7 @@ function getSystemTheme() {
nativeTheme.themeSource = oldThemeSource;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
return currentTheme;
@@ -73,8 +72,7 @@ function changeTheme(theme: Theme) {
nativeTheme.themeSource = theme;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
}

View File

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

View File

@@ -1,42 +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 { defineConfig } from "vitest/config";
export default defineConfig({
test: {
testTimeout: 120 * 1000,
hookTimeout: 120 * 1000,
sequence: {
concurrent: true,
shuffle: true
},
globalSetup: "./__tests__/global-setup.ts",
dir: "./__tests__/",
exclude: [
"**/node_modules/**",
"**/dist/**",
"**/cypress/**",
"**/.{idea,git,cache,output,temp}/**",
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
"**/test-results/**",
"**/test-artifacts/**"
]
}
});

View File

@@ -1,92 +0,0 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
$0: "jest",
config: "e2e/jest.config.js"
},
jest: {
setupTimeout: 120000
}
},
apps: {
"ios.debug": {
type: "ios.app",
binaryPath:
"ios/build/Build/Products/Debug-iphonesimulator/Notesnook.app",
build:
"xcodebuild -workspace ios/Notesnook.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build"
},
"ios.release": {
type: "ios.app",
binaryPath:
"ios/build/Build/Products/Release-iphonesimulator/Notesnook.app",
build:
"xcodebuild -workspace ios/Notesnook.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build"
},
"android.debug": {
type: "android.apk",
binaryPath: "android/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk",
testBinaryPath:
"android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk",
build:
"cd android ; ENVFILE=.env.test ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
reversePorts: [8081]
},
"android.release": {
type: "android.apk",
binaryPath:
"android/app/build/outputs/apk/release/app-arm64-v8a-release.apk",
testBinaryPath:
"android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk",
build:
"cd android ; ENVFILE=.env.test ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd .."
}
},
devices: {
simulator: {
type: "ios.simulator",
device: {
type: "iPhone 17 Pro Max"
}
},
attached: {
type: "android.attached",
device: {
adbName: ".*"
}
},
emulator: {
type: "android.emulator",
device: {
avdName: "Pixel_5_API_36"
}
}
},
configurations: {
"ios.sim.debug": {
device: "simulator",
app: "ios.debug"
},
"ios.sim.release": {
device: "simulator",
app: "ios.release"
},
"android.att.debug": {
device: "attached",
app: "android.debug"
},
"android.att.release": {
device: "attached",
app: "android.release"
},
"android.emu.debug": {
device: "emulator",
app: "android.debug"
},
"android.emu.release": {
device: "emulator",
app: "android.release"
}
}
};

View File

@@ -4,7 +4,7 @@ artifacts/
#
.DS_Store
android/app/src/main/assets/
native/android/app/src/main/assets/
*Issues.md
build_cache/
#
@@ -16,12 +16,11 @@ build_cache/
.cxx/
*.keystore
!debug.keystore
.kotlin/
# Xcode
#
ios/Pods
ios/DerivedData
native/ios/Pods
native/ios/DerivedData
build/
*.pbxuser
!default.pbxuser
@@ -39,8 +38,7 @@ DerivedData
*.ipa
*.xcuserstate
*.hprof
**/.xcode.env.local
cache
ios/.xcode.env.local
# Android/IntelliJ
#
rn-build-deps/
@@ -58,7 +56,7 @@ yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
# fastlane
#
@@ -70,7 +68,6 @@ buck-out/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
vendor
# Bundle artifact
*.jsbundle

View File

@@ -1,16 +0,0 @@
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
# Ruby 3.4.0 has removed some libraries from the standard library.
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'

View File

@@ -1,264 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.streetwriters.notesnook">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:node="remove" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.USE_FULL_SCREEN_INTENT"
tools:node="remove" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
<data android:mimeType="application/pdf" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
<data android:mimeType="text/*" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
<data android:mimeType="image/*" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
<data android:mimeType="video/*" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
<data android:mimeType="audio/*" />
</intent>
</queries>
<application
android:name=".MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/BootTheme">
<receiver
android:name=".NoteWidget"
android:exported="false"
android:label="@string/quick_note">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/new_note_widget_info" />
</receiver>
<receiver
android:name=".NotePreviewWidget"
android:exported="false"
android:label="@string/note">
<intent-filter>"
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/note_widget_info" />
</receiver>
<receiver
android:name=".ReminderWidgetProvider"
android:exported="false"
android:label="@string/reminders_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_reminders_info" />
</receiver>
<activity
android:name=".NotePreviewConfigureActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="true"
android:label="NotePreviewConfigure"
android:launchMode="singleTask"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="Notesnook">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="app.notesnook.com"
android:scheme="https" />
</intent-filter>
<intent-filter android:label="Notesnook">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="notesnook" />
</intent-filter>
</activity>
<activity
android:name="com.facebook.react.devsupport.DevSettingsActivity"
android:exported="false" />
<activity
android:name=".ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:excludeFromRecents="true"
android:exported="true"
android:label="@string/title_activity_share"
android:noHistory="true"
android:taskAffinity=""
android:theme="@style/AppThemeB"
android:windowSoftInputMode="adjustResize">
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="image/*" />
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="image/*" />
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
</activity>
<service
android:name=".OnClearFromRecentService"
android:enabled="true"
android:exported="true"
android:stopWithTask="false" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
<service
android:name="com.streetwriters.notesnook.BootTaskService"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="The service is required by the app to restore notifications of pinned notes, restore notification with reply input for creating notes and restore data in note preview widgets on device reboot." />
</service>
<service
android:name=".NotesnookTileService"
android:exported="true"
android:icon="@drawable/add_note"
android:label="New note"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".ReminderViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_viewer_provider_paths" />
</provider>
<provider
android:name="com.vinzscam.reactnativefileviewer.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_viewer_provider_paths" />
</provider>
<receiver
android:name=".BootRecieverService"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -1,68 +0,0 @@
package com.streetwriters.notesnook;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import javax.annotation.Nullable;
public class BootTaskService extends HeadlessJsTaskService {
Notification notification;
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
Log.d("BootTask", "Task Started");
return new HeadlessJsTaskConfig(
"com.streetwriters.notesnook.BOOT_TASK",
Arguments.createMap(),
30000, // timeout for the task
false // optional: defines whether or not the task is allowed in foreground. Default is false
);
}
@Override
public void onHeadlessJsTaskFinish(int taskId) {
super.onHeadlessJsTaskFinish(taskId);
Log.d("BootTask", "Task completed");
stopSelf();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("com.streetwriters.notesnook",
"Default",
NotificationManager.IMPORTANCE_DEFAULT);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
notification = new NotificationCompat.Builder(this, channel.getId())
.setContentTitle("Sync on boot")
.setContentText("")
.setSmallIcon(R.drawable.ic_stat_name)
.build();
if (android.os.Build.VERSION.SDK_INT >= 34) {
this.startForeground(
1,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
this.startForeground(
1,
notification);
}
}
return super.onStartCommand(intent, flags, startId);
}
}

View File

@@ -1,28 +0,0 @@
package com.streetwriters.notesnook
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import org.wonday.orientation.OrientationActivityLifecycle;
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(NNativeModulePackage());
},
)
}
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance());
loadReactNative(this)
}
}

View File

@@ -1,71 +0,0 @@
package com.streetwriters.notesnook;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViews;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
import com.google.gson.Gson;
import com.streetwriters.notesnook.datatypes.Note;
public class NotePreviewConfigureActivity extends ReactActivity {
static int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
static NotePreviewConfigureActivity activity;
/**
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
* (aka React 18) with two boolean flags.
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
getMainComponentName(),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
Intent intent = getIntent();
Bundle extras = intent.getExtras();
int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
NotePreviewConfigureActivity.appWidgetId = appWidgetId;
}
Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(Activity.RESULT_CANCELED, resultValue);
activity = this;
}
public static void saveAndFinish(Context context) {
if (NotePreviewConfigureActivity.activity == null || appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return;
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
NotePreviewWidget.updateAppWidget(context, appWidgetManager, appWidgetId);
Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
NotePreviewConfigureActivity.activity.setResult(RESULT_OK, resultValue);
NotePreviewConfigureActivity.activity.finish();
}
@Override
protected String getMainComponentName() {
return "NotePreviewConfigure";
}
}

View File

@@ -1,75 +0,0 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.widget.RemoteViews;
import com.google.gson.Gson;
import com.streetwriters.notesnook.datatypes.Note;
public class NotePreviewWidget extends AppWidgetProvider {
static String OpenNoteId = "com.streetwriters.notesnook.OpenNoteId";
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), "");
if (data.isEmpty()) {
return;
}
Gson gson = new Gson();
Note note = gson.fromJson(data, Note.class);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget);
views.setTextViewText(R.id.widget_title, note.getTitle());
views.setTextViewText(R.id.widget_body, note.getHeadline());
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra(OpenNoteId, note.getId());
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
intent.setData(Uri.parse("https://app.notesnook.com/open_note?id=" + note.getId()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return activityOptions.toBundle();
} else
return null;
}
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
SharedPreferences.Editor edit = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).edit();
for (int id: appWidgetIds) {
edit.remove(String.valueOf(id));
}
edit.apply();
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
}

View File

@@ -1,65 +0,0 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.widget.RemoteViews;
/**
* Implementation of App Widget functionality.
*/
public class NoteWidget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_note_widget);
setClickIntent(context, views);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
static void setClickIntent(Context context, RemoteViews views) {
Intent intent = new Intent(context, ShareActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.new_note, pendingIntent);
}
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return activityOptions.toBundle();
} else
return null;
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
updateAppWidget(context, appWidgetManager, appWidgetId, newOptions);
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle options) {
int minWidth = options != null ? options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) : 0;
int minHeight = options != null ? options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) : 0;
int layoutId = (minWidth < 100) ? R.layout.new_note_widget_icon : R.layout.new_note_widget;
RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
setClickIntent(context, views);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
}

View File

@@ -1,34 +0,0 @@
package com.streetwriters.notesnook;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import androidx.annotation.RequiresApi;
@RequiresApi(api = Build.VERSION_CODES.N)
public class NotesnookTileService extends TileService {
@SuppressLint("StartActivityAndCollapseDeprecated")
@Override
public void onClick() {
super.onClick();
Intent intent = new Intent(this.getApplicationContext(), ShareActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(
PendingIntent.getActivity(
this.getApplicationContext(),
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
);
} else {
startActivityAndCollapse(intent);
}
}
}

View File

@@ -1,242 +0,0 @@
package com.streetwriters.notesnook;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.WindowManager;
import android.widget.RemoteViews;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.google.gson.Gson;
import com.streetwriters.notesnook.datatypes.Note;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class RCTNNativeModule extends ReactContextBaseJavaModule {
Intent lastIntent;
ReactContext mContext;
static String IntentType = "com.streetwriters.notesnook.IntentType";
public RCTNNativeModule(ReactApplicationContext reactContext) {
super(reactContext);
mContext = reactContext;
}
@Override
public String getName() {
return "NNativeModule";
}
@ReactMethod
public void setBackgroundColor(final String color) {
try {
getCurrentActivity().getWindow().getDecorView().setBackgroundColor(Color.parseColor(color));
} catch (Exception e) {
}
}
@ReactMethod
public void getActivityName(Promise promise) {
try {
promise.resolve(getCurrentActivity().getClass().getSimpleName());
} catch (Exception e) {
promise.resolve(null);
}
}
@ReactMethod
public void setSecureMode(final boolean mode) {
try {
getCurrentActivity().runOnUiThread(() -> {
try {
if (mode)
getCurrentActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
else
getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
} catch (Exception e) {
}
});
} catch (Exception e) {
}
}
@ReactMethod
public void setAppState(final String appState) {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = appStateDetails.edit();
edit.putString("appState", appState);
edit.apply();
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String getAppState() {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
String appStateValue = appStateDetails.getString("appState", "");
return appStateValue.isEmpty() ? null : appStateValue;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public int getWidgetId() {
return NotePreviewConfigureActivity.appWidgetId;
}
@ReactMethod
public void setString(final String storeName, final String key, final String value) {
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
SharedPreferences.Editor edit = details.edit();
edit.putString(key, value);
edit.apply();
}
@ReactMethod
public void removeString(final String storeName, final String key) {
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
SharedPreferences.Editor edit = details.edit();
edit.remove(key);
edit.apply();
}
@ReactMethod
public void getString(final String storeName, final String key, Promise promise) {
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
String value = details.getString(key, "");
promise.resolve(value.isEmpty() ? null : value);
}
@ReactMethod
public void saveAndFinish() {
NotePreviewConfigureActivity.saveAndFinish(mContext);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap getIntent() {
WritableMap map = Arguments.createMap();
if (getCurrentActivity() != null) {
Intent intent = getCurrentActivity().getIntent();
Bundle extras = getCurrentActivity().getIntent().getExtras();
if (extras != null && intent != lastIntent) {
lastIntent = intent;
if (Objects.equals(extras.getString(IntentType), "NewReminder")) {
map.putString(ReminderWidgetProvider.NewReminder, extras.getString(ReminderWidgetProvider.NewReminder));
} else if (Objects.equals(extras.getString(IntentType), "OpenReminder")) {
map.putString(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId));
} else if (Objects.equals(extras.getString(IntentType), "OpenNote")) {
map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId));
}
}
}
return map;
}
@ReactMethod
public void cancelAndFinish() {
NotePreviewConfigureActivity.activity.setResult(Activity.RESULT_CANCELED);
NotePreviewConfigureActivity.activity.finish();
}
@ReactMethod
public void getWidgetNotes(Promise promise) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
WritableArray arr = Arguments.createArray();
for(Map.Entry<String,?> entry : map.entrySet()){
if (entry.getKey().equals("remindersList")) continue;
String value = (String) entry.getValue();
Gson gson = new Gson();
Note note = gson.fromJson(value, Note.class);
arr.pushString(note.getId());
}
promise.resolve(arr);
}
@ReactMethod
public void hasWidgetNote(final String noteId, Promise promise) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
boolean found = false;
for(Map.Entry<String,?> entry : map.entrySet()){
String value = (String) entry.getValue();
if (value.contains(noteId)) {
found = true;
}
}
promise.resolve(found);
}
@ReactMethod
public void updateWidgetNote(final String noteId, final String data) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
SharedPreferences.Editor edit = pref.edit();
ArrayList<String> ids = new ArrayList<>();
for(Map.Entry<String,?> entry : map.entrySet()) {
String value = (String) entry.getValue();
if (value.contains(noteId)) {
edit.putString(entry.getKey(), data);
ids.add(entry.getKey());
}
}
edit.apply();
for (String id: ids) {
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id));
}
}
@ReactMethod
public void updateReminderWidget() {
AppWidgetManager wm = AppWidgetManager.getInstance(mContext);
int[] ids = wm.getAppWidgetIds(ComponentName.createRelative(mContext.getPackageName(), ReminderWidgetProvider.class.getName()));
for (int id: ids) {
Log.d("Reminders", "Updating" + id);
RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_reminders);
ReminderWidgetProvider.updateAppWidget(mContext, wm, id, views);
wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view);
}
}
@ReactMethod(isBlockingSynchronousMethod = true)
public boolean isGestureNavigationEnabled() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
String navBarMode = Settings.Secure.getString(
mContext.getContentResolver(),
"navigation_mode"
);
return "2".equals(navBarMode);
} catch (Exception e) {
return false;
}
} else {
return false;
}
}
}

View File

@@ -1,106 +0,0 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViewsService;
import android.content.Context;
import android.widget.RemoteViews;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.streetwriters.notesnook.datatypes.Reminder;
import java.util.ArrayList;
import java.util.List;
public class ReminderViewsService extends RemoteViewsService {
static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId";
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ReminderRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private Context context;
private List<Reminder> reminders;
public ReminderRemoteViewsFactory(Context context, Intent intent) {
this.context = context;
}
@Override
public void onCreate() {
// Initialize reminders list
reminders = new ArrayList<Reminder>();
}
@Override
public void onDataSetChanged() {
reminders.clear();
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Gson gson = new Gson();
reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken<List<Reminder>>(){}.getType());
}
@Override
public void onDestroy() {
reminders.clear();
}
@Override
public int getCount() {
return reminders.size();
}
@Override
public RemoteViews getViewAt(int position) {
Reminder reminder = reminders.get(position);
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
if (!useMiniLayout) {
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
}
views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime());
final Intent fillInIntent = new Intent();
final Bundle extras = new Bundle();
extras.putString(ReminderViewsService.OpenReminderId, reminder.getId());
fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId()));
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
fillInIntent.putExtras(extras);
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
return views;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return true;
}
}

View File

@@ -1,56 +0,0 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.widget.RemoteViews;
public class ReminderWidgetProvider extends AppWidgetProvider {
static String NewReminder = "com.streetwriters.notesnook.NewReminder";
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders);
updateAppWidget(context, appWidgetManager, appWidgetId, views);
}
}
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return activityOptions.toBundle();
} else
return null;
}
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) {
Intent listview_intent_template = new Intent(context, MainActivity.class);
listview_intent_template.setAction(Intent.ACTION_VIEW);
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle());
views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent);
Intent new_reminder_intent = new Intent(context, MainActivity.class);
new_reminder_intent.putExtra(NewReminder, NewReminder);
new_reminder_intent.setAction(Intent.ACTION_VIEW);
new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder");
new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder"));
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.add_button, pendingIntent2);
Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class);
list_remote_adapter_intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
views.setRemoteAdapter(R.id.widget_list_view, list_remote_adapter_intent);
views.setEmptyView(R.id.widget_list_view, R.id.empty_view);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}

View File

@@ -1,92 +0,0 @@
package com.streetwriters.notesnook.datatypes;
import androidx.annotation.Keep;
@Keep
public class BaseItem {
private String id;
private String type;
private long dateModified;
private long dateCreated;
private Boolean migrated;
private Boolean remote;
private Boolean synced;
private Boolean deleted;
/**
* @deprecated only kept here for migration purposes
*/
private Object deleteReason; // Assuming never can be represented as Object
// Getters and Setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public long getDateModified() {
return dateModified;
}
public void setDateModified(long dateModified) {
this.dateModified = dateModified;
}
public long getDateCreated() {
return dateCreated;
}
public void setDateCreated(long dateCreated) {
this.dateCreated = dateCreated;
}
public Boolean getMigrated() {
return migrated;
}
public void setMigrated(Boolean migrated) {
this.migrated = migrated;
}
public Boolean getRemote() {
return remote;
}
public void setRemote(Boolean remote) {
this.remote = remote;
}
public Boolean getSynced() {
return synced;
}
public void setSynced(Boolean synced) {
this.synced = synced;
}
public Boolean getDeleted() {
return deleted;
}
public void setDeleted(Boolean deleted) {
this.deleted = deleted;
}
public Object getDeleteReason() {
return deleteReason;
}
public void setDeleteReason(Object deleteReason) {
this.deleteReason = deleteReason;
}
}

View File

@@ -1,125 +0,0 @@
package com.streetwriters.notesnook.datatypes;
import androidx.annotation.Keep;
@Keep
public class Note extends BaseItem {
private String title;
private String headline;
private String contentId;
private Boolean locked;
private boolean pinned;
private boolean favorite;
private boolean localOnly;
private boolean conflicted;
private boolean readonly;
private long dateEdited;
private Object dateDeleted; // Assuming null can be represented as Object
private Object itemType; // Assuming null can be represented as Object
private Object deletedBy; // Assuming null can be represented as Object
// Getters and Setters
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getHeadline() {
return headline;
}
public void setHeadline(String headline) {
this.headline = headline;
}
public String getContentId() {
return contentId;
}
public void setContentId(String contentId) {
this.contentId = contentId;
}
public Boolean getLocked() {
return locked;
}
public void setLocked(Boolean locked) {
this.locked = locked;
}
public boolean isPinned() {
return pinned;
}
public void setPinned(boolean pinned) {
this.pinned = pinned;
}
public boolean isFavorite() {
return favorite;
}
public void setFavorite(boolean favorite) {
this.favorite = favorite;
}
public boolean isLocalOnly() {
return localOnly;
}
public void setLocalOnly(boolean localOnly) {
this.localOnly = localOnly;
}
public boolean isConflicted() {
return conflicted;
}
public void setConflicted(boolean conflicted) {
this.conflicted = conflicted;
}
public boolean isReadonly() {
return readonly;
}
public void setReadonly(boolean readonly) {
this.readonly = readonly;
}
public long getDateEdited() {
return dateEdited;
}
public void setDateEdited(long dateEdited) {
this.dateEdited = dateEdited;
}
public Object getDateDeleted() {
return dateDeleted;
}
public void setDateDeleted(Object dateDeleted) {
this.dateDeleted = dateDeleted;
}
public Object getItemType() {
return itemType;
}
public void setItemType(Object itemType) {
this.itemType = itemType;
}
public Object getDeletedBy() {
return deletedBy;
}
public void setDeletedBy(Object deletedBy) {
this.deletedBy = deletedBy;
}
}

View File

@@ -1,138 +0,0 @@
package com.streetwriters.notesnook.datatypes;
import androidx.annotation.Keep;
import java.util.concurrent.TimeUnit;
@Keep
public class Reminder extends BaseItem {
private String title;
private String description;
private String formattedTime;
private String priority; // "silent", "vibrate", "urgent"
private long date;
private String mode; // "repeat", "once", "permanent"
private String recurringMode; // "week", "month", "day", "year"
private int[] selectedDays;
private boolean localOnly;
private boolean disabled;
private long snoozeUntil;
// Getters and Setters
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getPriority() {
return priority;
}
public void setPriority(String priority) {
this.priority = priority;
}
public long getDate() {
return date;
}
public void setDate(long date) {
this.date = date;
}
public String getMode() {
return mode;
}
public void setMode(String mode) {
this.mode = mode;
}
public String getRecurringMode() {
return recurringMode;
}
public void setRecurringMode(String recurringMode) {
this.recurringMode = recurringMode;
}
public int[] getSelectedDays() {
return selectedDays;
}
public void setSelectedDays(int[] selectedDays) {
this.selectedDays = selectedDays;
}
public boolean isLocalOnly() {
return localOnly;
}
public void setLocalOnly(boolean localOnly) {
this.localOnly = localOnly;
}
public boolean isDisabled() {
return disabled;
}
public void setDisabled(boolean disabled) {
this.disabled = disabled;
}
public long getSnoozeUntil() {
return snoozeUntil;
}
public void setSnoozeUntil(long snoozeUntil) {
this.snoozeUntil = snoozeUntil;
}
public String getFormattedTime() {
return formattedTime;
}
public void setFormattedTime(String formattedTime) {
this.formattedTime = formattedTime;
}
public String formatTime(long timeInMillis) {
long currentTime = System.currentTimeMillis();
long diff = timeInMillis - currentTime;
if (diff < TimeUnit.MINUTES.toMillis(1)) {
return "in " + (diff / 1000) + " seconds";
} else if (diff < TimeUnit.HOURS.toMillis(1)) {
long minutes = TimeUnit.MILLISECONDS.toMinutes(diff);
return "in " + minutes + " minute" + (minutes > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(1)) {
long hours = TimeUnit.MILLISECONDS.toHours(diff);
return "in " + hours + " hour" + (hours > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(2)) {
return "tomorrow";
} else if (diff < TimeUnit.DAYS.toMillis(7)) {
long days = TimeUnit.MILLISECONDS.toDays(diff);
return "in " + days + " day" + (days > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(30)) {
long weeks = TimeUnit.MILLISECONDS.toDays(diff) / 7;
return "in " + weeks + " week" + (weeks > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(365)) {
long months = TimeUnit.MILLISECONDS.toDays(diff) / 30;
return "in " + months + " month" + (months > 1 ? "s" : "");
} else {
long years = TimeUnit.MILLISECONDS.toDays(diff) / 365;
return "in " + years + " year" + (years > 1 ? "s" : "");
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,14 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:id="@+id/new_note"
android:background="@drawable/layout_bg"
android:padding="@dimen/widget_margin">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="16dp"
android:src="@drawable/add_note" />
</FrameLayout>

View File

@@ -1,40 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/open_note"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:background="@drawable/layout_bg"
android:paddingHorizontal="10dp"
android:paddingVertical="10dp"
android:elevation="5dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:id="@+id/widget_title"
android:textColor="@color/text"
android:textSize="16sp"
android:textStyle="bold"
android:text="Widget unconfigured" />
<TextView
android:id="@+id/widget_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:textColor="@color/text"
android:textSize="14sp"
android:text="Configure this widget to show a note here." />
</LinearLayout>
</RelativeLayout>

View File

@@ -1,8 +0,0 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="No upcoming reminders"
android:textSize="16sp"
android:textColor="@android:color/darker_gray"
android:gravity="center" />

View File

@@ -1,29 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:id="@+id/reminder_item_btn">
<TextView
android:id="@+id/reminder_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textColor="@color/text"
android:textSize="16sp" />
<TextView
android:id="@+id/reminder_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text"
android:textSize="14sp" />
<TextView
android:id="@+id/reminder_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text" />
</LinearLayout>

View File

@@ -1,22 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:id="@+id/reminder_item_btn">
<TextView
android:id="@+id/reminder_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textColor="@color/text"
android:textSize="16sp" />
<TextView
android:id="@+id/reminder_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text" />
</LinearLayout>

View File

@@ -1,64 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/layout_bg"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:layout_gravity="center"
android:paddingTop="12dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginTop="2dp"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Upcoming Reminders"/>
<ImageButton
android:layout_width="35dp"
android:id="@+id/add_button"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:background="@drawable/ic_newnote" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="8dp"
android:background="@color/border" />
<ListView
android:id="@+id/widget_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@color/border"
android:paddingBottom="12dp"
android:paddingHorizontal="12dp"
android:dividerHeight="1dp" />
<LinearLayout
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textAlignment="center"
android:gravity="center"
android:text="Tap on + to add reminder"/>
</LinearLayout>
</LinearLayout>

View File

@@ -1,12 +0,0 @@
<resources>
<string name="app_name">Notesnook</string>
<string name="title_activity_share">NotesnookShare</string>
<string name="appwidget_text">EXAMPLE</string>
<string name="add_widget">Add widget</string>
<string name="take_a_quick_note">Take a quick note.</string>
<string name="reminders">Quick overview of upcoming reminders</string>
<string name="reminders_title">Reminders</string>
<string name="note">Note</string>
<string name="note_description">Add a note to home screen</string>
<string name="quick_note">Quick note</string>
</resources>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/new_note_widget"
android:initialLayout="@layout/new_note_widget"
android:minWidth="400dp"
android:minHeight="50dp"
android:minResizeWidth="50dp"
android:minResizeHeight="50dp"
android:description="@string/take_a_quick_note"
android:targetCellWidth="5"
android:targetCellHeight="1"
android:previewImage="@drawable/widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"/>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/note_widget"
android:initialLayout="@layout/note_widget"
android:configure="com.streetwriters.notesnook.NotePreviewConfigureActivity"
android:widgetFeatures="reconfigurable"
android:minResizeWidth="100dp"
android:minResizeHeight="50dp"
android:minWidth="400dp"
android:description="@string/note_description"
android:minHeight="50dp"
android:targetCellWidth="5"
android:targetCellHeight="1"
android:previewImage="@drawable/note_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"/>

View File

@@ -1,14 +0,0 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_reminders"
android:minWidth="400dp"
android:minHeight="100dp"
android:minResizeWidth="400dp"
android:minResizeHeight="50dp"
android:description="@string/reminders"
android:targetCellWidth="5"
android:targetCellHeight="2"
android:resizeMode="horizontal|vertical"
android:previewImage="@drawable/reminder_preview"
android:updatePeriodMillis="1024"
android:widgetCategory="home_screen"
/>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:networkSecurityConfig="@xml/network_security_config">
</application>
</manifest>

View File

@@ -1,6 +0,0 @@
- Get an overview of your year with Notesnook with Wrapped 2025
- Cell selection and column resizing in tables
- Added support for uploading large files in background
- Bug fixes and minor improvements
Thank you for using Notesnook!

View File

@@ -1,11 +0,0 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'Notesnook'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
include ":react-native-config"
project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android')
include ":lazysodium-android"
project(':lazysodium-android').projectDir = new File(rootProject.projectDir, '../node_modules/@ammarahmed/react-native-sodium/android/lazysodium-android/app')

View File

@@ -16,82 +16,91 @@ 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 { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import {
ScopedThemeProvider,
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { Appearance, I18nManager, StatusBar } from "react-native";
import { I18nManager, View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import DialogProvider from "./components/dialog-provider";
import { initializeLogger } from "./common/database/logger";
import AppLockedOverlay from "./components/app-lock-overlay";
import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { Toast } from "./components/toast";
import { useAppEvents } from "./hooks/use-app-events";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { RootNavigation } from "./navigation/navigation-stack";
import { ApplicationHolder } from "./navigation";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
RNBootSplash.hide({
fade: true
});
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
const App = () => {
const init = useAppEvents();
useEffect(() => {
changeSystemBarColors();
SettingsService.onFirstLaunch();
setTimeout(async () => {
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote();
}
TipManager.init();
}, 100);
initializeLogger()
.catch((e) => {
console.log(e);
})
.finally(() => {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<SafeAreaProvider>
<StatusBar translucent={true} backgroundColor="transparent" />
<View
style={{
height: "100%",
width: "100%"
}}
>
<View
style={{
position: "absolute",
width: "100%",
height: "100%",
zIndex: -1
}}
pointerEvents="none"
>
<SafeAreaProvider>
<GlobalSafeAreaProvider />
</SafeAreaProvider>
</View>
<GestureHandlerRootView
style={{
height: "100%",
width: "100%"
}}
>
<GlobalSafeAreaProvider />
{props.configureMode === "note-preview" ? (
<NotePreviewConfigure />
) : (
<RootNavigation />
)}
<ScopedThemeProvider value="dialog">
<Toast />
</ScopedThemeProvider>
<DialogProvider />
<AppLocked />
<ApplicationHolder />
</GestureHandlerRootView>
</SafeAreaProvider>
<AppLockedOverlay />
</View>
);
};
@@ -101,8 +110,8 @@ let currTheme =
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: (props: any) => JSX.Element) => {
return function AppWithThemeProvider(props: any) {
export const withTheme = (Element: () => JSX.Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,
@@ -121,44 +130,26 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
})
.then((theme) => {
if (theme) {
console.log(theme.version, "theme updated");
theme.colorScheme === "dark"
? useThemeStore.getState().setDarkTheme(theme)
: useThemeStore.getState().setLightTheme(theme);
}
})
.catch(() => {
/* empty */
});
.catch(console.log);
}, 1000);
const listener = Appearance.addChangeListener(({ colorScheme }) => {
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
useThemeStore
.getState()
.setColorScheme(colorScheme as "light" | "dark");
}
});
return () => {
listener.remove();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
currTheme = nextTheme;
}
}, [colorScheme, darkTheme, lightTheme]);
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
currTheme = nextTheme;
}
return (
<I18nProvider i18n={i18n}>
<Element {...props} />
</I18nProvider>
);
return <Element />;
};
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,156 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import { MMKV } from "../database/mmkv";
import { useSettingStore } from "../../stores/use-setting-store";
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
const baseUrl = "https://analytics.streetwriters.co/api/collect";
const UA =
Platform.OS === "ios"
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
: `
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
/**
*
* @param {Routes} route
* @param {Routes[]} conditions
* @returns
*/
async function canUpdateAnalytics(route, conditions = []) {
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
}
if (eventsList && eventsList[route]) {
console.log("analytics: event already sent", route);
return false;
}
if (route !== "/welcome") {
for (let cond of conditions) {
if (!eventsList || !eventsList[cond]) {
console.log("analytics: conditions not met for event", route, cond);
return false;
}
}
}
console.log("analytics: will send event", route);
return true;
}
async function saveAnalytics(route, value = true) {
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
} else {
eventsList = {};
}
eventsList[route] = value;
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
}
/**
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
* @param {Routes} prevRoute
* @param {Routes} route
* @param {Routes[]} conditions
* @param {boolean} once
* @returns
*/
async function pageView(
route,
prevRoute = "",
conditions = ["/welcome"],
once = true
) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
hostname: `notesnook-${Platform.OS}`,
language: "en-US",
screen: "1920x1080"
},
type: "pageview"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log("analytics: event sent", route);
await saveAnalytics(route);
return await response.text();
} catch (e) {
console.log(e);
}
}
async function sendEvent(type, value, once = true) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(type)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: "/",
event_type: type,
event_value: value,
hostname: "notesnook-android-app",
language: "en-US",
screen: "1920x1080"
},
type: "event"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log(response.status);
return await response.text();
} catch (e) {
console.log(e);
}
}
export default {
sendEvent,
pageView,
saveAnalytics
};

View File

@@ -0,0 +1,385 @@
/*
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 Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { MMKV } from "./mmkv";
import { ToastManager } from "../../services/event-manager";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
// app lock with password enabled.
export const CipherStorage = new MMKVLoader()
.withInstanceID("cipher_storage")
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
},
android: {}
});
function generatePassword() {
const length = 80;
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
"Crypto API is not supported. Please upgrade your web browser"
);
}
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
const indexes = crypto.getRandomValues(new Uint32Array(length));
let secret = "";
for (const index of indexes) {
secret += charset[index % charset.length];
}
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = await encrypt(appLockCredentials, key);
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
MMKV.removeItem(DB_KEY_CIPHER);
return true;
}
export async function setAppLockVerificationCipher(appLockPassword) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = await encrypt(appLockCredentials, generatePassword());
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
console.log(e);
}
}
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = credentials.password;
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
);
if (userKeyCredentials) {
const userKeyCipher = await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
return DB_KEY;
} catch (e) {
ToastManager.error(e, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKey(data) {
try {
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
{
key: await getDatabaseKey(),
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key
);
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey(_name) {
try {
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return null;
}
const key = await decrypt(
{
key: await getDatabaseKey(),
salt: keyCipher.salt
},
keyCipher
);
return key;
} catch (e) {
console.log("getCryptoKey", e);
DatabaseLogger.error(e);
}
}
export async function removeCryptoKey(_name) {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getRandomBytes(length) {
return await generateSecureRandom(length);
}
export async function hash(password, email) {
let result = await Sodium.hashPassword(password, email);
return result;
}
export async function generateCryptoKey(password, salt) {
try {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
DatabaseLogger.error(e);
}
}
export function getAlgorithm(base64Variant) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
data = data.map((d) => {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
encryptionAlgorithm: enc,
kdfAlgorithm: kdf,
compressionAlgorithm: compressionAlg,
isCompress: compressed === "1",
base64_variant: base64variant
};
}
export async function encrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let message = {
type: "plain",
data: data
};
let result = await Sodium.encrypt(password, message);
return {
...result,
alg: getAlgorithm(7)
};
}
export async function encryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let results = await Sodium.encryptMulti(
password,
data.map((item) => ({
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}

View File

@@ -1,476 +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 Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
import { SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
// app lock with password enabled.
export const CipherStorage = new MMKVLoader()
.withInstanceID("cipher_storage")
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
},
android: {}
});
function generatePassword() {
const length = 80;
//@ts-ignore
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
"Crypto API is not supported. Please upgrade your web browser"
);
}
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
const indexes = crypto.getRandomValues(new Uint32Array(length));
let secret = "";
for (const index of indexes) {
secret += charset[index % charset.length];
}
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER) as Cipher;
const databaseKey = (await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
)) as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
MMKV.removeItem(DB_KEY_CIPHER);
return true;
}
export async function setAppLockVerificationCipher(appLockPassword: string) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword: string) {
try {
const appLockCipher: Cipher = CipherStorage.getMap(
APPLOCK_CIPHER
) as Cipher;
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY: string | undefined;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(
"databaseKeyCipher"
) as Cipher;
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials =
await Keychain.getInternetCredentials("notesnook");
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
if (!DB_KEY) {
throw new Error(
`Failed to get database key, ${await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
)}`
);
}
return DB_KEY;
}
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
try {
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
);
if (!credentials) return;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function deriveCryptoKey(data: SerializedKey) {
try {
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const credentials = (await Sodium.deriveKey(
data.password,
data.salt
)) as Password;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return undefined;
}
const key = await decrypt(
{
key: (await getDatabaseKey()) as string,
salt: keyCipher.salt
},
keyCipher
);
return key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function removeCryptoKey() {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getRandomBytes(length: number) {
return await generateSecureRandom(length);
}
export async function hash(
password: string,
email: string,
options?: { usesFallback?: boolean }
) {
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
if (options?.usesFallback && Platform.OS !== "ios") {
return "";
}
return (
options?.usesFallback
? await Sodium.hashPasswordFallback?.(password, email)
: await Sodium.hashPassword(password, email)
) as string;
}
export async function generateCryptoKey(password: string, salt?: string) {
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
}
export async function generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return Sodium.deriveKeyFallback?.(
password,
salt as string
) as Promise<SerializedKey>;
}
export function getAlgorithm(base64Variant: number) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
const _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decrypt(key, _data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decrypt(fallbackKey, _data);
} else {
throw e;
}
}
}
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(
password: Password,
data: Cipher<"base64">[]
) {
data = data.map((d) => {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decryptMulti(key, data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt as string
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decryptMulti(fallbackKey, data);
} else {
throw e;
}
}
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg: string) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
encryptionAlgorithm: enc,
kdfAlgorithm: kdf,
compressionAlgorithm: compressionAlg,
isCompress: compressed === "1",
base64_variant: base64variant
};
}
export async function encrypt(password: SerializedKey, plainText: string) {
const result = await Sodium.encrypt<"base64">(password, {
type: "plain",
data: plainText
});
return {
...result,
alg: getAlgorithm(7)
};
}
export async function encryptMulti(
password: SerializedKey,
plainText: string[]
) {
const results = await Sodium.encryptMulti<"base64">(
password,
plainText.map((item) => ({
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}

View File

@@ -16,64 +16,58 @@ 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, getFeature, getFeatureLimit } from "@notesnook/common";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger } from "@notesnook/core/dist/logger";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { FileStorage } from "../filesystem";
import { getDatabaseKey } from "./encryption";
import "./logger";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { Storage } from "./storage";
import SettingsService from "../../services/settings";
import { getDatabaseKey } from "./encryption";
export async function setupDatabase(password?: string) {
database.host(
__DEV__
? {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: "http://192.168.43.5:5264",
// AUTH_HOST: "http://192.168.43.5:8264",
// SSE_HOST: "http://192.168.43.5:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
// ISSUES_HOST: "http://192.168.43.5:2624"
}
: {
API_HOST: "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 setupDatabase(password) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
if (!key)
throw new Error("Database setup failed, could not get database key");
// const base = `http://192.168.100.92`;
// database.host({
// API_HOST: `${base}:5264`,
// AUTH_HOST: `${base}:8264`,
// SSE_HOST: `${base}:7264`,
// ISSUES_HOST: `${base}:2624`,
// SUBSCRIPTIONS_HOST: `${base}:9264`,
// MONOGRAPH_HOST: `${base}:6264`,
// NOTESNOOK_HOST: `${base}:8788`
// });
database.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
NOTESNOOK_HOST: "https://notesnook.com",
...(SettingsService.getProperty("serverUrls") || {})
});
console.log("Opening database with key:", !!key);
database.setup({
storage: Storage,
eventsource: (Platform.OS === "ios"
? EventSource
: AndroidEventSource) as any,
fs: FileStorage,
compressor: async () =>
({
compress: Gzip.deflate,
decompress: Gzip.inflate
}) as ICompressor,
batchSize: 50,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({
createDriver: () => {
@@ -86,10 +80,6 @@ export async function setupDatabase(password?: string) {
tempStore: "memory",
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL",
password: key
},
maxNoteVersions: async () => {
const limit = await getFeatureLimit(getFeature("maxNoteVersions"));
return typeof limit.caption === "number" ? limit.caption : undefined;
}
});
}

View File

@@ -17,19 +17,13 @@ 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 { initialize } from "@notesnook/core";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import { initialize } from "@notesnook/core/dist/logger";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import { Platform } from "react-native";
import { setLogger } from ".";
import { RNSqliteDriver } from "./sqlite.kysely";
let loggerLoaded = false;
const initializeLogger = async () => {
if (loggerLoaded) return;
await initialize({
dialect: (name) => ({
createDriver: () => {
@@ -43,7 +37,6 @@ const initializeLogger = async () => {
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
});
setLogger();
loggerLoaded = true;
};
export { initializeLogger };

View File

@@ -17,16 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type {
DatabaseConnection,
Driver,
QueryResult
} from "@streetwriters/kysely";
import { CompiledQuery } from "@streetwriters/kysely";
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
type Config = { dbName: string; async: boolean; location?: string };
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
@@ -98,7 +93,7 @@ class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error(strings.streamingNotSupported());
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(

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