mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
135 Commits
release/v3
...
v3.1.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ade6e305db | ||
|
|
692635dc31 | ||
|
|
a2131b9f37 | ||
|
|
a79cbbc643 | ||
|
|
6ab8d1beba | ||
|
|
890d6a49f9 | ||
|
|
a532ebb2c7 | ||
|
|
29ecc3e7a5 | ||
|
|
0082c02e17 | ||
|
|
d3ed0d5571 | ||
|
|
e1034e3a99 | ||
|
|
aa46ae6c07 | ||
|
|
b4d1719e13 | ||
|
|
1094781774 | ||
|
|
f8f82b8975 | ||
|
|
a1c0d33bf2 | ||
|
|
2a89166724 | ||
|
|
4887434607 | ||
|
|
806f130832 | ||
|
|
cb72a46c29 | ||
|
|
7ba56357f3 | ||
|
|
b3718f25aa | ||
|
|
7abfa8e4ce | ||
|
|
ff02a2bcd1 | ||
|
|
d829c97a7e | ||
|
|
0048da3090 | ||
|
|
05e87d627a | ||
|
|
796f6f14b4 | ||
|
|
521c13dfa3 | ||
|
|
b2aaac01db | ||
|
|
eba96d483c | ||
|
|
8c12344123 | ||
|
|
b1edeb0f5b | ||
|
|
788023c95d | ||
|
|
8cd1be5d6b | ||
|
|
7b3b0fc9fe | ||
|
|
f0e46def68 | ||
|
|
880655e220 | ||
|
|
1bcc5c3ba4 | ||
|
|
db6e52ce07 | ||
|
|
2199b86f71 | ||
|
|
b0a7bf5c02 | ||
|
|
e233fe783e | ||
|
|
90b67c2a1c | ||
|
|
b6506a2ab4 | ||
|
|
22601b4181 | ||
|
|
710c28f3c2 | ||
|
|
25d4f2666f | ||
|
|
2fd23807a6 | ||
|
|
f4b3fc4f4e | ||
|
|
10742aa3fa | ||
|
|
74b70df6d7 | ||
|
|
658585cc1d | ||
|
|
af1f1510d0 | ||
|
|
e74a50675f | ||
|
|
34dc49ac4c | ||
|
|
17e105b3c6 | ||
|
|
f7e4ffc650 | ||
|
|
0dfd98a27b | ||
|
|
df90332eab | ||
|
|
15db924150 | ||
|
|
61552ec87f | ||
|
|
ef2a4f5126 | ||
|
|
47c959374c | ||
|
|
c2f32f02a2 | ||
|
|
d16db9ad23 | ||
|
|
3ccf016c65 | ||
|
|
5494c25aa8 | ||
|
|
b2dc460a7e | ||
|
|
6b664d9c56 | ||
|
|
076297dcd0 | ||
|
|
956fdd4bc9 | ||
|
|
60ad9801be | ||
|
|
4a00dc21a0 | ||
|
|
a63bd53622 | ||
|
|
990df2a392 | ||
|
|
97979e1631 | ||
|
|
5e7292f04d | ||
|
|
863d6bf465 | ||
|
|
1bb25b6128 | ||
|
|
5fe3c51c5f | ||
|
|
5a1cdf054b | ||
|
|
2ae2a19a6f | ||
|
|
b8d7a41e4b | ||
|
|
48156b86e6 | ||
|
|
b8b0dbbc3e | ||
|
|
f61ba08096 | ||
|
|
7d324b7999 | ||
|
|
fdb436adfa | ||
|
|
8fa1cb616d | ||
|
|
2c0a6eec48 | ||
|
|
8895f13c51 | ||
|
|
fb6f0a0c1e | ||
|
|
8ee3249ecb | ||
|
|
7fdf5ca10f | ||
|
|
286050b323 | ||
|
|
fd5af6a8eb | ||
|
|
f431ccb45c | ||
|
|
fcd3728a83 | ||
|
|
da9eeea741 | ||
|
|
54edbc3f2e | ||
|
|
04986b594a | ||
|
|
8972296445 | ||
|
|
3dddf995d0 | ||
|
|
d0197bf095 | ||
|
|
6a34319455 | ||
|
|
b65fcedf29 | ||
|
|
9addf95c76 | ||
|
|
55adcc906f | ||
|
|
f1f39f17da | ||
|
|
1dcb0a363c | ||
|
|
c6c6f62319 | ||
|
|
5faf8378a2 | ||
|
|
bcc7a6fb05 | ||
|
|
a44650ee1a | ||
|
|
e734e87c14 | ||
|
|
232dfad3ac | ||
|
|
f61c868eed | ||
|
|
8e211d65ce | ||
|
|
9a0d6fbbb4 | ||
|
|
9256aec1c9 | ||
|
|
2734965bce | ||
|
|
453ccf0a89 | ||
|
|
564e7190e8 | ||
|
|
9c32fe9c2e | ||
|
|
5a23223a85 | ||
|
|
a7c8cef69c | ||
|
|
fa37d6cc25 | ||
|
|
c52eb4608d | ||
|
|
c156defd57 | ||
|
|
a0b4553344 | ||
|
|
c3080e9446 | ||
|
|
c020b0ae7c | ||
|
|
7802e4a530 | ||
|
|
9978532f57 |
@@ -7,7 +7,7 @@ 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
|
||||
|
||||
5
.github/workflows/android.e2e.yml
vendored
5
.github/workflows/android.e2e.yml
vendored
@@ -47,11 +47,6 @@ jobs:
|
||||
- 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
|
||||
|
||||
@@ -69,16 +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: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
|
||||
10
.github/workflows/android.publish.yml
vendored
10
.github/workflows/android.publish.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -69,16 +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: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
|
||||
23
.github/workflows/core.tests.yml
vendored
23
.github/workflows/core.tests.yml
vendored
@@ -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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- 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
|
||||
|
||||
127
.github/workflows/desktop.publish.yml
vendored
127
.github/workflows/desktop.publish.yml
vendored
@@ -45,7 +45,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -65,13 +65,17 @@ jobs:
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
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
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Generate desktop build (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
run: BETA=true npm run tx @notesnook/web:build:desktop
|
||||
run: BETA=true npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Build desktop bundle
|
||||
working-directory: ./apps/desktop
|
||||
@@ -103,8 +107,7 @@ 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 }}
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
@@ -195,9 +198,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
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build zip and dmg
|
||||
env:
|
||||
@@ -208,13 +211,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
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --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,8 +232,8 @@ 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
|
||||
@@ -247,76 +250,9 @@ 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
|
||||
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 x64 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:x64 -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:x64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -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
|
||||
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:
|
||||
@@ -331,33 +267,35 @@ jobs:
|
||||
- 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
|
||||
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:arm64 -p never
|
||||
# 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
|
||||
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
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 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
|
||||
- name: Publish on Snapcraft
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
@@ -396,13 +334,10 @@ jobs:
|
||||
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:
|
||||
|
||||
29
.github/workflows/desktop.tests.yml
vendored
29
.github/workflows/desktop.tests.yml
vendored
@@ -6,16 +6,10 @@ on:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/desktop/**"
|
||||
- "app/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:
|
||||
@@ -33,8 +27,12 @@ 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 desktop build
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -66,7 +64,8 @@ jobs:
|
||||
npm run bootstrap -- --scope=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 app
|
||||
run: |
|
||||
@@ -109,7 +108,8 @@ jobs:
|
||||
npm run bootstrap -- --scope=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 app
|
||||
run: |
|
||||
@@ -158,7 +158,8 @@ jobs:
|
||||
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 app
|
||||
run: |
|
||||
@@ -204,13 +205,11 @@ jobs:
|
||||
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
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
|
||||
14
.github/workflows/editor.tests.yml
vendored
14
.github/workflows/editor.tests.yml
vendored
@@ -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"
|
||||
@@ -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
|
||||
|
||||
20
.github/workflows/ios.publish.yml
vendored
20
.github/workflows/ios.publish.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
@@ -14,18 +14,13 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.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
|
||||
@@ -59,16 +54,7 @@ jobs:
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
|
||||
|
||||
- name: Install Pods
|
||||
run: |
|
||||
cd apps/mobile/native/ios
|
||||
bundle install
|
||||
RCT_NEW_ARCH_ENABLED=0 bundle exec 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
|
||||
|
||||
6
.github/workflows/monograph.publish.yml
vendored
6
.github/workflows/monograph.publish.yml
vendored
@@ -39,13 +39,13 @@ jobs:
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
|
||||
- name: Generate build
|
||||
run: npm run tx @notesnook/monograph:build
|
||||
run: npx nx build @notesnook/monograph
|
||||
|
||||
# Setup Buildx
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
context: apps/monograph
|
||||
file: apps/monograph/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
|
||||
cache-from: streetwriters/monograph:latest
|
||||
|
||||
|
||||
1
.github/workflows/theme-builder.publish.yml
vendored
1
.github/workflows/theme-builder.publish.yml
vendored
@@ -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
|
||||
|
||||
|
||||
1
.github/workflows/vericrypt.publish.yml
vendored
1
.github/workflows/vericrypt.publish.yml
vendored
@@ -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
|
||||
|
||||
|
||||
4
.github/workflows/web.benchmarks.yml
vendored
4
.github/workflows/web.benchmarks.yml
vendored
@@ -27,6 +27,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: Build
|
||||
run: npm run build:web
|
||||
|
||||
|
||||
1
.github/workflows/web.publish.yml
vendored
1
.github/workflows/web.publish.yml
vendored
@@ -30,6 +30,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
|
||||
|
||||
|
||||
25
.github/workflows/web.tests.yml
vendored
25
.github/workflows/web.tests.yml
vendored
@@ -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' }}
|
||||
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 }}
|
||||
|
||||
- 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
|
||||
|
||||
@@ -67,8 +54,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,5 +11,4 @@ nx-cloud.env
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
/*.patch
|
||||
.taskcache
|
||||
*.patch
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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,28 +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 for development:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
### Release mode
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
@@ -67,10 +58,10 @@ This will compile and run the app in production mode but it won't generate any p
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 --publish never
|
||||
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
|
||||
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
|
||||
|
||||
@@ -17,226 +17,230 @@ 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 { test } from "vitest";
|
||||
import { harness } from "./utils.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);
|
||||
test("update starts downloading if version is outdated", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async ({ page }) => {
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
t.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
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
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 page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /updating/i })
|
||||
.waitFor({ state: "attached" });
|
||||
},
|
||||
{ version: "3.0.0" }
|
||||
);
|
||||
|
||||
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);
|
||||
test("update is only shown if version is outdated and auto updates are disabled", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
await ctx.relaunch();
|
||||
|
||||
await ctx.relaunch();
|
||||
const { page } = ctx;
|
||||
|
||||
const { page } = ctx;
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
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
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
await skipDialog(page);
|
||||
|
||||
await skipDialog(page);
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
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);
|
||||
});
|
||||
.waitFor({ state: "attached" });
|
||||
},
|
||||
{ version: "3.0.0" }
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
test("update to stable if it is newer", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "stable"
|
||||
})
|
||||
);
|
||||
await ctx.relaunch();
|
||||
|
||||
await ctx.relaunch();
|
||||
const { page } = ctx;
|
||||
|
||||
const { page } = ctx;
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
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
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
await skipDialog(page);
|
||||
|
||||
await skipDialog(page);
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
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] || "";
|
||||
t.expect(gt(version, "3.0.0-beta.0")).toBe(true);
|
||||
},
|
||||
{ version: "3.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
test("update is not available if it latest stable version is older", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.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" });
|
||||
|
||||
t.expect(
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.isHidden()
|
||||
).toBe(true);
|
||||
},
|
||||
{ version: "99.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
|
||||
test("downgrade to stable on switching to stable release track", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "stable"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.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] || "";
|
||||
t.expect(lt(version, "99.0.0-beta.0")).toBe(true);
|
||||
},
|
||||
{ version: "99.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
await page
|
||||
.waitForSelector(".ReactModal__Content", {
|
||||
timeout: 1000
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(async () => {
|
||||
const positiveButton = page.locator(
|
||||
"button[data-role='positive-button']"
|
||||
);
|
||||
const negativeButton = page.locator(
|
||||
"button[data-role='negative-button']"
|
||||
);
|
||||
if (await positiveButton.isVisible()) await positiveButton.click();
|
||||
else if (await negativeButton.isVisible()) await negativeButton.click();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +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 { buildApp } from "./utils";
|
||||
|
||||
export default async function setup() {
|
||||
await buildApp();
|
||||
}
|
||||
@@ -17,24 +17,22 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { testCleanup, test } from "./test-override.js";
|
||||
import { test } from "vitest";
|
||||
import { harness } from "./utils.js";
|
||||
import assert from "assert";
|
||||
|
||||
test("make sure app loads", async ({
|
||||
ctx: { page },
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
test("make sure app loads", async (t) => {
|
||||
await harness(t, async ({ page }) => {
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
assert.ok(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
);
|
||||
|
||||
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
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 */
|
||||
});
|
||||
}
|
||||
@@ -18,198 +18,144 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { cp } from "fs/promises";
|
||||
import { mkdir } from "fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path, { join, resolve } from "path";
|
||||
import path from "path";
|
||||
import { _electron as electron } from "playwright";
|
||||
import { existsSync } from "fs";
|
||||
import slugify from "slugify";
|
||||
import { TaskContext } from "vitest";
|
||||
|
||||
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 {
|
||||
interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
page: import("playwright").Page;
|
||||
configPath: string;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface TestOptions {
|
||||
interface TestOptions {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface Fixtures {
|
||||
options: TestOptions;
|
||||
ctx: AppContext;
|
||||
export async function harness(
|
||||
t: TaskContext,
|
||||
cb: (ctx: AppContext) => Promise<void>,
|
||||
options?: TestOptions
|
||||
) {
|
||||
const ctx = await buildAndLaunchApp(options);
|
||||
|
||||
t.onTestFinished(async (result) => {
|
||||
if (result.state === "fail") {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await ctx.page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(t.task.name)}-${process.platform}-${
|
||||
process.arch
|
||||
}-error.png`
|
||||
)
|
||||
});
|
||||
}
|
||||
await ctx.app.close();
|
||||
});
|
||||
|
||||
await cb(ctx);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
|
||||
const productName = makeid(10);
|
||||
const executablePath = await buildApp({ ...options, productName });
|
||||
const { app, page, configPath } = await launchApp(executablePath);
|
||||
const ctx: AppContext = {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
userDataDir,
|
||||
outputDir,
|
||||
relaunch: async () => {
|
||||
const { app, page, configPath, userDataDir } = await launchApp(
|
||||
executablePath,
|
||||
productName,
|
||||
options?.version
|
||||
);
|
||||
const { app, page, configPath } = await launchApp(executablePath);
|
||||
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
|
||||
);
|
||||
async function launchApp(executablePath: string) {
|
||||
const app = await electron.launch({
|
||||
executablePath,
|
||||
args: IS_DEBUG ? [] : ["--hidden"],
|
||||
env: {
|
||||
...(process.platform === "linux"
|
||||
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
|
||||
}
|
||||
: {})
|
||||
}
|
||||
: (process.env as Record<string, string>)
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
|
||||
const configPath = path.join(userDataDir, "UserData", "config.json");
|
||||
const userDataDirectory = await app.evaluate((a) => {
|
||||
return a.app.getPath("userData");
|
||||
});
|
||||
const configPath = path.join(userDataDirectory, "config.json");
|
||||
return {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
userDataDir
|
||||
configPath
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
async function buildApp({
|
||||
version,
|
||||
productName
|
||||
}: {
|
||||
version?: string;
|
||||
productName: string;
|
||||
}) {
|
||||
const buildRoot = path.join("test-artifacts", `${productName}-build`);
|
||||
const output = path.join("test-artifacts", `${productName}-output`);
|
||||
execSync(`npm run release -- --root ${buildRoot} --skip-tsc-build`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore"
|
||||
});
|
||||
|
||||
return appDir;
|
||||
const args = [
|
||||
`--config electron-builder.config.js`,
|
||||
`--c.extraMetadata.productName=${productName}`,
|
||||
"--publish=never"
|
||||
];
|
||||
if (version) args.push(`--c.extraMetadata.version=${version}`);
|
||||
|
||||
execSync(`npx electron-builder --dir --${process.arch} ${args.join(" ")}`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
NOTESNOOK_STAGING: "true",
|
||||
NN_BUILD_ROOT: buildRoot,
|
||||
NN_PRODUCT_NAME: productName,
|
||||
NN_APP_ID: `com.notesnook.test.${productName}`,
|
||||
NN_OUTPUT_DIR: output
|
||||
}
|
||||
});
|
||||
|
||||
return path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
output,
|
||||
process.platform === "linux"
|
||||
? process.arch === "arm64"
|
||||
? "linux-arm64-unpacked"
|
||||
: "linux-unpacked"
|
||||
: process.platform === "darwin"
|
||||
? process.arch === "arm64"
|
||||
? `mac-arm64/${productName}.app/Contents/MacOS/`
|
||||
: `mac/${productName}.app/Contents/MacOS/`
|
||||
: "win-unpacked",
|
||||
process.platform === "win32" ? `${productName}.exe` : productName
|
||||
);
|
||||
}
|
||||
|
||||
function makeid(length: number) {
|
||||
|
||||
@@ -38,20 +38,13 @@ const linuxExecutableName = process.env.NN_PRODUCT_NAME
|
||||
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}/**/*"
|
||||
],
|
||||
asar: false,
|
||||
files: [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
@@ -64,7 +57,6 @@ module.exports = {
|
||||
"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}",
|
||||
@@ -78,20 +70,6 @@ module.exports = {
|
||||
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"
|
||||
],
|
||||
@@ -182,27 +160,29 @@ module.exports = {
|
||||
description: "Your private note taking space",
|
||||
executableName: linuxExecutableName,
|
||||
desktop: {
|
||||
desktopActions: {
|
||||
"new-note": {
|
||||
Name: "New note",
|
||||
Exec: `${linuxExecutableName} new note`
|
||||
actions: [
|
||||
{
|
||||
id: "new-note",
|
||||
name: "New note",
|
||||
args: "new note"
|
||||
},
|
||||
"new-notebook": {
|
||||
Name: "New notebook",
|
||||
Exec: `${linuxExecutableName} new notebook`
|
||||
{
|
||||
id: "new-notebook",
|
||||
name: "New notebook",
|
||||
args: "new notebook"
|
||||
},
|
||||
"new-reminder": {
|
||||
Name: "New reminder",
|
||||
Exec: `${linuxExecutableName} new reminder`
|
||||
{
|
||||
id: "new-reminder",
|
||||
name: "New reminder",
|
||||
args: "new reminder"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
snap: {
|
||||
autoStart: false,
|
||||
confinement: "strict",
|
||||
allowNativeWayland: true,
|
||||
base: "core22"
|
||||
allowNativeWayland: true
|
||||
},
|
||||
extraResources: ["app-update.yml", "./assets/**"],
|
||||
extraMetadata: {
|
||||
|
||||
2676
apps/desktop/package-lock.json
generated
2676
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,15 +2,13 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.5",
|
||||
"version": "3.1.0-beta.5",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"sideEffects": [
|
||||
"src/overrides.ts"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
@@ -29,42 +27,37 @@
|
||||
"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",
|
||||
"better-sqlite3-multiple-ciphers": "11.5.0",
|
||||
"electron-trpc": "0.7.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"icojs": "^0.19.5",
|
||||
"sqlite-better-trigram": "0.0.3",
|
||||
"sqlite3-fts5-html": "^0.0.4",
|
||||
"electron-updater": "^6.3.4",
|
||||
"icojs": "^0.19.4",
|
||||
"sqlite-better-trigram": "0.0.2",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "3.24.3"
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/node": "22.7.0",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"chokidar": "^4.0.1",
|
||||
"electron": "^31.7.4",
|
||||
"electron-builder": "^25.1.8",
|
||||
"esbuild": "0.21.5",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
"playwright": "1.48.2",
|
||||
"node-abi": "^3.68.0",
|
||||
"node-gyp-build": "^4.8.2",
|
||||
"playwright": "^1.48.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^7.8.0",
|
||||
"vitest": "^3.2.4"
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"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",
|
||||
|
||||
36
apps/desktop/patches/app-builder-lib+25.0.5.patch
Normal file
36
apps/desktop/patches/app-builder-lib+25.0.5.patch
Normal 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 739a48c..7a7b4af 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
+ actions: undefined
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
+
|
||||
+ if (targetSpecificOptions.desktop.actions) {
|
||||
+ let actionsData = "";
|
||||
+ const validActions = [];
|
||||
+ for (const action of targetSpecificOptions.desktop.actions) {
|
||||
+ if (!action.id || !action.name || !action.args) continue;
|
||||
+ actionsData += "\n";
|
||||
+ actionsData += `[Desktop Action ${action.id}]
|
||||
+Name=${action.name}
|
||||
+Exec=${desktopMeta.Exec} ${action.args}`;
|
||||
+ actionsData += "\n";
|
||||
+
|
||||
+ validActions.push(action.id);
|
||||
+ }
|
||||
+
|
||||
+ data += `Actions=${validActions.join(";")};\n${actionsData}`
|
||||
+ }
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
}
|
||||
@@ -1,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: [
|
||||
@@ -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',
|
||||
],
|
||||
}
|
||||
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
@@ -0,0 +1,39 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 07971db..b67a3cf 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -537,8 +537,15 @@ class be {
|
||||
attachWindow(r) {
|
||||
F(this, w).includes(r) || (y("Attaching window", r.id), F(this, w).push(r), v(this, N, Z).call(this, r));
|
||||
}
|
||||
- detachWindow(r) {
|
||||
- y("Detaching window", r.id), L(this, w, F(this, w).filter((e) => e !== r)), v(this, R, M).call(this, { webContentsId: r.webContents.id });
|
||||
+ detachWindow(r, webContentsId) {
|
||||
+ y("Detaching window", r.id);
|
||||
+
|
||||
+ if (r.isDestroyed() && webContentsId === undefined) {
|
||||
+ throw new Error('webContentsId is required when calling detachWindow on a destroyed window');
|
||||
+ }
|
||||
+
|
||||
+ L(this, w, F(this, w).filter((e) => e !== r));
|
||||
+ v(this, R, M).call(this, { webContentsId: webContentsId ?? r.webContents.id });
|
||||
}
|
||||
}
|
||||
w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
@@ -548,6 +555,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
for (const [i, a] of F(this, E).entries())
|
||||
i.startsWith(`${r}-${e ?? ""}`) && (y("Closing subscription", i), a.unsubscribe(), F(this, E).delete(i));
|
||||
}, N = new WeakSet(), Z = function(r) {
|
||||
+ const webContentsId = r.webContents.id;
|
||||
r.webContents.on("did-start-navigation", ({ frame: e }) => {
|
||||
y(
|
||||
"Handling webContents `did-start-navigation` event",
|
||||
@@ -558,7 +566,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
frameRoutingId: e.routingId
|
||||
});
|
||||
}), r.webContents.on("destroyed", () => {
|
||||
- y("Handling webContents `destroyed` event"), this.detachWindow(r);
|
||||
+ y("Handling webContents `destroyed` event"), this.detachWindow(r, webContentsId);
|
||||
});
|
||||
};
|
||||
const me = ({
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
console.log("starting build...");
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
@@ -27,7 +26,6 @@ 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);
|
||||
@@ -36,27 +34,17 @@ const skipTscBuild = args.skipTscBuild || false;
|
||||
|
||||
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");
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
console.log("rebuilding...");
|
||||
await exec(
|
||||
"node scripts/execute.mjs @notesnook/web:build:desktop",
|
||||
"npx 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"), {
|
||||
recursive: true,
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { readFile, rm, writeFile } from "fs/promises";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
@@ -25,8 +25,6 @@ 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,
|
||||
"..",
|
||||
@@ -36,23 +34,15 @@ export async function patchBetterSQLite3() {
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
delete json.homepage;
|
||||
delete json.repository;
|
||||
json.version = "11.5.1";
|
||||
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||
json.repository.url =
|
||||
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||
|
||||
await writeFile(jsonPath, JSON.stringify(json));
|
||||
|
||||
await rm(
|
||||
path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"better-sqlite3-multiple-ciphers",
|
||||
"build"
|
||||
),
|
||||
{ force: true, recursive: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (process.argv[1] === __filename) {
|
||||
console.log("Patching better-sqlite3");
|
||||
patchBetterSQLite3();
|
||||
}
|
||||
|
||||
@@ -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 { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -42,7 +33,6 @@ import { isFlatpak, isSnap } 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();
|
||||
|
||||
@@ -235,63 +225,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")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -48,6 +46,8 @@ export class SQLite {
|
||||
this.sqlite = require("better-sqlite3-multiple-ciphers")(
|
||||
filePath
|
||||
).unsafeMode(true);
|
||||
const betterTrigram = require("sqlite-better-trigram");
|
||||
betterTrigram.load(this.sqlite);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,29 +115,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 +142,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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -182,17 +181,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();
|
||||
|
||||
|
||||
@@ -143,44 +143,16 @@ function setupMenu() {
|
||||
})
|
||||
);
|
||||
|
||||
if (params.isEditable) {
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: strings.paste(),
|
||||
role: "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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,13 +21,11 @@ import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120 * 1000,
|
||||
hookTimeout: 120 * 1000,
|
||||
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000,
|
||||
sequence: {
|
||||
concurrent: true,
|
||||
shuffle: true
|
||||
},
|
||||
globalSetup: "./__tests__/global-setup.ts",
|
||||
dir: "./__tests__/",
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
|
||||
1
apps/mobile/.gitignore
vendored
1
apps/mobile/.gitignore
vendored
@@ -69,7 +69,6 @@ buck-out/
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
native/vendor
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { Appearance, I18nManager, StatusBar } from "react-native";
|
||||
import { I18nManager, StatusBar } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -129,15 +129,6 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
/* empty */
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
useThemeStore.getState().setColorScheme(colorScheme);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -94,9 +94,7 @@ export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(
|
||||
DB_KEY_CIPHER
|
||||
) as Cipher;
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = (await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
@@ -137,9 +135,7 @@ export async function clearAppLockVerificationCipher() {
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(
|
||||
APPLOCK_CIPHER
|
||||
) as Cipher;
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
@@ -163,9 +159,7 @@ export function clearDatabaseKey() {
|
||||
export async function getDatabaseKey(appLockPassword?: string) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(
|
||||
"databaseKeyCipher"
|
||||
) as Cipher;
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
@@ -299,7 +293,7 @@ export async function deriveCryptoKey(data: SerializedKey) {
|
||||
|
||||
export async function getCryptoKey() {
|
||||
try {
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return undefined;
|
||||
|
||||
@@ -16,7 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { database, getFeature, getFeatureLimit } from "@notesnook/common";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger, ICompressor } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import {
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@streetwriters/kysely";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import SettingsService from "../../services/settings";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { FileStorage } from "../filesystem";
|
||||
@@ -33,24 +34,11 @@ import { getDatabaseKey } from "./encryption";
|
||||
import "./logger";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { Storage } from "./storage";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export async function setupDatabase(password?: string) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
||||
|
||||
// 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",
|
||||
@@ -58,7 +46,6 @@ export async function setupDatabase(password?: string) {
|
||||
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") || {})
|
||||
});
|
||||
|
||||
@@ -86,10 +73,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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,12 +136,6 @@ export const Storage: IStorage = {
|
||||
clear(): Promise<void> {
|
||||
return DefaultStorage.clear();
|
||||
},
|
||||
generateCryptoKeyPair() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
decryptAsymmetric() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
getAllKeys(): Promise<string[]> {
|
||||
return DefaultStorage.getAllKeys();
|
||||
},
|
||||
|
||||
@@ -149,9 +149,7 @@ export async function downloadFile(
|
||||
});
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
|
||||
@@ -137,7 +137,6 @@ export async function deleteFile(
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
@@ -222,7 +221,6 @@ export async function deleteCacheFileByName(name: string) {
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache") || file.startsWith("NN_")) {
|
||||
|
||||
@@ -74,6 +74,21 @@ export async function uploadFile(
|
||||
return true;
|
||||
}
|
||||
|
||||
const uploadUrlResponse = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
|
||||
const uploadUrl = uploadUrlResponse.ok
|
||||
? await uploadUrlResponse.text()
|
||||
: await uploadUrlResponse.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
}
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
const uploadRequest = RNFetchBlob.config({
|
||||
@@ -82,9 +97,8 @@ export async function uploadFile(
|
||||
})
|
||||
.fetch(
|
||||
"PUT",
|
||||
url,
|
||||
uploadUrl,
|
||||
{
|
||||
...headers,
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(filePath)
|
||||
|
||||
@@ -25,17 +25,10 @@ import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const Announcement = ({ color }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [announcements, remove] = useMessageStore((state) => [
|
||||
state.announcements,
|
||||
state.remove
|
||||
]);
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
@@ -64,38 +57,6 @@ export const Announcement = ({ color }) => {
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.secondary.heading} size={AppFontSize.xxs}>
|
||||
{strings.announcement()}
|
||||
</Heading>
|
||||
|
||||
<Button
|
||||
type="plain"
|
||||
icon="close"
|
||||
height={null}
|
||||
onPress={() => {
|
||||
remove(announcement.id);
|
||||
}}
|
||||
iconSize={20}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{announcement?.body
|
||||
.filter((item) => allowedOnPlatform(item.platforms))
|
||||
.map((item, index) =>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eCloseAnnouncementDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PricingPlans } from "../premium/pricing-plans";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { allowedOnPlatform, getStyle } from "./functions";
|
||||
@@ -44,6 +45,18 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
Linking.openURL(item.data).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else if (item.type === "promo") {
|
||||
presentSheet({
|
||||
component: (
|
||||
<PricingPlans
|
||||
marginTop={1}
|
||||
promo={{
|
||||
promoCode: item.data,
|
||||
text: item.title
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
@@ -51,8 +64,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
...getStyle(style),
|
||||
flexDirection: inline ? "row" : "column",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
flexDirection: inline ? "row" : "column"
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="premium_cta" />
|
||||
@@ -87,10 +99,12 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
fontSize={AppFontSize.sm}
|
||||
type="plain"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
paddingHorizontal: 0,
|
||||
marginLeft: 12
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
@@ -105,6 +119,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={AppFontSize.md}
|
||||
buttonType={{
|
||||
color: color ? color : colors.primary.accent,
|
||||
text: color
|
||||
@@ -114,8 +129,10 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
opacity: 1
|
||||
}}
|
||||
onPress={() => onPress(item)}
|
||||
width={250}
|
||||
style={{
|
||||
width: "100%"
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -132,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
height={30}
|
||||
style={{
|
||||
minWidth: "50%",
|
||||
width: "100%"
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
|
||||
@@ -21,17 +21,16 @@ import React from "react";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Description = ({ text, style = {}, inline }) => {
|
||||
return (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginHorizontal: 12,
|
||||
...getStyle(style),
|
||||
textAlign: inline ? "left" : style?.textAlign
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
size={inline ? AppFontSize.sm : AppFontSize.md}
|
||||
>
|
||||
{text}
|
||||
</Paragraph>
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { useCallback } from "react";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
/**
|
||||
* Test announcement
|
||||
@@ -139,9 +138,9 @@ export const AnnouncementDialog = () => {
|
||||
maxHeight: DDS.isTab ? "90%" : "100%",
|
||||
borderRadius: DDS.isTab ? 10 : 0,
|
||||
overflow: "hidden",
|
||||
paddingVertical: DefaultAppStyles.GAP,
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15
|
||||
marginBottom: DDS.isTab ? 20 : 0,
|
||||
borderTopRightRadius: 10,
|
||||
borderTopLeftRadius: 10
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
|
||||
@@ -27,6 +27,10 @@ import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Title = ({ text, style = {}, inline }) => {
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const remove = useMessageStore((state) => state.remove);
|
||||
|
||||
return inline ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -38,21 +42,46 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
...getStyle(style),
|
||||
textAlign: inline ? "left" : style?.textAlign,
|
||||
flexShrink: 1
|
||||
}}
|
||||
numberOfLines={1}
|
||||
size={inline ? AppFontSize.md : AppFontSize.xl}
|
||||
>
|
||||
{inline ? text?.toUpperCase() : text}
|
||||
</Heading>
|
||||
|
||||
<Button
|
||||
type="plain"
|
||||
icon="close"
|
||||
height={null}
|
||||
onPress={() => {
|
||||
remove(announcement.id);
|
||||
}}
|
||||
hitSlop={{
|
||||
left: 15,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
right: 0
|
||||
}}
|
||||
iconSize={24}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
marginRight: 12,
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginHorizontal: DefaultAppStyles.GAP,
|
||||
...getStyle(style),
|
||||
marginTop: style?.marginTop || DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
|
||||
@@ -54,8 +54,6 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -111,9 +109,6 @@ const AppLocked = () => {
|
||||
);
|
||||
|
||||
const onUnlockAppRequested = useCallback(async () => {
|
||||
editorController.current?.commands.blur(useTabStore.getState().currentTab);
|
||||
editorController.current?.commands.blurPassInput();
|
||||
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
@@ -299,9 +294,7 @@ const AppLocked = () => {
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
) {
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 32);
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
@@ -336,6 +329,7 @@ const AppLocked = () => {
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={AppFontSize.md}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -167,7 +167,6 @@ const Actions = ({
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
positiveText: strings.rename()
|
||||
});
|
||||
|
||||
@@ -19,6 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
@@ -28,7 +31,9 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Toast } from "../toast";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -46,6 +51,7 @@ const AuthModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenLoginDialog, open);
|
||||
@@ -93,18 +99,81 @@ const AuthModal = () => {
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title={strings.skip()}
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
|
||||
@@ -17,20 +17,26 @@ 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 React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import BackupService from "../../services/backup";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { Dialog } from "../dialog";
|
||||
import BackupService from "../../services/backup";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const passwordInputRef = useRef();
|
||||
@@ -79,7 +85,8 @@ export const ChangePassword = () => {
|
||||
context: "global"
|
||||
});
|
||||
setLoading(false);
|
||||
Navigation.goBack();
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
@@ -101,6 +108,9 @@ export const ChangePassword = () => {
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<DialogHeader title={strings.changePassword()} />
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={oldPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
@@ -150,3 +160,9 @@ export const ChangePassword = () => {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ChangePassword.present = () => {
|
||||
presentSheet({
|
||||
component: <ChangePassword />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,18 +25,13 @@ export const AuthMode = {
|
||||
login: 0,
|
||||
signup: 1,
|
||||
welcomeSignup: 2,
|
||||
welcomeLogin: 3,
|
||||
trialSignup: 4
|
||||
trialSignup: 3
|
||||
};
|
||||
|
||||
export const initialAuthMode = createRef(0);
|
||||
export function hideAuth(context) {
|
||||
export function hideAuth() {
|
||||
eSendEvent(eCloseLoginDialog);
|
||||
if (
|
||||
initialAuthMode.current === AuthMode.welcomeSignup ||
|
||||
initialAuthMode.current === AuthMode.welcomeLogin ||
|
||||
context === "intro"
|
||||
) {
|
||||
if (initialAuthMode.current === AuthMode.welcomeSignup) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
|
||||
@@ -1,78 +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 { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const route = useRoute();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth((route.params as any)?.context);
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.welcome ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -17,29 +17,99 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { AuthMode, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const Auth = ({ navigation, route }) => {
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(
|
||||
route?.params?.mode || AuthMode.login
|
||||
);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
initialAuthMode.current = route?.params.mode || AuthMode.login;
|
||||
useNavigationFocus(navigation, {});
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: colors.primary.background }}
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login &&
|
||||
currentAuthMode !== AuthMode.welcomeLogin ? (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: insets.top,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
backgroundColor:
|
||||
deviceMode === "mobile" ? colors.secondary.background : null,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
if (initialAuthMode.current === 2) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title={strings.skip()}
|
||||
onPress={() => {
|
||||
if (initialAuthMode.current === 2) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
@@ -53,7 +123,7 @@ const Auth = ({ navigation, route }) => {
|
||||
)}
|
||||
|
||||
<Toast context="local" />
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,22 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import Sync from "../../services/sync";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
@@ -42,9 +37,8 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { AuthHeader } from "./header";
|
||||
import { useLogin } from "./use-login";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -55,7 +49,6 @@ const LoginSteps = {
|
||||
export const Login = ({ changeMode }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [focused, setFocused] = useState(false);
|
||||
const route = useRoute();
|
||||
const {
|
||||
step,
|
||||
setStep,
|
||||
@@ -68,24 +61,15 @@ export const Login = ({ changeMode }) => {
|
||||
setError,
|
||||
login
|
||||
} = useLogin(async () => {
|
||||
hideAuth();
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
await sleep(500);
|
||||
hideAuth();
|
||||
Progress.present();
|
||||
setTimeout(() => {
|
||||
if (!useUserStore.getState().syncing) {
|
||||
Sync.run("global", false, "full");
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
if (!PremiumService.get() && !SettingsService.getProperty("serverUrls")) {
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "signup",
|
||||
state: route.params.state,
|
||||
canGoBack: false
|
||||
});
|
||||
} else {
|
||||
Progress.present();
|
||||
}
|
||||
});
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
@@ -104,227 +88,217 @@ export const Login = ({ changeMode }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<ForgotPassword />
|
||||
<Dialog context="two_factor_verify" />
|
||||
<KeyboardAwareScrollView
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "70%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
loading={loading}
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title={strings.cancelLogin()}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
type="secondaryAccented"
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
width: 250
|
||||
}}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
/>
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title={strings.cancelLogin()}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: 250
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="secondaryAccented"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.signUp()}
|
||||
</Paragraph>
|
||||
{strings.signUp()}
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { useContext } from "react";
|
||||
|
||||
export const SignupContext = React.createContext<{
|
||||
signup?: () => Promise<boolean>;
|
||||
}>({
|
||||
signup: undefined
|
||||
});
|
||||
|
||||
export const useSignupContext = () => useContext(SignupContext);
|
||||
@@ -19,36 +19,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Loading } from "../loading";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { hideAuth } from "./common";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
selectPlan: 1,
|
||||
createAccount: 2
|
||||
};
|
||||
|
||||
export const Signup = ({ changeMode, welcome }) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
@@ -62,8 +52,6 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
const route = useRoute();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
@@ -82,26 +70,23 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(email.current.toLowerCase(), password.current);
|
||||
let user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
if (!SettingsService.getProperty("serverUrls")) {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: false,
|
||||
state: route.params.state,
|
||||
context: "signup"
|
||||
});
|
||||
hideAuth();
|
||||
SettingsService.setProperty("encryptedBackup", true);
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
} else {
|
||||
PremiumService.showVerifyEmailDialog();
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.signupFailed(),
|
||||
@@ -109,264 +94,214 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SignupContext.Provider
|
||||
value={{
|
||||
signup: signup
|
||||
}}
|
||||
>
|
||||
{currentStep === SignupSteps.signup ? (
|
||||
<>
|
||||
<AuthHeader welcome={welcome} />
|
||||
<KeyboardAwareScrollView
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "70%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
flexDirection: "row"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "50%",
|
||||
minHeight: height * 0.25
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.createYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DDS.isTab ? 0 : 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
blurOnSubmit={false}
|
||||
onSubmit={() => {
|
||||
if (!email.current) return;
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
defaultValue={password.current}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
if (!password.current) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
defaultValue={confirmPassword.current}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
signup();
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[1]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser(
|
||||
"https://notesnook.com/privacy",
|
||||
colors
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loading
|
||||
title={"Setting up your account..."}
|
||||
description="Your account is almost ready, please wait..."
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.email()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SignupContext.Provider>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[1]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
style={{
|
||||
width: 250
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,8 +24,12 @@ import { View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database/index";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
@@ -34,7 +38,6 @@ import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -43,7 +46,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
method: mfaInfo?.primaryMethod,
|
||||
isPrimary: true
|
||||
});
|
||||
const { seconds, start, reset } = useTimer(currentMethod.method);
|
||||
const { seconds, start } = useTimer(currentMethod.method);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const inputRef = useRef();
|
||||
const [sending, setSending] = useState(false);
|
||||
@@ -59,7 +62,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -130,17 +133,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 60
|
||||
}}
|
||||
onLayout={() => {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 500);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -211,15 +203,13 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
cursorColor={colors.selected.accent}
|
||||
selectionHandleColor={colors.selected.accent}
|
||||
selectionColor={colors.selected.accent}
|
||||
onSubmitEditing={onNext}
|
||||
caretHidden
|
||||
height={60}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
textAlign: "center",
|
||||
letterSpacing: 7,
|
||||
letterSpacing: 10,
|
||||
width: 250
|
||||
}}
|
||||
keyboardType={
|
||||
@@ -227,6 +217,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
borderWidth: 0,
|
||||
width: undefined,
|
||||
minWidth: "50%"
|
||||
}}
|
||||
wrapperStyle={{
|
||||
@@ -244,10 +236,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
reset();
|
||||
onCancel();
|
||||
}}
|
||||
onPress={onCancel}
|
||||
width={250}
|
||||
/>
|
||||
|
||||
@@ -308,7 +297,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
};
|
||||
|
||||
TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
|
||||
presentDialog({
|
||||
presentSheet({
|
||||
component: () => (
|
||||
<TwoFactorVerification
|
||||
onMfaLogin={onMfaLogin}
|
||||
@@ -317,9 +306,7 @@ TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
|
||||
/>
|
||||
),
|
||||
context: context || "two_factor_verify",
|
||||
disableClosing: true,
|
||||
transparent: false,
|
||||
statusBarTranslucent: true
|
||||
disableClosing: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -17,17 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useRef, useState } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -35,18 +34,15 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (
|
||||
onFinishLogin?: () => void,
|
||||
sessionExpired = false
|
||||
) => {
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const email = useRef<string>();
|
||||
const password = useRef<string>();
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const email = useRef();
|
||||
const password = useRef();
|
||||
const emailInputRef = useRef();
|
||||
const passwordInputRef = useRef();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (
|
||||
@@ -73,15 +69,11 @@ export const useLogin = (
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
if (!email.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa: any, callback: (success: boolean) => void) => {
|
||||
async (mfa, callback) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
@@ -99,8 +91,8 @@ export const useLogin = (
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
if (e.message === "invalid_grant") {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
@@ -108,7 +100,7 @@ export const useLogin = (
|
||||
},
|
||||
mfaInfo,
|
||||
() => {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
@@ -119,14 +111,10 @@ export const useLogin = (
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
if (!email.current || !password.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
undefined,
|
||||
null,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
@@ -135,11 +123,11 @@ export const useLogin = (
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
finishWithError(e as Error);
|
||||
finishWithError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const finishWithError = async (e: Error) => {
|
||||
const finishWithError = async (e) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -158,7 +146,7 @@ export const useLogin = (
|
||||
clearMessage();
|
||||
ToastManager.show({
|
||||
heading: strings.loginSuccess(),
|
||||
message: strings.loginSuccessDesc(user.email),
|
||||
message: strings.loginSuccessDesc(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -35,8 +35,6 @@ import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { AppFontSize, normalize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
|
||||
interface FloatingButtonProps {
|
||||
onPress: () => void;
|
||||
@@ -149,9 +147,7 @@ const FloatingButton = ({
|
||||
justifyContent: "center",
|
||||
height: normalize(size === "small" ? 40 : 60),
|
||||
width: normalize(size === "small" ? 40 : 60),
|
||||
backgroundColor: color
|
||||
? RGB_Linear_Shade(0.87, hexToRGBA(color))
|
||||
: colors.primary.shade,
|
||||
backgroundColor: colors.primary.shade,
|
||||
borderRadius: size === "small" ? 15 : 20
|
||||
}}
|
||||
>
|
||||
@@ -165,7 +161,7 @@ const FloatingButton = ({
|
||||
? "delete"
|
||||
: "plus"
|
||||
}
|
||||
color={color || colors.primary.accent}
|
||||
color={colors.primary.accent}
|
||||
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -31,39 +31,37 @@ import ResultDialog from "../dialogs/result";
|
||||
import { VaultDialog } from "../dialogs/vault";
|
||||
import ImagePreview from "../image-preview";
|
||||
import MergeConflicts from "../merge-conflicts";
|
||||
import PremiumDialog from "../premium";
|
||||
import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import Progress from "../dialogs/progress";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppLockPassword />
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<PremiumDialog colors={colors} />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<SheetProvider />
|
||||
<SheetProvider context="sync_progress" />
|
||||
<Dialog context="global" />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<Expiring />
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<Progress />
|
||||
|
||||
{isAppLoading ? null : (
|
||||
<>
|
||||
<MergeConflicts />
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ import { notesnook } from "../../../e2e/test.ids";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Button, ButtonProps } from "../ui/button";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
const DialogButtons = ({
|
||||
@@ -37,14 +37,6 @@ const DialogButtons = ({
|
||||
loading,
|
||||
doneText,
|
||||
positiveType
|
||||
}: {
|
||||
onPressPositive?: () => void;
|
||||
onPressNegative: () => void;
|
||||
positiveTitle: string;
|
||||
negativeTitle?: string;
|
||||
loading?: boolean;
|
||||
doneText?: string;
|
||||
positiveType?: ButtonProps["type"];
|
||||
}) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
|
||||
@@ -21,7 +21,7 @@ import React from "react";
|
||||
import { Text, View, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button, ButtonProps } from "../ui/button";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableProps } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -31,7 +31,13 @@ type DialogHeaderProps = {
|
||||
icon?: string;
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
button?: ButtonProps;
|
||||
button?: {
|
||||
onPress?: () => void;
|
||||
loading?: boolean;
|
||||
title?: string;
|
||||
type?: PressableProps["type"];
|
||||
icon?: string;
|
||||
};
|
||||
paragraphColor?: string;
|
||||
padding?: number;
|
||||
centered?: boolean;
|
||||
@@ -89,14 +95,17 @@ const DialogHeader = ({
|
||||
|
||||
{button ? (
|
||||
<Button
|
||||
onPress={button.onPress}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
loading={button.loading}
|
||||
fontSize={13}
|
||||
title={button.title}
|
||||
icon={button.icon}
|
||||
type={button.type || "secondary"}
|
||||
height={30}
|
||||
{...button}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -17,20 +17,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { KeyboardTypeOptions } from "react-native";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
|
||||
import { ButtonProps } from "../ui/button";
|
||||
|
||||
export type DialogInfo = {
|
||||
type DialogInfo = {
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
positiveText: string;
|
||||
negativeText: string;
|
||||
background?: string;
|
||||
transparent?: boolean;
|
||||
statusBarTranslucent?: boolean;
|
||||
positivePress?: (...args: any[]) => Promise<any>;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (...args: any[]) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
@@ -49,18 +44,16 @@ export type DialogInfo = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: KeyboardTypeOptions;
|
||||
keyboardType?: string;
|
||||
check?: {
|
||||
info: string;
|
||||
type?: ButtonProps["type"];
|
||||
type?: string;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
notice: {
|
||||
text: string;
|
||||
type: "alert" | "information";
|
||||
};
|
||||
disableBackdropClosing: boolean;
|
||||
component: JSX.Element | ((close?: () => void) => JSX.Element);
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View, ViewStyle } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
@@ -40,26 +40,42 @@ import Seperator from "../ui/seperator";
|
||||
import BaseDialog from "./base-dialog";
|
||||
import DialogButtons from "./dialog-buttons";
|
||||
import DialogHeader from "./dialog-header";
|
||||
import { DialogInfo } from "./functions";
|
||||
|
||||
export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState<boolean>();
|
||||
const [checked, setChecked] = useState<boolean>();
|
||||
const [loading, setLoading] = useState<boolean>();
|
||||
const values = useRef<{
|
||||
inputValue?: string;
|
||||
}>({
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const values = useRef({
|
||||
inputValue: undefined
|
||||
});
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [dialogInfo, setDialogInfo] = useState<DialogInfo>();
|
||||
const inputRef = useRef();
|
||||
const [dialogInfo, setDialogInfo] = useState({
|
||||
title: "",
|
||||
paragraph: "",
|
||||
positiveText: strings.done(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: () => {},
|
||||
onClose: () => {},
|
||||
positiveType: "transparent",
|
||||
icon: null,
|
||||
paragraphColor: colors.primary.paragraph,
|
||||
input: false,
|
||||
inputPlaceholder: "Enter some text",
|
||||
defaultValue: "",
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent",
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
const onPressPositive = async () => {
|
||||
if (dialogInfo?.positivePress) {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
setLoading(true);
|
||||
let result = false;
|
||||
let result;
|
||||
try {
|
||||
result = await dialogInfo.positivePress(
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
@@ -81,7 +97,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
};
|
||||
|
||||
const show = useCallback(
|
||||
(data: DialogInfo) => {
|
||||
(data) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
@@ -92,14 +108,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
[context]
|
||||
);
|
||||
|
||||
const hide = React.useCallback(() => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setVisible(false);
|
||||
setDialogInfo(undefined);
|
||||
dialogInfo?.onClose?.();
|
||||
}, [dialogInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
@@ -110,14 +118,21 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
};
|
||||
}, [hide, show]);
|
||||
|
||||
const hide = React.useCallback(() => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setVisible(false);
|
||||
dialogInfo.onClose?.();
|
||||
}, [dialogInfo]);
|
||||
|
||||
const onNegativePress = async () => {
|
||||
if (dialogInfo?.onClose) {
|
||||
if (dialogInfo.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
hide();
|
||||
};
|
||||
|
||||
const style: ViewStyle = {
|
||||
const style = {
|
||||
...getElevationStyle(5),
|
||||
width: DDS.isTab ? 400 : "85%",
|
||||
maxHeight: 450,
|
||||
@@ -128,19 +143,11 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
overflow: "hidden"
|
||||
};
|
||||
|
||||
return visible && dialogInfo ? (
|
||||
return visible ? (
|
||||
<BaseDialog
|
||||
statusBarTranslucent={
|
||||
dialogInfo.statusBarTranslucent === undefined
|
||||
? false
|
||||
: dialogInfo.statusBarTranslucent
|
||||
}
|
||||
statusBarTranslucent={false}
|
||||
bounce={!dialogInfo.input}
|
||||
closeOnTouch={!dialogInfo.disableBackdropClosing}
|
||||
background={dialogInfo.background}
|
||||
transparent={
|
||||
dialogInfo.transparent === undefined ? true : dialogInfo.transparent
|
||||
}
|
||||
onShow={async () => {
|
||||
if (dialogInfo.input) {
|
||||
inputRef.current?.setNativeProps({
|
||||
@@ -153,99 +160,89 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
visible={true}
|
||||
onRequestClose={hide}
|
||||
>
|
||||
{typeof dialogInfo.component === "function"
|
||||
? dialogInfo.component(() => hide())
|
||||
: dialogInfo.component}
|
||||
<View style={style}>
|
||||
<DialogHeader
|
||||
title={dialogInfo.title}
|
||||
icon={dialogInfo.icon}
|
||||
paragraph={dialogInfo.paragraph}
|
||||
paragraphColor={dialogInfo.paragraphColor}
|
||||
padding={12}
|
||||
style={{
|
||||
minHeight: 0
|
||||
}}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
{dialogInfo.component ? null : (
|
||||
<View style={style}>
|
||||
<DialogHeader
|
||||
title={dialogInfo.title}
|
||||
icon={dialogInfo.icon}
|
||||
paragraph={dialogInfo.paragraph}
|
||||
paragraphColor={dialogInfo.paragraphColor}
|
||||
padding={12}
|
||||
{dialogInfo.input ? (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 0
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
{dialogInfo.input ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
>
|
||||
<Input
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.inputValue = value;
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.inputValue = value;
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
//defaultValue={dialogInfo.defaultValue}
|
||||
onSubmit={() => {
|
||||
onPressPositive();
|
||||
}}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
//defaultValue={dialogInfo.defaultValue}
|
||||
onSubmit={onPressPositive}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setChecked(!checked);
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
icon={
|
||||
checked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
iconColor={checked ? colors.secondary.icon : colors.primary.icon}
|
||||
style={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setChecked(!checked);
|
||||
}}
|
||||
icon={
|
||||
checked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
iconColor={
|
||||
checked ? colors.secondary.icon : colors.primary.icon
|
||||
}
|
||||
style={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
loading={loading}
|
||||
positiveTitle={dialogInfo.positiveText}
|
||||
negativeTitle={dialogInfo.negativeText}
|
||||
positiveType={dialogInfo.positiveType}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
loading={loading}
|
||||
positiveTitle={dialogInfo.positiveText}
|
||||
negativeTitle={dialogInfo.negativeText}
|
||||
positiveType={dialogInfo.positiveType}
|
||||
/>
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
) : null;
|
||||
@@ -16,9 +16,6 @@ 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 { useIsFeatureAvailable } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Image,
|
||||
@@ -28,13 +25,15 @@ import {
|
||||
View
|
||||
} from "react-native";
|
||||
import { Image as ImageType } from "react-native-image-crop-picker";
|
||||
import { presentSheet, ToastManager } from "../../../services/event-manager";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Notice } from "../../ui/notice";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export default function AttachImage({
|
||||
response,
|
||||
@@ -45,7 +44,6 @@ export default function AttachImage({
|
||||
onAttach: ({ compress }: { compress: boolean }) => void;
|
||||
close: ((ctx?: string | undefined) => void) | undefined;
|
||||
}) {
|
||||
const fullQualityImagesResult = useIsFeatureAvailable("fullQualityImages");
|
||||
const { colors } = useThemeColors();
|
||||
const [compress, setCompress] = useState(true);
|
||||
|
||||
@@ -106,42 +104,21 @@ export default function AttachImage({
|
||||
alignSelf: "center",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
opacity: fullQualityImagesResult?.isAllowed ? 1 : 0.5
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!fullQualityImagesResult?.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: fullQualityImagesResult?.error,
|
||||
type: "info",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCompress(!compress);
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size={AppFontSize.lg}
|
||||
name={compress ? "checkbox-marked" : "checkbox-blank-outline"}
|
||||
color={
|
||||
compress && fullQualityImagesResult?.isAllowed
|
||||
? colors.primary.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
color={compress ? colors.primary.accent : colors.primary.icon}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!fullQualityImagesResult?.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: fullQualityImagesResult?.error,
|
||||
type: "info",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCompress(!compress);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -17,15 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Dimensions, TextInput, View } from "react-native";
|
||||
import Orientation from "react-native-orientation-locker";
|
||||
import Pdf from "react-native-pdf";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import downloadAttachment from "../../../common/filesystem/download-attachment";
|
||||
import { deleteCacheFileByPath, exists } from "../../../common/filesystem/io";
|
||||
import { cacheDir } from "../../../common/filesystem/utils";
|
||||
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
@@ -33,9 +30,8 @@ import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
@@ -43,6 +39,11 @@ import SheetProvider from "../../sheet-provider";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
@@ -104,19 +105,15 @@ const PDFPreview = () => {
|
||||
const open = useCallback(
|
||||
async (attachment) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
setAttachment(attachment);
|
||||
let hash = attachment.hash;
|
||||
if (!hash) return;
|
||||
if (!(await exists(hash))) setLoading(true);
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
if (!(await exists(hash))) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
snapshotValue.current = snapshot.current;
|
||||
setPDFSource("file://" + path);
|
||||
@@ -169,7 +166,8 @@ const PDFPreview = () => {
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<View
|
||||
<Animated.View
|
||||
exiting={FadeOut}
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
@@ -190,7 +188,7 @@ const PDFPreview = () => {
|
||||
>
|
||||
{strings.loadingWithProgress(progress?.percent)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
@@ -244,7 +242,7 @@ const PDFPreview = () => {
|
||||
textAlign: "center",
|
||||
marginRight: 4,
|
||||
borderRadius: 3,
|
||||
fontFamily: "Inter-Regular"
|
||||
fontFamily: "OpenSans-Regular"
|
||||
}}
|
||||
selectTextOnFocus
|
||||
keyboardType="decimal-pad"
|
||||
@@ -272,42 +270,49 @@ const PDFPreview = () => {
|
||||
</View>
|
||||
</View>
|
||||
{pdfSource ? (
|
||||
<Pdf
|
||||
source={{
|
||||
uri: pdfSource
|
||||
}}
|
||||
ref={pdfRef}
|
||||
onLoadComplete={(numberOfPages) => {
|
||||
setNumPages(numberOfPages);
|
||||
}}
|
||||
onPageChanged={(page) => {
|
||||
setCurrentPage(page);
|
||||
inputRef.current?.setNativeProps({
|
||||
text: page + ""
|
||||
});
|
||||
saveSnapshot({
|
||||
currentPage: page,
|
||||
scale: snapshot?.current?.scale
|
||||
});
|
||||
}}
|
||||
// scale={snapshotValue.current?.scale}
|
||||
// onScaleChanged={(scale) => {
|
||||
// saveSnapshot({
|
||||
// currentPage: snapshot?.current?.currentPage,
|
||||
// scale: scale
|
||||
// });
|
||||
// }}
|
||||
page={snapshotValue?.current?.currentPage}
|
||||
password={password}
|
||||
maxScale={6}
|
||||
onError={onError}
|
||||
onPressLink={(uri) => {}}
|
||||
<Animated.View
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width,
|
||||
height: Dimensions.get("window").height
|
||||
flex: 1
|
||||
}}
|
||||
/>
|
||||
entering={FadeIn}
|
||||
>
|
||||
<Pdf
|
||||
source={{
|
||||
uri: pdfSource
|
||||
}}
|
||||
ref={pdfRef}
|
||||
onLoadComplete={(numberOfPages) => {
|
||||
setNumPages(numberOfPages);
|
||||
}}
|
||||
onPageChanged={(page) => {
|
||||
setCurrentPage(page);
|
||||
inputRef.current?.setNativeProps({
|
||||
text: page + ""
|
||||
});
|
||||
saveSnapshot({
|
||||
currentPage: page,
|
||||
scale: snapshot?.current?.scale
|
||||
});
|
||||
}}
|
||||
// scale={snapshotValue.current?.scale}
|
||||
// onScaleChanged={(scale) => {
|
||||
// saveSnapshot({
|
||||
// currentPage: snapshot?.current?.currentPage,
|
||||
// scale: scale
|
||||
// });
|
||||
// }}
|
||||
page={snapshotValue?.current?.currentPage}
|
||||
password={password}
|
||||
maxScale={6}
|
||||
onError={onError}
|
||||
onPressLink={(uri) => {}}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width,
|
||||
height: Dimensions.get("window").height
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -135,6 +135,7 @@ const ResultDialog = () => {
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
onPress={close}
|
||||
fontSize={AppFontSize.md + 2}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -30,7 +30,6 @@ import { BackHandler, Platform, ViewProps } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -68,7 +67,6 @@ export interface TabsRef {
|
||||
setScrollEnabled: () => true;
|
||||
isDrawerOpen: () => boolean;
|
||||
node: RefObject<Animated.View>;
|
||||
tabChangedFromSwipeAction: SharedValue<boolean>;
|
||||
}
|
||||
|
||||
export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
@@ -118,7 +116,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
const isLoaded = useRef(false);
|
||||
const prevWidths = useRef(widths);
|
||||
const isIPhone = Platform.OS === "ios";
|
||||
const tabChangedFromSwipeAction = useSharedValue(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (deviceMode === "tablet" || fullscreen) {
|
||||
@@ -194,7 +191,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
: withTiming(editorPosition);
|
||||
currentTab.value = 2;
|
||||
}
|
||||
tabChangedFromSwipeAction.value = false;
|
||||
},
|
||||
goToIndex: (index: number, animated = true) => {
|
||||
if (deviceMode === "tablet") {
|
||||
@@ -214,7 +210,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
: editorPosition;
|
||||
currentTab.value = 2;
|
||||
}
|
||||
tabChangedFromSwipeAction.value = false;
|
||||
},
|
||||
unlock: () => {
|
||||
forcedLock.value = false;
|
||||
@@ -257,8 +252,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
},
|
||||
page: () => (currentTab.value === 1 ? "home" : "editor"),
|
||||
setScrollEnabled: () => true,
|
||||
node: node,
|
||||
tabChangedFromSwipeAction: tabChangedFromSwipeAction
|
||||
node: node
|
||||
}),
|
||||
[
|
||||
currentTab,
|
||||
@@ -268,8 +262,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
homePosition,
|
||||
editorPosition,
|
||||
forcedLock,
|
||||
isDrawerOpen,
|
||||
tabChangedFromSwipeAction
|
||||
isDrawerOpen
|
||||
]
|
||||
);
|
||||
|
||||
@@ -309,7 +302,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
const gesture = Gesture.Pan()
|
||||
.maxPointers(1)
|
||||
.enabled(enabled && !disabled)
|
||||
.activeOffsetX([-5, 5])
|
||||
.activeOffsetX([-10, 10])
|
||||
.failOffsetY([-10, 10])
|
||||
.onBegin((event) => {
|
||||
locked.value = false;
|
||||
@@ -369,20 +362,18 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
isDrawerOpen.value = true;
|
||||
currentTab.value = 1;
|
||||
runOnJS(onDrawerStateChange)(true);
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
} else if (!isSwipeLeft && finalValue > 100) {
|
||||
translateX.value = withSpring(homePosition, animationConfig);
|
||||
isDrawerOpen.value = false;
|
||||
currentTab.value = 1;
|
||||
runOnJS(onDrawerStateChange)(false);
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
|
||||
return;
|
||||
} else if (!isSwipeLeft && finalValue < 100) {
|
||||
translateX.value = withSpring(0, animationConfig);
|
||||
isDrawerOpen.value = true;
|
||||
currentTab.value = 1;
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -393,8 +384,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
translateX.value = withSpring(editorPosition, animationConfig);
|
||||
currentTab.value = 2;
|
||||
isDrawerOpen.value = false;
|
||||
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -409,8 +398,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
translateX.value = withSpring(editorPosition, animationConfig);
|
||||
currentTab.value = 2;
|
||||
}
|
||||
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +97,9 @@ export const Header = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: DefaultAppStyles.GAP_SMALL,
|
||||
borderRadius: 10,
|
||||
paddingVertical: 3,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL - 2,
|
||||
borderWidth: hasSearch ? 1 : 0,
|
||||
borderColor: colors.primary.border,
|
||||
paddingHorizontal: !hasSearch ? 0 : DefaultAppStyles.GAP_SMALL,
|
||||
|
||||
@@ -20,22 +20,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Linking, useWindowDimensions, View } from "react-native";
|
||||
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const Intro = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const isTablet = width > 600;
|
||||
|
||||
@@ -116,102 +115,75 @@ const Intro = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
<ScrollView
|
||||
testID="notesnook.splashscreen"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
<View
|
||||
testID="notesnook.splashscreen"
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
width: "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7 - (insets.top + insets.bottom)
|
||||
},
|
||||
isTablet && {
|
||||
width: width / 2,
|
||||
alignSelf: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 20,
|
||||
marginTop: 50
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: "100%",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
flexGrow: 1
|
||||
},
|
||||
isTablet && {
|
||||
width: width / 2,
|
||||
alignSelf: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 20,
|
||||
marginTop: 50
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SwiperFlatList
|
||||
autoplay
|
||||
autoplayDelay={10}
|
||||
autoplayLoop={true}
|
||||
index={0}
|
||||
useReactNativeGestureHandler={true}
|
||||
showPagination
|
||||
data={strings.introData}
|
||||
paginationActiveColor={colors.primary.accent}
|
||||
paginationStyleItem={{
|
||||
width: 10,
|
||||
height: 5,
|
||||
marginRight: 4,
|
||||
marginLeft: 4
|
||||
}}
|
||||
paginationDefaultColor={colors.primary.border}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
<SwiperFlatList
|
||||
autoplay
|
||||
autoplayDelay={10}
|
||||
autoplayLoop={true}
|
||||
index={0}
|
||||
useReactNativeGestureHandler={true}
|
||||
showPagination
|
||||
data={strings.introData}
|
||||
paginationActiveColor={colors.primary.accent}
|
||||
paginationStyleItem={{
|
||||
width: 10,
|
||||
height: 5,
|
||||
marginRight: 4,
|
||||
marginLeft: 4
|
||||
}}
|
||||
paginationDefaultColor={colors.primary.border}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: isTablet ? "50%" : "100%",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: isTablet ? 0 : DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
flexShrink: 1,
|
||||
alignSelf: "center"
|
||||
minHeight: height * 0.3
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
SettingsService.set({ introCompleted: true });
|
||||
Navigation.push("Auth", {
|
||||
mode: AuthMode.welcomeSignup
|
||||
});
|
||||
}}
|
||||
fontSize={AppFontSize.md}
|
||||
type="accent"
|
||||
title={strings.getStarted()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
title={strings.iAlreadyHaveAnAccount()}
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.push("Auth", {
|
||||
mode: AuthMode.welcomeLogin,
|
||||
context: "intro"
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,8 +44,9 @@ export const Header = React.memo(
|
||||
}: ListHeaderProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return (
|
||||
return selectionMode ? null : (
|
||||
<>
|
||||
{announcements.length !== 0 && !noAnnouncement ? (
|
||||
<Announcement color={color || colors.primary.accent} />
|
||||
|
||||
@@ -101,7 +101,7 @@ export const SectionHeader = React.memo<
|
||||
alignSelf: "center",
|
||||
textAlignVertical: "center"
|
||||
}}
|
||||
color={color || colors.primary.accent}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{!item.title || item.title === ""
|
||||
? strings.pinned().toUpperCase()
|
||||
@@ -133,9 +133,7 @@ export const SectionHeader = React.memo<
|
||||
<Sort
|
||||
screen={screen}
|
||||
type={dataType}
|
||||
hideGroupOptions={
|
||||
screen === "Reminders" || screen === "Search"
|
||||
}
|
||||
hideGroupOptions={screen === "Reminders"}
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
@@ -30,9 +30,7 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import useNavigationStore, {
|
||||
RouteParams
|
||||
} from "../../../stores/use-navigation-store";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
|
||||
@@ -55,6 +53,7 @@ import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import dayjs from "dayjs";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
|
||||
type NoteItemProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
@@ -68,7 +67,6 @@ type NoteItemProps = {
|
||||
isTrash?: boolean;
|
||||
noOpen?: boolean;
|
||||
locked?: boolean;
|
||||
renderedInRoute?: keyof RouteParams;
|
||||
};
|
||||
|
||||
const NoteItem = ({
|
||||
@@ -81,9 +79,9 @@ const NoteItem = ({
|
||||
tags,
|
||||
attachmentsCount,
|
||||
locked,
|
||||
noOpen = false,
|
||||
renderedInRoute
|
||||
noOpen = false
|
||||
}: NoteItemProps) => {
|
||||
const route = useRoute();
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId ===
|
||||
@@ -267,7 +265,7 @@ const NoteItem = ({
|
||||
{notebooks?.items
|
||||
?.filter(
|
||||
(item) =>
|
||||
renderedInRoute !== "Notebook" ||
|
||||
route.name !== "Notebook" ||
|
||||
item.id !== useNavigationStore.getState().focusedRouteId
|
||||
)
|
||||
.map((item) => (
|
||||
@@ -309,8 +307,7 @@ const NoteItem = ({
|
||||
backgroundColor: colors.secondary.background,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
|
||||
borderWidth: 0.5,
|
||||
borderColor:
|
||||
color?.colorCode || primaryColors.border,
|
||||
borderColor: primaryColors.border,
|
||||
paddingVertical: 1
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -34,7 +34,6 @@ import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
import NotePreview from "../../note-history/preview";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import { RouteParams } from "../../../stores/use-navigation-store";
|
||||
|
||||
export const openNote = async (
|
||||
item: Note,
|
||||
@@ -83,7 +82,6 @@ type NoteWrapperProps = {
|
||||
date: number;
|
||||
isRenderedInActionSheet: boolean;
|
||||
locked?: boolean;
|
||||
renderedInRoute?: keyof RouteParams;
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo<
|
||||
|
||||
@@ -41,7 +41,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
positiveText: strings.restore(),
|
||||
negativeText: strings.delete(),
|
||||
positivePress: async () => {
|
||||
await db.trash.restore(item.id);
|
||||
if ((await db.trash.restore(item.id)) === false) return;
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
|
||||
@@ -17,26 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Reminder } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import AddReminder from "../../../screens/add-reminder";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import ReminderSheet from "../../sheets/reminder";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const ReminderItem = React.memo(
|
||||
({
|
||||
@@ -51,10 +49,8 @@ const ReminderItem = React.memo(
|
||||
const { colors } = useThemeColors();
|
||||
const openReminder = () => {
|
||||
if (selectItem(item)) return;
|
||||
AddReminder.present(item, undefined);
|
||||
if (isSheet) {
|
||||
eSendEvent(eCloseSheet);
|
||||
}
|
||||
|
||||
ReminderSheet.present(item, undefined, isSheet);
|
||||
};
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const [selected] = useIsSelected(item);
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from "react";
|
||||
import { HighlightedResult } from "@notesnook/core";
|
||||
import { View } from "react-native";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import { db } from "../../../common/database";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eOnLoadNote } from "../../../utils/events";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
type SearchResultProps = {
|
||||
item: HighlightedResult;
|
||||
};
|
||||
|
||||
export const SearchResult = (props: SearchResultProps) => {
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const openNote = async (index?: number) => {
|
||||
const note = await db.notes.note(props.item.id);
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: {
|
||||
...note,
|
||||
content: props.item.rawContent
|
||||
? {
|
||||
data: props.item.rawContent || "",
|
||||
type: "tiptap"
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
searchResultIndex: index
|
||||
});
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
borderRadius: 0
|
||||
}}
|
||||
onLongPress={async () => {
|
||||
const note = await db.notes.note(props.item.id);
|
||||
Properties.present(note);
|
||||
}}
|
||||
onPress={async () => openNote()}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL / 2,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{props.item.content?.length ? (
|
||||
<IconButton
|
||||
name={!expanded ? "chevron-right" : "chevron-down"}
|
||||
onPress={() => setExpanded((prev) => !prev)}
|
||||
size={AppFontSize.md + 2}
|
||||
color={colors.secondary.icon}
|
||||
style={{
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Heading
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{props.item.title.map((title, index) => (
|
||||
<React.Fragment key={props.item.id + index + "-srt"}>
|
||||
{title.prefix}
|
||||
<Heading
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
backgroundColor: colors.secondary.accent,
|
||||
color: colors.secondary.accentForeground
|
||||
}}
|
||||
>
|
||||
{title.match}
|
||||
</Heading>
|
||||
{title.suffix}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Heading>
|
||||
</View>
|
||||
{props.item.content?.length ? (
|
||||
<Paragraph size={AppFontSize.xxs} color={colors.secondary.paragraph}>
|
||||
{props.item.content.length}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{expanded &&
|
||||
props.item.content.map((content, index) => (
|
||||
<Pressable
|
||||
key={props.item.id + index}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
borderColor: colors.primary.border,
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
width: "100%"
|
||||
}}
|
||||
onLongPress={async () => {
|
||||
const note = await db.notes.note(props.item.id);
|
||||
Properties.present(note);
|
||||
}}
|
||||
onPress={() => {
|
||||
let activeIndex = 0;
|
||||
for (let i = 0; i <= index; i++) {
|
||||
activeIndex += props.item.content[i].length;
|
||||
}
|
||||
openNote(activeIndex);
|
||||
}}
|
||||
>
|
||||
<Paragraph>
|
||||
{content.map((match, index) => (
|
||||
<React.Fragment key={props.item.id + index + "src"}>
|
||||
{match.prefix}
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
backgroundColor: colors.secondary.accent,
|
||||
color: colors.secondary.accentForeground
|
||||
}}
|
||||
>
|
||||
{match.match}
|
||||
</Paragraph>
|
||||
{match.suffix}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -25,7 +25,6 @@ import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import { View } from "react-native";
|
||||
|
||||
export function selectItem(item: Item) {
|
||||
if (useSelectionStore.getState().selectionMode === item.type) {
|
||||
@@ -113,18 +112,6 @@ const SelectionWrapper = ({
|
||||
marginBottom: isSheet ? DefaultAppStyles.GAP_VERTICAL : undefined
|
||||
}}
|
||||
>
|
||||
{isEditingNote ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: color || colors.selected.accent,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 5
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
@@ -66,7 +66,7 @@ export const Card = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -109,6 +109,25 @@ export const Card = ({
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{fontScale > 1 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import { Header } from "../list-items/headers/header";
|
||||
import { Empty, PlaceholderData } from "./empty";
|
||||
import { ListItemWrapper } from "./list-item.wrapper";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
|
||||
type ListProps = {
|
||||
data: VirtualizedGrouping<Item> | undefined;
|
||||
@@ -73,7 +74,7 @@ export default function List(props: ListProps) {
|
||||
? "home"
|
||||
: props.renderedInRoute === "Favorites"
|
||||
? "favorites"
|
||||
: props.renderedInRoute === "Trash" || props.dataType === "trash"
|
||||
: props.renderedInRoute === "Trash"
|
||||
? "trash"
|
||||
: `${props.dataType}s`;
|
||||
|
||||
@@ -150,7 +151,7 @@ export default function List(props: ListProps) {
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{props.data?.placeholders?.length === 0 || !props.data ? (
|
||||
{props.data?.placeholders?.length === 0 ? (
|
||||
<>
|
||||
{props.CustomLisHeader ? (
|
||||
props.CustomLisHeader
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
GroupHeader,
|
||||
GroupOptions,
|
||||
GroupingKey,
|
||||
HighlightedResult,
|
||||
Item,
|
||||
ItemType,
|
||||
Note,
|
||||
@@ -50,7 +49,6 @@ import { NoteWrapper } from "../list-items/note/wrapper";
|
||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
import TagItem from "../list-items/tag";
|
||||
import { SearchResult } from "../list-items/search-result";
|
||||
|
||||
type ListItemWrapperProps<TItem = Item> = {
|
||||
group?: GroupingKey;
|
||||
@@ -155,9 +153,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
},
|
||||
items?.cacheItem(index) ? 100 : 0
|
||||
);
|
||||
} catch (e) {
|
||||
/** empty */
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
}, [index, items, refreshItem]);
|
||||
|
||||
@@ -200,11 +196,10 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
notebooks={notebooks.current}
|
||||
reminder={reminder.current}
|
||||
attachmentsCount={attachmentsCount.current}
|
||||
date={getDate(item as Note, group)}
|
||||
date={getDate(item, group)}
|
||||
isRenderedInActionSheet={isSheet}
|
||||
index={index}
|
||||
locked={locked.current}
|
||||
renderedInRoute={props.renderedInRoute}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -231,7 +226,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
<NotebookWrapper
|
||||
item={item as Notebook}
|
||||
totalNotes={totalNotes.current}
|
||||
date={getDate(item as Notebook, group)}
|
||||
date={getDate(item, group)}
|
||||
index={index}
|
||||
/>
|
||||
</>
|
||||
@@ -289,34 +284,12 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case "searchResult":
|
||||
return (
|
||||
<>
|
||||
{groupHeader && previousIndex.current === index && !isSheet ? (
|
||||
<SectionHeader
|
||||
screen={props.renderedInRoute}
|
||||
item={groupHeader}
|
||||
index={index}
|
||||
dataType={item.type}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: props.scrollRef,
|
||||
data: items
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<SearchResult item={item as HighlightedResult} />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getDate(item: Notebook | Note, groupType?: GroupingKey): number {
|
||||
function getDate(item: Item, groupType?: GroupingKey): number {
|
||||
return (
|
||||
getSortValue(
|
||||
groupType
|
||||
|
||||
@@ -30,10 +30,6 @@ import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../sheets/paywall";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
interface ReorderableListProps<T extends { id: string }>
|
||||
extends Omit<DraxListProps<T>, "renderItem" | "data" | "renderItemContent"> {
|
||||
@@ -63,9 +59,6 @@ function ReorderableList<T extends { id: string }>({
|
||||
const [hiddenItemsState, setHiddenItems] = useState(hiddenItems);
|
||||
const dragging = useSideBarDraggingStore((state) => state.dragging);
|
||||
const listRef = useRef<FlatList | null>(null);
|
||||
const customizableSidebarFeature = useIsFeatureAvailable(
|
||||
"customizableSidebar"
|
||||
);
|
||||
|
||||
if (dragging) {
|
||||
fluidTabsRef.current?.lock();
|
||||
@@ -132,7 +125,6 @@ function ReorderableList<T extends { id: string }>({
|
||||
);
|
||||
|
||||
function getOrderedItems() {
|
||||
if (!customizableSidebarFeature?.isAllowed) return data;
|
||||
const items: T[] = [];
|
||||
itemOrderState.forEach((id) => {
|
||||
const item = data.find((i) => i.id === id);
|
||||
@@ -166,39 +158,15 @@ function ReorderableList<T extends { id: string }>({
|
||||
}
|
||||
}}
|
||||
longPressDelay={500}
|
||||
onItemDragStart={async () => {
|
||||
if (
|
||||
customizableSidebarFeature &&
|
||||
!customizableSidebarFeature?.isAllowed
|
||||
) {
|
||||
ToastManager.show({
|
||||
message: customizableSidebarFeature?.error,
|
||||
type: "info",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => PaywallSheet.present(customizableSidebarFeature)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onItemDragStart={() =>
|
||||
useSideBarDraggingStore.setState({
|
||||
dragging: true
|
||||
});
|
||||
}}
|
||||
})
|
||||
}
|
||||
disableVirtualization
|
||||
itemsDraggable={disableDefaultDrag ? dragging : true}
|
||||
lockItemDragsToMainAxis
|
||||
onItemReorder={async ({ fromIndex, fromItem, toIndex, toItem }) => {
|
||||
if (
|
||||
customizableSidebarFeature &&
|
||||
!customizableSidebarFeature?.isAllowed
|
||||
) {
|
||||
ToastManager.show({
|
||||
message: customizableSidebarFeature.error,
|
||||
type: "info",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => PaywallSheet.present(customizableSidebarFeature)
|
||||
});
|
||||
return;
|
||||
}
|
||||
onItemReorder={({ fromIndex, fromItem, toIndex, toItem }) => {
|
||||
const newOrder = getOrderedItems().map((item) => item.id);
|
||||
const element = newOrder.splice(fromIndex, 1)[0];
|
||||
if (toIndex === 0) {
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const Loading = (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16
|
||||
}}
|
||||
>
|
||||
{props.icon ? (
|
||||
<Icon name={props.icon} size={80} color={colors.primary.accent} />
|
||||
) : null}
|
||||
|
||||
{props.title ? (
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{props.description ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.description}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -117,8 +117,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
{!session?.locked && !locked ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: colors.primary.background
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<ReadonlyEditor
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
96
apps/mobile/app/components/premium/compact-features.js
Normal file
96
apps/mobile/app/components/premium/compact-features.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { FeatureBlock } from "./feature";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const CompactFeatures = ({
|
||||
vertical,
|
||||
features = [],
|
||||
maxHeight = 600,
|
||||
scrollRef
|
||||
}) => {
|
||||
let data = vertical
|
||||
? features
|
||||
: [
|
||||
{
|
||||
highlight: "Everything",
|
||||
content: "in basic",
|
||||
icon: "emoticon-wink"
|
||||
},
|
||||
{
|
||||
highlight: "Unlimited",
|
||||
content: "notebooks",
|
||||
icon: "notebook"
|
||||
},
|
||||
{
|
||||
highlight: "File & image",
|
||||
content: "attachments",
|
||||
icon: "attachment"
|
||||
},
|
||||
{
|
||||
highlight: "Instant",
|
||||
content: "syncing",
|
||||
icon: "sync"
|
||||
},
|
||||
{
|
||||
highlight: "Private",
|
||||
content: "vault",
|
||||
icon: "shield"
|
||||
},
|
||||
{
|
||||
highlight: "Daily, weekly & monthly",
|
||||
content: "recurring reminders",
|
||||
icon: "bell"
|
||||
},
|
||||
{
|
||||
highlight: "Rich text",
|
||||
content: "editing",
|
||||
icon: "square-edit-outline"
|
||||
},
|
||||
{
|
||||
highlight: "PDF & markdown",
|
||||
content: "exports",
|
||||
icon: "file"
|
||||
},
|
||||
{
|
||||
highlight: "Encrypted",
|
||||
content: "backups",
|
||||
icon: "backup-restore"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal={!vertical}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxHeight: maxHeight,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{data.map((item) => (
|
||||
<FeatureBlock key={item.highlight} vertical={vertical} {...item} />
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
290
apps/mobile/app/components/premium/component.js
Normal file
290
apps/mobile/app/components/premium/component.js
Normal file
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
|
||||
import { LAUNCH_ROCKET } from "../../assets/images/assets";
|
||||
import { db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eClosePremiumDialog, eCloseSheet } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { features } from "./features";
|
||||
import { Group } from "./group";
|
||||
import { PricingPlans } from "./pricing-plans";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Component = ({ close, promo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const userCanRequestTrial =
|
||||
user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
const [floatingButton, setFloatingButton] = useState(false);
|
||||
const pricing = usePricing("monthly");
|
||||
|
||||
const onPress = async () => {
|
||||
if (user) {
|
||||
presentSheet({
|
||||
context: "pricing_plans",
|
||||
component: (
|
||||
<PricingPlans showTrialOption={false} marginTop={1} promo={promo} />
|
||||
)
|
||||
});
|
||||
} else {
|
||||
close();
|
||||
Navigation.navigate("Auth", {
|
||||
mode: AuthMode.trialSignup
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = (event) => {
|
||||
let contentSize = event.nativeEvent.contentSize.height;
|
||||
contentSize = contentSize - event.nativeEvent.layoutMeasurement.height;
|
||||
let yOffset = event.nativeEvent.contentOffset.y;
|
||||
if (yOffset > 600 && yOffset < contentSize - 400) {
|
||||
setFloatingButton(true);
|
||||
} else {
|
||||
setFloatingButton(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 10,
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="pricing_plans" />
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: DDS.isTab ? 30 : 15,
|
||||
top: Platform.OS === "ios" ? 0 : 30,
|
||||
zIndex: 10,
|
||||
width: 50,
|
||||
height: 50
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingHorizontal: DDS.isTab ? DDS.width / 5 : 0
|
||||
}}
|
||||
scrollEventThrottle={0}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<View
|
||||
key="top-banner"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
height: 400,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
width={350}
|
||||
height={350}
|
||||
src={LAUNCH_ROCKET(colors.primary.accent)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Heading
|
||||
key="heading"
|
||||
size={AppFontSize.lg}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingTop: 20
|
||||
}}
|
||||
>
|
||||
Notesnook{" "}
|
||||
<Heading size={AppFontSize.lg} color={colors.primary.accent}>
|
||||
Pro
|
||||
</Heading>
|
||||
</Heading>
|
||||
|
||||
{!pricing ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={AppFontSize.md}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={AppFontSize.md}
|
||||
>
|
||||
(
|
||||
{Platform.OS === "android"
|
||||
? pricing.product?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].formattedPrice
|
||||
: pricing.product?.localizedPrice}{" "}
|
||||
/ mo)
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
key="description"
|
||||
size={AppFontSize.md}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
textAlign: "center",
|
||||
alignSelf: "center",
|
||||
paddingBottom: 20,
|
||||
width: "90%"
|
||||
}}
|
||||
>
|
||||
Ready to take the next step on your private note taking journey?
|
||||
</Paragraph>
|
||||
|
||||
{userCanRequestTrial ? (
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Walkthrough.present("trialstarted", false, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title="Try free for 14 days"
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type={userCanRequestTrial ? "secondaryAccented" : "accent"}
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
{!user || userCanRequestTrial ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
maxWidth: "80%"
|
||||
}}
|
||||
>
|
||||
{user
|
||||
? 'On clicking "Try free for 14 days", your free trial will be activated.'
|
||||
: "After sign up you will be asked to activate your free trial."}{" "}
|
||||
<Paragraph size={AppFontSize.xs} style={{ fontWeight: "bold" }}>
|
||||
No credit card is required.
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Seperator key="seperator_1" />
|
||||
|
||||
{features.map((item, index) => (
|
||||
<Group key={item.title} item={item} index={index} />
|
||||
))}
|
||||
|
||||
<View
|
||||
key="plans"
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<PricingPlans showTrialOption={false} promo={promo} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{floatingButton ? (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type="accent"
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP * 2,
|
||||
position: "absolute",
|
||||
borderRadius: 100,
|
||||
bottom: 30,
|
||||
...getElevationStyle(10)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
218
apps/mobile/app/components/premium/expiring.js
Normal file
218
apps/mobile/app/components/premium/expiring.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
eOpenPremiumDialog,
|
||||
eOpenResultDialog,
|
||||
eOpenTrialEndingDialog
|
||||
} from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import DialogContainer from "../dialog/dialog-container";
|
||||
import { Button } from "../ui/button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { CompactFeatures } from "./compact-features";
|
||||
import { Offer } from "./offer";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Expiring = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [status, setStatus] = useState({
|
||||
title: "Your trial is ending soon",
|
||||
offer: "Get 30% off",
|
||||
extend: true
|
||||
});
|
||||
const pricing = usePricing("yearly");
|
||||
const promo =
|
||||
status.offer && pricing?.info
|
||||
? {
|
||||
promoCode:
|
||||
pricing?.info?.discount > 30
|
||||
? pricing.info.sku
|
||||
: "com.streetwriters.notesnook.sub.yr.trialoffer",
|
||||
text: `GET ${
|
||||
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}% OFF on yearly`,
|
||||
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenTrialEndingDialog, open);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenTrialEndingDialog, open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = (status) => {
|
||||
setStatus(status);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<BaseDialog
|
||||
onRequestClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<DialogContainer>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
textBreakStrategy="balanced"
|
||||
style={{
|
||||
textAlign: "center",
|
||||
paddingTop: 18
|
||||
}}
|
||||
>
|
||||
{status.title}
|
||||
</Heading>
|
||||
<Seperator />
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{status.offer ? (
|
||||
<>
|
||||
<Offer padding={20} off={promo?.discount || 30} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Paragraph
|
||||
textBreakStrategy="balanced"
|
||||
style={{
|
||||
textAlign: "center",
|
||||
paddingTop: 0,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
size={AppFontSize.md + 2}
|
||||
>
|
||||
Upgrade now to continue using all the pro features after
|
||||
your trial ends
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CompactFeatures />
|
||||
|
||||
<Paragraph
|
||||
onPress={async () => {
|
||||
setVisible(false);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog, promo);
|
||||
}}
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.secondary.paragraph,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
{"See what's included in Basic & Pro plans"}
|
||||
</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%",
|
||||
borderBottomRightRadius: 10,
|
||||
borderBottomLeftRadius: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="transparent"
|
||||
title="Subscribe now"
|
||||
onPress={async () => {
|
||||
setVisible(false);
|
||||
await sleep(300);
|
||||
PremiumService.sheet(
|
||||
null,
|
||||
promo?.discount > 30 ? null : promo
|
||||
);
|
||||
}}
|
||||
fontSize={AppFontSize.md + 2}
|
||||
style={{
|
||||
marginBottom: status.extend ? 0 : 10,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP * 2
|
||||
}}
|
||||
/>
|
||||
|
||||
{status.extend && (
|
||||
<Button
|
||||
type="plain"
|
||||
title="Not sure yet? Extend trial for 7 days"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={async () => {
|
||||
setVisible(false);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenResultDialog, {
|
||||
title: "Your trial has been extended",
|
||||
paragraph:
|
||||
"Try out all features of Notesnook free for 7 more days. No limitations. No commitments.",
|
||||
button: "Continue"
|
||||
});
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
height={30}
|
||||
style={{
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</DialogContainer>
|
||||
</BaseDialog>
|
||||
)
|
||||
);
|
||||
};
|
||||
97
apps/mobile/app/components/premium/feature.js
Normal file
97
apps/mobile/app/components/premium/feature.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ProTag } from "./pro-tag";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const FeatureBlock = ({
|
||||
vertical,
|
||||
highlight,
|
||||
content,
|
||||
icon,
|
||||
pro,
|
||||
proTagBg
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return vertical ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 10,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginLeft: 5,
|
||||
flexShrink: 1
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
{content}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
height: 100,
|
||||
justifyContent: "center",
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
marginRight: 10,
|
||||
borderRadius: defaultBorderRadius,
|
||||
minWidth: 100
|
||||
}}
|
||||
>
|
||||
<Icon color={colors.primary.icon} name={icon} size={AppFontSize.xl} />
|
||||
<Paragraph size={AppFontSize.md}>
|
||||
<Text style={{ color: colors.primary.accent }}>{highlight}</Text>
|
||||
{content ? "\n" + content : null}
|
||||
</Paragraph>
|
||||
|
||||
{pro ? (
|
||||
<>
|
||||
<View style={{ height: 5 }} />
|
||||
<ProTag width={50} size={AppFontSize.xs} background={proTagBg} />
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: 30,
|
||||
height: 3,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderRadius: 100,
|
||||
backgroundColor: colors.primary.accent
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
318
apps/mobile/app/components/premium/features.js
Normal file
318
apps/mobile/app/components/premium/features.js
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const features = [
|
||||
{
|
||||
title: "Focused on privacy",
|
||||
detail:
|
||||
"Everything you do in Notesnook stays private. We use XChaCha20-Poly1305-IETF and Argon2 to encrypt your notes.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Zero ads",
|
||||
content: "& zero trackers",
|
||||
icon: "billboard"
|
||||
},
|
||||
{
|
||||
highlight: "On device",
|
||||
content: "encryption",
|
||||
icon: "cellphone"
|
||||
},
|
||||
{
|
||||
highlight: "Secure app",
|
||||
content: "lock for all",
|
||||
icon: "cellphone-lock"
|
||||
},
|
||||
{
|
||||
highlight: "100% end-to-end ",
|
||||
content: "encrypted",
|
||||
icon: "lock"
|
||||
},
|
||||
{
|
||||
highlight: "Password protected",
|
||||
content: "notes sharing",
|
||||
icon: "file-lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "No limit on notes or devices",
|
||||
detail:
|
||||
"Basic or Pro, you can create unlimited number of notes and access them on all your devices. You won't be running out of space or blocks ever."
|
||||
},
|
||||
{
|
||||
title: "Attach files & images",
|
||||
detail:
|
||||
"Add your documents, PDFs, images and videos, and keep them safe and organized.",
|
||||
pro: true,
|
||||
features: [
|
||||
{
|
||||
highlight: "Bullet proof",
|
||||
content: "encryption",
|
||||
icon: "lock"
|
||||
},
|
||||
{
|
||||
highlight: "High quality",
|
||||
content: "4k images",
|
||||
icon: "image-multiple"
|
||||
},
|
||||
{
|
||||
highlight: "No monthly",
|
||||
content: "storage limit",
|
||||
icon: "harddisk"
|
||||
},
|
||||
{
|
||||
highlight: "Generous 500 MB",
|
||||
content: "max file size",
|
||||
icon: "file-cabinet"
|
||||
},
|
||||
{
|
||||
highlight: "No restriction",
|
||||
content: "on file type",
|
||||
icon: "file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Cross platform Reminders",
|
||||
detail: "Stay updated on all your upcoming tasks with reminders.",
|
||||
features: [
|
||||
{
|
||||
highlight: "One-time",
|
||||
content: "reminders",
|
||||
icon: "bell"
|
||||
},
|
||||
{
|
||||
highlight: "Daily, weekly & monthly",
|
||||
content: "reminders",
|
||||
icon: "refresh",
|
||||
pro: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Two-factor authentication",
|
||||
detail:
|
||||
"Improve account security & prevent intruders from accessing your notes",
|
||||
info: "* 2FA via email is enabled by default for all users.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Email *",
|
||||
icon: "bell"
|
||||
},
|
||||
{
|
||||
highlight: "Authentication",
|
||||
content: "app",
|
||||
icon: "refresh"
|
||||
},
|
||||
{
|
||||
highlight: "SMS",
|
||||
icon: "refresh",
|
||||
pro: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Keep secrets always locked with private vault",
|
||||
detail:
|
||||
"An extra layer of security for any important data. Notes in the vault always stay encrypted and require a password to be accessed or edited everytime.",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
title: "Organize yourself in the best way",
|
||||
detail:
|
||||
"We offer multiple ways to keep you organized. The only limit is your imagination.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Unlimited",
|
||||
content: "notebooks & tags*",
|
||||
icon: "emoticon",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Organize",
|
||||
content: "with colors",
|
||||
icon: "palette",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Side menu",
|
||||
content: "shortcuts",
|
||||
icon: "link-variant"
|
||||
},
|
||||
{
|
||||
highlight: "Pin note in",
|
||||
content: "notifications",
|
||||
icon: "pin",
|
||||
platform: "android"
|
||||
}
|
||||
],
|
||||
info: "* Free users are limited to keeping 3 notebooks and 5 tags."
|
||||
},
|
||||
|
||||
{
|
||||
title: "Instant sync",
|
||||
detail:
|
||||
"Seamlessly work from anywhere on any device. Every change is synced instantly to all your devices.",
|
||||
info: "* Disable sync completely, turn off auto sync or disable editor realtime sync.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Sync to unlimited",
|
||||
content: "devices",
|
||||
icon: "cellphone"
|
||||
},
|
||||
{
|
||||
highlight: "Realtime",
|
||||
content: "editor sync",
|
||||
icon: "sync"
|
||||
},
|
||||
{
|
||||
highlight: "Granular sync",
|
||||
content: "controls *",
|
||||
icon: "sync-off"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Rich tools for rich editing",
|
||||
detail:
|
||||
"Having the right tool at the right time is crucial for note taking. Lists, tables, codeblocks — you name it, we have it.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Basic formatting",
|
||||
content: "and lists",
|
||||
icon: "format-bold"
|
||||
},
|
||||
{
|
||||
highlight: "Checklists",
|
||||
content: "& tables",
|
||||
icon: "table",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Markdown",
|
||||
content: "support",
|
||||
icon: "language-markdown",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Personalized",
|
||||
content: "editor toolbar",
|
||||
icon: "gesture-tap-button",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Write notes from",
|
||||
content: "notifications",
|
||||
icon: "bell",
|
||||
platform: "android"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Safe publishing to the Internet",
|
||||
detail:
|
||||
"Publishing is nothing new but we offer fully encrypted, anonymous publishing. Take any note & share it with the world.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Password protected",
|
||||
content: "sharing",
|
||||
icon: "send-lock"
|
||||
},
|
||||
{
|
||||
highlight: "Self destruct",
|
||||
content: "monographs",
|
||||
icon: "bomb"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Export and take your notes anywhere",
|
||||
pro: true,
|
||||
detail:
|
||||
"You own your notes, not us. No proprietary formats. No vendor lock in. No waiting for hours to download your notes.",
|
||||
info: "* Free users can export notes in well formatted plain text.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Export as ",
|
||||
content: "Markdown",
|
||||
icon: "language-markdown",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Export as",
|
||||
content: "PDF",
|
||||
icon: "file-pdf-box",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Export as",
|
||||
content: "HTML",
|
||||
icon: "language-html5",
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
highlight: "Export as",
|
||||
content: "text",
|
||||
icon: "clipboard-text-outline"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Backup & keep your notes safe",
|
||||
detail:
|
||||
"Do not worry about losing your data. Turn on automatic backups on weekly or daily basis.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Backup",
|
||||
content: "encryption",
|
||||
icon: "backup-restore"
|
||||
}
|
||||
],
|
||||
pro: true
|
||||
},
|
||||
{
|
||||
title: "Personalize & make Notesnook your own",
|
||||
detail:
|
||||
"Change app themes to match your style. Custom themes are coming soon.",
|
||||
|
||||
features: [
|
||||
{
|
||||
highlight: "Automatic",
|
||||
content: "dark mode",
|
||||
icon: "theme-light-dark",
|
||||
pro: false
|
||||
},
|
||||
{
|
||||
highlight: "Change accent",
|
||||
content: "color",
|
||||
icon: "invert-colors",
|
||||
pro: true
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
{
|
||||
highlight: 'Private vault',
|
||||
content: 'for notes',
|
||||
icon: 'shield-lock'
|
||||
}
|
||||
*/
|
||||
93
apps/mobile/app/components/premium/group.js
Normal file
93
apps/mobile/app/components/premium/group.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { FeatureBlock } from "./feature";
|
||||
import { ProTag } from "./pro-tag";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Group = ({ item, index }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor:
|
||||
index % 2 !== 0
|
||||
? colors.primary.background
|
||||
: colors.secondary.background,
|
||||
paddingVertical: 40
|
||||
}}
|
||||
>
|
||||
{item?.pro ? (
|
||||
<ProTag
|
||||
size={AppFontSize.sm}
|
||||
background={
|
||||
index % 2 === 0
|
||||
? colors.primary.background
|
||||
: colors.secondary.background
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Heading>{item.title}</Heading>
|
||||
<Paragraph size={AppFontSize.md}>{item.detail}</Paragraph>
|
||||
|
||||
{item.features && (
|
||||
<ScrollView
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP
|
||||
}}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
{item.features?.map((item) => (
|
||||
<FeatureBlock
|
||||
key={item.detail}
|
||||
{...item}
|
||||
detail={item.detail}
|
||||
pro={item.pro}
|
||||
proTagBg={
|
||||
index % 2 === 0
|
||||
? colors.primary.background
|
||||
: colors.secondary.background
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
{item.info ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{item.info}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
87
apps/mobile/app/components/premium/index.js
Normal file
87
apps/mobile/app/components/premium/index.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Component } from "./component";
|
||||
|
||||
class PremiumDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: false,
|
||||
promo: null
|
||||
};
|
||||
this.actionSheetRef = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
eSubscribeEvent(eOpenPremiumDialog, this.open);
|
||||
eSubscribeEvent(eClosePremiumDialog, this.close);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenPremiumDialog, this.open);
|
||||
eUnSubscribeEvent(eClosePremiumDialog, this.close);
|
||||
}
|
||||
|
||||
open = (promoInfo) => {
|
||||
this.setState({
|
||||
visible: true,
|
||||
promo: promoInfo
|
||||
});
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.setState({
|
||||
visible: false,
|
||||
promo: null
|
||||
});
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return !this.state.visible ? null : (
|
||||
<BaseDialog
|
||||
animation="slide"
|
||||
bounce={false}
|
||||
background={this.props.colors.primary.background}
|
||||
onRequestClose={this.onClose}
|
||||
>
|
||||
<Component
|
||||
getRef={() => this.actionSheetRef}
|
||||
promo={this.state.promo}
|
||||
close={this.close}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PremiumDialog;
|
||||
@@ -16,26 +16,31 @@ 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);
|
||||
}
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
if (process.env.CUSTOM_USER_DATA_DIR) {
|
||||
app.setPath(
|
||||
"appData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
|
||||
export const Offer = ({
|
||||
off = "30",
|
||||
text = "on yearly plan, offer ends soon",
|
||||
padding = 0
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center",
|
||||
paddingVertical: padding
|
||||
}}
|
||||
size={AppFontSize.xxxl}
|
||||
>
|
||||
GET {off}
|
||||
<Text style={{ color: colors.primary.accent }}>%</Text> OFF!{"\n"}
|
||||
<Paragraph color={colors.secondary.paragraph}>{text}</Paragraph>
|
||||
</Paragraph>
|
||||
);
|
||||
app.setPath(
|
||||
"userData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
|
||||
);
|
||||
app.setPath(
|
||||
"documents",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
|
||||
);
|
||||
}
|
||||
};
|
||||
144
apps/mobile/app/components/premium/premium-toast.js
Normal file
144
apps/mobile/app/components/premium/premium-toast.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseSheet,
|
||||
eOpenPremiumDialog,
|
||||
eShowGetPremium
|
||||
} from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const PremiumToast = ({ context = "global", offset = 0 }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [msg, setMsg] = useState(null);
|
||||
const timer = useRef();
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const open = useCallback(
|
||||
(event) => {
|
||||
if (!event) {
|
||||
clearTimeout(timer);
|
||||
timer.current = null;
|
||||
setMsg(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.context === context && msg?.desc !== event.desc) {
|
||||
if (timer.current !== null) {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
}
|
||||
setMsg(event);
|
||||
timer.current = setTimeout(async () => {
|
||||
setMsg(null);
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
[context, msg?.desc]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowGetPremium, open);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eShowGetPremium, open);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const onPress = async () => {
|
||||
open(null);
|
||||
eSendEvent(eCloseActionSheet);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
};
|
||||
|
||||
return (
|
||||
!!msg && (
|
||||
<Animated.View
|
||||
entering={FadeInUp}
|
||||
exiting={FadeOutUp}
|
||||
style={{
|
||||
position: "absolute",
|
||||
backgroundColor: colors.secondary.background,
|
||||
zIndex: 999,
|
||||
...getElevationStyle(20),
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: 10,
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
top: offset + keyboard.keyboardHeight,
|
||||
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setMsg(null);
|
||||
clearTimeout(timer.current);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
paddingRight: 6
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.md}
|
||||
>
|
||||
{msg.title}
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{msg.desc}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Button onPress={onPress} title="Get Now" type="accent" />
|
||||
</Animated.View>
|
||||
)
|
||||
);
|
||||
};
|
||||
101
apps/mobile/app/components/premium/pricing-item.tsx
Normal file
101
apps/mobile/app/components/premium/pricing-item.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import RNIap from "react-native-iap";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const PricingItem = ({
|
||||
product,
|
||||
onPress,
|
||||
compact,
|
||||
strikethrough
|
||||
}: {
|
||||
product: {
|
||||
type: "yearly" | "monthly";
|
||||
data?: RNIap.Subscription;
|
||||
info: string;
|
||||
offerType?: "yearly" | "monthly";
|
||||
};
|
||||
strikethrough?: boolean;
|
||||
onPress?: () => void;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
type="secondary"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: compact ? 15 : 10,
|
||||
width: compact ? null : "100%",
|
||||
minWidth: 150,
|
||||
opacity: strikethrough ? 0.7 : 1
|
||||
}}
|
||||
disabled={strikethrough}
|
||||
>
|
||||
{!compact && (
|
||||
<View>
|
||||
<Heading size={AppFontSize.lg - 2}>
|
||||
{product?.type === "yearly" || product?.offerType === "yearly"
|
||||
? "Yearly"
|
||||
: "Monthly"}
|
||||
</Heading>
|
||||
{product?.info && (
|
||||
<Paragraph size={AppFontSize.xs}>{product.info}</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: strikethrough ? "line-through" : undefined
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textDecorationLine: strikethrough ? "line-through" : undefined
|
||||
}}
|
||||
size={AppFontSize.lg - 2}
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails?.[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice}
|
||||
</Heading>
|
||||
{product?.type === "yearly" || product?.offerType === "yearly"
|
||||
? "/year"
|
||||
: "/month"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user