mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
333 Commits
fix-loadin
...
fix/289
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61aaaba452 | ||
|
|
9992bf8c32 | ||
|
|
b8b7d37799 | ||
|
|
1312ce7d50 | ||
|
|
d16cd510b3 | ||
|
|
af08ed5174 | ||
|
|
365e1d7029 | ||
|
|
80b75fc28c | ||
|
|
6de4b4ac2d | ||
|
|
39bfff6b14 | ||
|
|
e381b62e5c | ||
|
|
ffef5affce | ||
|
|
780276b30e | ||
|
|
276239dd2b | ||
|
|
5ef3bdcad4 | ||
|
|
63076d37f5 | ||
|
|
724ad58824 | ||
|
|
7fcdb3b55d | ||
|
|
47ba40d7cd | ||
|
|
0801e73242 | ||
|
|
c82487983c | ||
|
|
2ad39048da | ||
|
|
3421627f31 | ||
|
|
cf6262e995 | ||
|
|
8a325bf7c0 | ||
|
|
883667b411 | ||
|
|
06b88e74ae | ||
|
|
7800a25775 | ||
|
|
01d0de3909 | ||
|
|
d61f9472c8 | ||
|
|
2465337148 | ||
|
|
af4638ac50 | ||
|
|
5e6be09aab | ||
|
|
f626953e0a | ||
|
|
4d4bce301d | ||
|
|
d45b18afd6 | ||
|
|
4f212453a6 | ||
|
|
2c1b9081bf | ||
|
|
7c5dc1615c | ||
|
|
3fb07369bb | ||
|
|
9b480950da | ||
|
|
9f20d243fa | ||
|
|
13e6573342 | ||
|
|
f37869cb5a | ||
|
|
46d802548b | ||
|
|
2bf42c207d | ||
|
|
65e42e5d8b | ||
|
|
c85007f0dd | ||
|
|
36058145b5 | ||
|
|
a376a6b497 | ||
|
|
cd62d51e45 | ||
|
|
3f6fcbacbc | ||
|
|
2a2bf67fea | ||
|
|
b8fca5cd98 | ||
|
|
4d87155fdd | ||
|
|
ed38bb6299 | ||
|
|
7c9e3e79df | ||
|
|
43b90070a9 | ||
|
|
04f6e1068b | ||
|
|
9bea4adcf9 | ||
|
|
a5790de23c | ||
|
|
eb4edeea8f | ||
|
|
e3ccd504da | ||
|
|
4627d7d5df | ||
|
|
f216c1b65d | ||
|
|
4692ba7183 | ||
|
|
543825918c | ||
|
|
9f995b861b | ||
|
|
9963a6c655 | ||
|
|
1696ec5c72 | ||
|
|
7c95faad21 | ||
|
|
ca80441c14 | ||
|
|
9c497fd2bb | ||
|
|
ef5e5e5b4c | ||
|
|
ede900fdbd | ||
|
|
1758f03f93 | ||
|
|
f18db748e0 | ||
|
|
2fba2af126 | ||
|
|
1ef3d712e1 | ||
|
|
405adff8f4 | ||
|
|
9a21ca1cf4 | ||
|
|
49252e66ce | ||
|
|
4ba2e900c5 | ||
|
|
b6714b559e | ||
|
|
a6c1757cf6 | ||
|
|
3d847ea00c | ||
|
|
63453942ee | ||
|
|
cb5a7904d3 | ||
|
|
f2c088fb18 | ||
|
|
4e2fd9dde6 | ||
|
|
92c1ca7b6f | ||
|
|
5fcad35cdc | ||
|
|
2b11fcea41 | ||
|
|
3f0c24d27b | ||
|
|
71088fca99 | ||
|
|
d6f7f0c0be | ||
|
|
4044b68701 | ||
|
|
610ced00af | ||
|
|
f9fecda1f1 | ||
|
|
74921ae1b3 | ||
|
|
40f2c88ead | ||
|
|
7fe410ae89 | ||
|
|
4cbdd085d6 | ||
|
|
24556dab83 | ||
|
|
dd8116eba8 | ||
|
|
ae82a8c0a0 | ||
|
|
af4dc5e9ea | ||
|
|
71b03a90dd | ||
|
|
728e50cd13 | ||
|
|
c8e8935b34 | ||
|
|
21f4b906d7 | ||
|
|
d49d42b736 | ||
|
|
267fe4e453 | ||
|
|
413ca61ef5 | ||
|
|
02b96470bc | ||
|
|
7234128f78 | ||
|
|
27e070e555 | ||
|
|
539950bdc5 | ||
|
|
83280e7cee | ||
|
|
cee0275565 | ||
|
|
87bddf0037 | ||
|
|
e736e3d810 | ||
|
|
092f15406c | ||
|
|
befcd0f112 | ||
|
|
8b92936d05 | ||
|
|
aec135de83 | ||
|
|
f9738168e5 | ||
|
|
e873b88dbd | ||
|
|
214f5b8a98 | ||
|
|
70cf17fdd5 | ||
|
|
7d557810cb | ||
|
|
ed24282703 | ||
|
|
e2cf3e81af | ||
|
|
28fa133b6d | ||
|
|
37893ced80 | ||
|
|
900c48292c | ||
|
|
302ae5ddb4 | ||
|
|
24fca08fdc | ||
|
|
91077dc132 | ||
|
|
273bbea938 | ||
|
|
36c44ad201 | ||
|
|
46f5a2d7a7 | ||
|
|
9b359a91cc | ||
|
|
0af831d50b | ||
|
|
3efaeaeeb9 | ||
|
|
00a20dda48 | ||
|
|
ec45160b27 | ||
|
|
eda284ca25 | ||
|
|
1ed331b4bb | ||
|
|
ee74bb4fbf | ||
|
|
df104669a3 | ||
|
|
9621b43bf2 | ||
|
|
503c1a5c8d | ||
|
|
a2fa1d4d78 | ||
|
|
6c6ad9a766 | ||
|
|
fc8807f74d | ||
|
|
edec48f9d4 | ||
|
|
ce98551057 | ||
|
|
74a2cc5b19 | ||
|
|
9628a940e9 | ||
|
|
d48643cbb9 | ||
|
|
558beaf216 | ||
|
|
6cc5d2452b | ||
|
|
e934ef7ffd | ||
|
|
e2d8032ef4 | ||
|
|
81e6101fb5 | ||
|
|
070354920b | ||
|
|
404168fb86 | ||
|
|
d5fa9fd02b | ||
|
|
6cc66d91be | ||
|
|
20a3b3b95e | ||
|
|
9145ea4234 | ||
|
|
0fa79433bd | ||
|
|
5185273b38 | ||
|
|
eadd23de4d | ||
|
|
71d9738980 | ||
|
|
80328af888 | ||
|
|
5e83da5575 | ||
|
|
521afe90ca | ||
|
|
ee3b7b5d7d | ||
|
|
95a9775c26 | ||
|
|
93d1319b25 | ||
|
|
b2bac3808b | ||
|
|
ebf58e68cf | ||
|
|
dc3e909dfe | ||
|
|
bf09e9615a | ||
|
|
b2d30d47d6 | ||
|
|
b1296fd24f | ||
|
|
62d8c89300 | ||
|
|
80c44eee89 | ||
|
|
153eb126fc | ||
|
|
f1fc166713 | ||
|
|
dda36bb036 | ||
|
|
843ad08813 | ||
|
|
a0e3c923e4 | ||
|
|
3095b03b09 | ||
|
|
ea5a0e0a7c | ||
|
|
2b4ec5b03b | ||
|
|
59f7add065 | ||
|
|
9c7bfc2347 | ||
|
|
ef90f40be4 | ||
|
|
bca6a4f22c | ||
|
|
1d3e2b56b2 | ||
|
|
a93a20ed5b | ||
|
|
971a80c183 | ||
|
|
125b60facc | ||
|
|
ef99b47178 | ||
|
|
d65f870213 | ||
|
|
5b3f1c9de2 | ||
|
|
4afb090cb1 | ||
|
|
8cf6e65ed8 | ||
|
|
c85c51ec08 | ||
|
|
dbeabea6c5 | ||
|
|
dfbe2d0f0f | ||
|
|
ea2277ea4e | ||
|
|
ad78105a6e | ||
|
|
36584175e2 | ||
|
|
084b1fdfd2 | ||
|
|
bed091e20f | ||
|
|
4420663687 | ||
|
|
677ae82ffa | ||
|
|
48a3fac9e0 | ||
|
|
e797d37b01 | ||
|
|
ffafd2e787 | ||
|
|
8500c971eb | ||
|
|
46a67a56fb | ||
|
|
5dc62e6df2 | ||
|
|
4479d6cf56 | ||
|
|
1faaf1f6b1 | ||
|
|
1f33ef191a | ||
|
|
d5bf8a6e3c | ||
|
|
5c252c7843 | ||
|
|
d2e10ccaf6 | ||
|
|
89d2b6870f | ||
|
|
26a9086d96 | ||
|
|
9fc8e6b1e5 | ||
|
|
6718650619 | ||
|
|
a622949f23 | ||
|
|
5a2df5b2c1 | ||
|
|
ae751cdd65 | ||
|
|
0d00bb2866 | ||
|
|
4299965064 | ||
|
|
6f3fc8db3d | ||
|
|
db731856f8 | ||
|
|
217f1dd6d4 | ||
|
|
cf87929476 | ||
|
|
d343a0720e | ||
|
|
51bd58edb2 | ||
|
|
f778d83316 | ||
|
|
b169206c68 | ||
|
|
4cea69e71d | ||
|
|
04d422b411 | ||
|
|
49dec52aa0 | ||
|
|
b83a62814d | ||
|
|
ce5449f25a | ||
|
|
aa769a8921 | ||
|
|
a8d4e6c754 | ||
|
|
f2402cf086 | ||
|
|
07a481f002 | ||
|
|
f41638db6d | ||
|
|
82baf41d8e | ||
|
|
e60370d4dc | ||
|
|
3e412323ee | ||
|
|
d635c1a056 | ||
|
|
f02db2a024 | ||
|
|
8c74324ff8 | ||
|
|
f2230eb39b | ||
|
|
ab900ae5da | ||
|
|
1cf28c56c2 | ||
|
|
08cd33a0cf | ||
|
|
7b0d2a239d | ||
|
|
5d922d934c | ||
|
|
5bab9ea914 | ||
|
|
da06d7e636 | ||
|
|
aece140473 | ||
|
|
656f87e3c6 | ||
|
|
36a95616d9 | ||
|
|
60bdc4715d | ||
|
|
7da168ab52 | ||
|
|
b20ad48b31 | ||
|
|
8b17495f0e | ||
|
|
e113e1b59c | ||
|
|
6bd4924c0b | ||
|
|
a2939aa183 | ||
|
|
effa726b65 | ||
|
|
20d9bf1f23 | ||
|
|
09e5d014df | ||
|
|
add1b8a68a | ||
|
|
fe143f66b7 | ||
|
|
75c8ca29ef | ||
|
|
c52779df23 | ||
|
|
b7b77646e6 | ||
|
|
1157c6b7cd | ||
|
|
ab824d7f66 | ||
|
|
702e28a6e6 | ||
|
|
0e0ab58959 | ||
|
|
e5cdc9ea3d | ||
|
|
d3c9744e65 | ||
|
|
6b02144b30 | ||
|
|
91cbc5ddf0 | ||
|
|
93aa1ca5aa | ||
|
|
6ae13ebb2f | ||
|
|
73cdbf64c7 | ||
|
|
0aeea965c0 | ||
|
|
a63b15637f | ||
|
|
5153b43bc6 | ||
|
|
9ea7d09722 | ||
|
|
b7b474b245 | ||
|
|
e2184a8bdf | ||
|
|
9ea84ca9ae | ||
|
|
50136e3689 | ||
|
|
e87f5e5f89 | ||
|
|
d44792d132 | ||
|
|
50b01e0b1b | ||
|
|
122c3a96c7 | ||
|
|
4d407bd646 | ||
|
|
16b6b37b94 | ||
|
|
8c766ef4f0 | ||
|
|
b7d2865435 | ||
|
|
4a158b2547 | ||
|
|
e381e54bb7 | ||
|
|
7001f98353 | ||
|
|
a836642d74 | ||
|
|
a3d3add48f | ||
|
|
994060cce2 | ||
|
|
75103dc706 | ||
|
|
cf63c2fdc5 | ||
|
|
f15b4cf634 | ||
|
|
6430611e3e | ||
|
|
afe57e41ce | ||
|
|
86f3f90143 | ||
|
|
d3ea51cc01 | ||
|
|
94dbec4486 |
30
.github/pull_request_template.md
vendored
Normal file
30
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
## Description
|
||||
<!-- Add a detailed summary of what this feature/bugfix does -->
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature
|
||||
|
||||
## Visuals
|
||||
- [ ] Attached relevant screenshots / screen recording / GIF
|
||||
- [ ] N/A (not a feature or no UI changes)
|
||||
|
||||
## Testing
|
||||
- [ ] Ran all E2E tests
|
||||
- [ ] Ran all integration tests
|
||||
- [ ] Added/updated tests for this change (if needed)
|
||||
- [ ] N/A (tests not needed — explanation provided below)
|
||||
|
||||
### If tests were not added, explain why
|
||||
<!-- explanation -->
|
||||
|
||||
## Platform
|
||||
<!-- Describe which platforms this PR is related to -->
|
||||
|
||||
- [ ] Web
|
||||
- [ ] Mobile
|
||||
- [ ] Desktop
|
||||
|
||||
## Sign-off
|
||||
- [ ] QA passed
|
||||
- [ ] UI/UX passed
|
||||
135
.github/workflows/android.preview.firebase.yml
vendored
Normal file
135
.github/workflows/android.preview.firebase.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
name: Notesnook Android Preview
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/android.preview.firebase.yml"
|
||||
|
||||
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
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
STAGING_BUILD: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build arm64-v8a apk
|
||||
run: |
|
||||
cd apps/mobile/android
|
||||
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: saionaro/extract-package-version@master
|
||||
with:
|
||||
path: apps/mobile
|
||||
|
||||
- name: Publish to firebase CLI
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{secrets.FIREBASE_APP_ID}}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- android-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/android/app/build/generated/sourcemaps/**/*.map
|
||||
301
.github/workflows/desktop.preview.yml
vendored
Normal file
301
.github/workflows/desktop.preview.yml
vendored
Normal file
@@ -0,0 +1,301 @@
|
||||
name: Preview @notesnook/desktop
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/desktop/**"
|
||||
- "apps/web/**"
|
||||
- "packages/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.preview.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Generate desktop build (stable)
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Build desktop bundle
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run bundle
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
runs-on: macos-14
|
||||
outputs:
|
||||
macos-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install setuptools
|
||||
run: brew install python-setuptools
|
||||
|
||||
- name: Setup notarization
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
||||
|
||||
- name: Collect app metadata
|
||||
id: app_metadata
|
||||
working-directory: ./apps/desktop
|
||||
run: |
|
||||
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
|
||||
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
echo ::set-output name=bundle_version::$(cat package.json | jq -r .build.mac.bundleVersion)
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build dmg
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.mac_certs }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
run: |
|
||||
npm run tx @notesnook/desktop:release
|
||||
cd apps/desktop
|
||||
yarn electron-builder --config=electron-builder.config.js --mac dmg --arm64 -p never
|
||||
|
||||
- name: Upload dmg artifact
|
||||
id: artifact-upload-step
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-build
|
||||
path: apps/desktop/output/*.dmg
|
||||
|
||||
build-linux-x64:
|
||||
name: Build for Linux x64
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
linux-x64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build AppImage
|
||||
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload AppImage artifact
|
||||
id: artifact-upload-step
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-x64-build
|
||||
path: apps/desktop/output/*.AppImage
|
||||
|
||||
build-linux-arm64:
|
||||
name: Build for Linux arm64
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04-arm
|
||||
outputs:
|
||||
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build AppImage
|
||||
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload AppImage artifact
|
||||
id: artifact-upload-step
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-arm64-build
|
||||
path: apps/desktop/output/*.AppImage
|
||||
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
NOTESNOOK_STAGING: true
|
||||
run: yarn electron-builder --config=electron-builder.config.js --win nsis:x64 --publish never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload exe artifact
|
||||
id: artifact-upload-step
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-build
|
||||
path: apps/desktop/output/*.exe
|
||||
|
||||
post-pr-comment:
|
||||
name: Post PR comment with preview URLs
|
||||
needs:
|
||||
[build, build-macos, build-linux-x64, build-linux-arm64, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
macos_artifact_url: ${{ needs.build-macos.outputs.macos-artifact-url }}
|
||||
linux_x64_artifact_url: ${{ needs.build-linux-x64.outputs.linux-x64-artifact-url }}
|
||||
linux_arm64_artifact_url: ${{ needs.build-linux-arm64.outputs.linux-arm64-artifact-url }}
|
||||
windows_artifact_url: ${{ needs.build-windows.outputs.windows-artifact-url }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- desktop-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = [
|
||||
{ platform: 'macOS', url: process.env.macos_artifact_url },
|
||||
{ platform: 'Linux x64', url: process.env.linux_x64_artifact_url },
|
||||
{ platform: 'Linux arm64', url: process.env.linux_arm64_artifact_url },
|
||||
{ platform: 'Windows x64', url: process.env.windows_artifact_url },
|
||||
].filter(u => u.url).map(u => `- [${u.platform}](${u.url})`).join('\n');
|
||||
const body = `${marker}\n**Desktop Previews**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
168
.github/workflows/ios.preview.firebase.yml
vendored
Normal file
168
.github/workflows/ios.preview.firebase.yml
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
name: Notesnook iOS Preview
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
- ".github/workflows/ios.preview.firebase.yml"
|
||||
|
||||
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
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.1.1"
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npm run tx @notesnook/mobile:build
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@v3
|
||||
id: pods-cache
|
||||
with:
|
||||
path: apps/mobile/ios/Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
|
||||
|
||||
- name: Install Pods
|
||||
run: |
|
||||
cd apps/mobile/ios
|
||||
pod install
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Get marketing version
|
||||
id: marketing-version
|
||||
run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Make staging xcconfig active
|
||||
run: |
|
||||
cd apps/mobile/ios/build-configs
|
||||
./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}}
|
||||
|
||||
- name: Build iOS App
|
||||
uses: ammarahm-ed/ios-build-action@master
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
export-options: apps/mobile/ios/ExportOptionsStaging.plist
|
||||
export-method: "ad-hoc"
|
||||
project-path: apps/mobile/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
|
||||
update-targets: |
|
||||
Notesnook
|
||||
Make Note
|
||||
NotesWidgetExtension
|
||||
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
|
||||
code-signing-identity: Apple Distribution
|
||||
team-id: ${{ secrets.APPLE_TEAM_ID }}
|
||||
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
||||
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
|
||||
output-path: Notesnook.ipa
|
||||
mobileprovision-base64: |
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
|
||||
|
||||
- name: Upload IPA artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-preview-ipa
|
||||
path: Notesnook.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Download IPA artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ios-preview-ipa
|
||||
path: .
|
||||
|
||||
- name: Publish to firebase CLI
|
||||
id: firebase-output
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{secrets.FIREBASE_IOS_APP_ID}}
|
||||
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
groups: testers
|
||||
file: Notesnook.ipa
|
||||
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- ios-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
40
.github/workflows/web.benchmarks.yml
vendored
40
.github/workflows/web.benchmarks.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Benchmark @notesnook/web
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.benchmarks.yml"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: Bench
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: apps/web
|
||||
|
||||
- name: Run benchmarks
|
||||
id: benchmark
|
||||
run: node __bench__/app.bench.mjs
|
||||
working-directory: apps/web
|
||||
80
.github/workflows/web.preview.yml
vendored
Normal file
80
.github/workflows/web.preview.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Notesnook Web PR Preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "packages/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.preview.yml"
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build web
|
||||
run: npm run build:web
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
id: deploy
|
||||
working-directory: ./apps/web
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BRANCH=pr-${{ github.event.number }}-$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
echo "Deploying branch: $BRANCH"
|
||||
DEPLOY_OUT=$(npx --yes wrangler pages deploy --project-name=notesnook-app --branch="$BRANCH" ./build 2>&1) || { echo "$DEPLOY_OUT"; exit 1; }
|
||||
echo "$DEPLOY_OUT"
|
||||
PREVIEW_URL=$(printf "%s" "$DEPLOY_OUT" | grep -Eo 'https?://[^ ]+' | head -1 || true)
|
||||
if [ -z "$PREVIEW_URL" ]; then
|
||||
echo "WARNING: could not parse preview URL from wrangler output"
|
||||
fi
|
||||
echo "preview_url=$PREVIEW_URL" >> $GITHUB_ENV
|
||||
|
||||
- name: Post or update PR comment
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
preview_url: ${{ env.preview_url }}
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- pages-preview-comment -->';
|
||||
const prNumber = context.issue.number;
|
||||
const previewUrl = process.env.preview_url || '';
|
||||
const body = `${marker}\n**Cloudflare Pages Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@@ -198,6 +198,9 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
},
|
||||
toolsets: {
|
||||
appimage: "1.0.2"
|
||||
},
|
||||
snap: {
|
||||
autoStart: false,
|
||||
confinement: "strict",
|
||||
|
||||
2132
apps/desktop/package-lock.json
generated
2132
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.8",
|
||||
"version": "3.3.17",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -48,7 +48,7 @@
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "0.21.5",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
|
||||
@@ -27,6 +27,7 @@ import crypto from "crypto";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const root = path.join(__dirname, "..");
|
||||
const RUNNING_PROCESSES = [];
|
||||
const RESTARTABLE_PROCESSES = [];
|
||||
let lastBundleHash = null;
|
||||
@@ -36,7 +37,7 @@ const ENV = {
|
||||
FORCE_COLOR: "false",
|
||||
COLOR: "0"
|
||||
};
|
||||
process.chdir(path.join(__dirname, ".."));
|
||||
process.chdir(root);
|
||||
|
||||
await onChange(true);
|
||||
|
||||
@@ -55,13 +56,16 @@ async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("npm rebuild electron --verbose --foreground-scripts");
|
||||
await exec(
|
||||
"npm run postinstall --verbose",
|
||||
path.join(root, "node_modules", "electron")
|
||||
);
|
||||
|
||||
await exec("yarn electron-builder install-app-deps");
|
||||
await exec("yarn electron-builder install-app-deps", root);
|
||||
}
|
||||
|
||||
await exec(`yarn run bundle`);
|
||||
await exec(`yarn run build`);
|
||||
await exec(`yarn run bundle`, root);
|
||||
await exec(`yarn run build`, root);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
@@ -111,7 +115,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
|
||||
async function exec(cmd, cwd) {
|
||||
try {
|
||||
console.log(">", cmd);
|
||||
console.log(">", cmd, cwd);
|
||||
|
||||
return execSync(cmd, {
|
||||
env: ENV,
|
||||
|
||||
@@ -178,7 +178,9 @@ export const osIntegrationRouter = t.router({
|
||||
})
|
||||
});
|
||||
notification.show();
|
||||
if (input.urgency === "critical") {
|
||||
if (input.urgency === "critical" && process.platform !== "linux") {
|
||||
// due to an Electron bug in versions below 40.x, shell.beep() causes a segfault on Linux.
|
||||
// TODO: Remove when migrating to Electron 40.x or newer.
|
||||
shell.beep();
|
||||
}
|
||||
|
||||
@@ -252,6 +254,7 @@ export const osIntegrationRouter = t.router({
|
||||
if (menuItem) menu.append(menuItem);
|
||||
}
|
||||
if (menu.items.length > 0) menu.popup();
|
||||
menu.on("menu-will-close", () => emit.next([]));
|
||||
return () => {
|
||||
menu.removeAllListeners();
|
||||
menu.closePopup();
|
||||
|
||||
@@ -113,7 +113,8 @@ export class SQLite {
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
if (e instanceof Error)
|
||||
throw rewriteError(e, `${e.message} (query: ${sql})`);
|
||||
throw e;
|
||||
} finally {
|
||||
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
|
||||
@@ -212,3 +213,11 @@ function getExtensionPath(extensionName: string, entryPoint: string) {
|
||||
}
|
||||
return loadablePath;
|
||||
}
|
||||
|
||||
function rewriteError(e: Error, message: string) {
|
||||
const error = new Error(message);
|
||||
error.stack = e.stack;
|
||||
error.name = e.name;
|
||||
error.cause = e.cause;
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,17 @@ export const windowRouter = t.router({
|
||||
}),
|
||||
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
|
||||
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
|
||||
onClose: t.procedure.subscription(() => {
|
||||
return observable<void>((emit) => {
|
||||
function listener() {
|
||||
emit.next();
|
||||
}
|
||||
globalThis.window?.addListener("close", listener);
|
||||
return () => {
|
||||
globalThis.window?.removeListener("close", listener);
|
||||
};
|
||||
});
|
||||
}),
|
||||
onWindowStateChanged: t.procedure.subscription(() => {
|
||||
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
|
||||
function listener() {
|
||||
|
||||
@@ -52,6 +52,9 @@ locale.then(({ default: locale }) => {
|
||||
});
|
||||
setI18nGlobal(i18n);
|
||||
|
||||
const appHostnames = isDevelopment()
|
||||
? ["localhost", "127.0.0.1"]
|
||||
: ["app.notesnook.com"];
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
console.log("Another instance is already running!");
|
||||
@@ -173,6 +176,19 @@ async function createWindow() {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
mainWindow.webContents.on("will-navigate", (event, url) => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (!appHostnames.includes(parsedUrl.hostname)) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("will-navigate: failed to parse URL", url, e);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
nativeTheme.on("updated", () => {
|
||||
setupTray();
|
||||
setupJumplist();
|
||||
|
||||
@@ -37,7 +37,11 @@ async function configureAutoUpdater() {
|
||||
config.releaseTrack === "stable" &&
|
||||
autoUpdater.currentVersion.prerelease.length > 0;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
// Do NOT auto-install on quit. On Windows, if the system shuts down while
|
||||
// the NSIS installer is running, it first removes all old files and then
|
||||
// gets killed before copying new ones — leaving an empty install directory.
|
||||
// Updates should only be installed when the user explicitly triggers it.
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,17 @@ function registerProtocol() {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
return net.fetch(request, { bypassCustomProtocolHandlers: true });
|
||||
const requestHeaders = new Headers(request.headers);
|
||||
if (!requestHeaders.has("referer"))
|
||||
requestHeaders.set("referer", PROTOCOL_URL);
|
||||
return await net.fetch(
|
||||
new Request(request, {
|
||||
headers: requestHeaders
|
||||
}),
|
||||
{
|
||||
bypassCustomProtocolHandlers: true
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
</p>
|
||||
|
||||
<h1 align="center">Notesnook Mobile</h1>
|
||||
<h3 align="center">The mobile app is built using React Native, Typescript & Javascript for both iOS & Android.</h3>
|
||||
<p align="center">
|
||||
<a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
|
||||
</p>
|
||||
<h3 align="center">The mobile app is built with React Native for both iOS and Android.</h3>
|
||||
<p align="center"><a href="#build-instructions">Build instructions</a> | <a href="#developer-guide">Developer guide</a> | <a href="#running-e2e-tests-detox">E2E tests</a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.streetwriters.notesnook">
|
||||
@@ -25,21 +23,21 @@
|
||||
|
||||
Requirements:
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/download/)
|
||||
1. [Node.js](https://nodejs.org/en/download/) 20+ (the repo is pinned to Node `22.20.0` via Volta)
|
||||
2. [git](https://git-scm.com/downloads)
|
||||
3. NPM (not yarn or pnpm)
|
||||
4. [React Native](https://reactnative.dev/docs/environment-setup)
|
||||
3. `npm`
|
||||
4. [React Native environment setup](https://reactnative.dev/docs/set-up-your-environment)
|
||||
|
||||
To run the app locally, you will need to setup React Native on your system:
|
||||
To run the app locally, first complete React Native native tooling setup:
|
||||
|
||||
1. Open the official [environment setup guide here](https://reactnative.dev/docs/environment-setup)
|
||||
1. Open [React Native environment setup](https://reactnative.dev/docs/set-up-your-environment)
|
||||
2. Select `React Native CLI Quickstart`
|
||||
3. Select your OS & the platform to run the app on (iOS or Android)
|
||||
3. Select your OS and target platform(s): iOS and/or Android
|
||||
4. Follow the steps listed.
|
||||
|
||||
> Please keep in mind that **Expo is not supported**.
|
||||
> Expo is not used in this project.
|
||||
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
Clone the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
@@ -48,26 +46,27 @@ git clone https://github.com/streetwriters/notesnook.git
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
Install dependencies and bootstrap the mobile workspace:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
npm run bootstrap -- --scope=mobile
|
||||
```
|
||||
|
||||
### Running the app on Android
|
||||
|
||||
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
|
||||
[Set up an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) (or connect a physical device), then run:
|
||||
|
||||
```bash
|
||||
npm run start:android
|
||||
```
|
||||
|
||||
If you want to run the app on your phone, make sure to [enable USB debugging](https://developer.android.com/studio/debug/dev-options).
|
||||
If you are using a physical device, enable [USB debugging](https://developer.android.com/studio/debug/dev-options).
|
||||
|
||||
### Running the app on iOS
|
||||
|
||||
To run the app on iOS:
|
||||
Install CocoaPods dependencies first, then run the iOS app:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
@@ -76,90 +75,87 @@ npm run prepare:ios
|
||||
npm run start:ios
|
||||
```
|
||||
|
||||
### Useful development commands
|
||||
|
||||
```bash
|
||||
# start Metro only
|
||||
npm run start:metro
|
||||
|
||||
# start Re.Pack bundler
|
||||
npm run start:repack
|
||||
```
|
||||
|
||||
## Developer guide
|
||||
|
||||
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
|
||||
> The mobile app is a mixed TypeScript/JavaScript codebase.
|
||||
|
||||
### The tech stack
|
||||
|
||||
We try to keep the stack as lean as possible:
|
||||
|
||||
1. React Native
|
||||
2. Typescript/Javascript
|
||||
3. Zustand: State management
|
||||
4. Detox: Runs all our e2e tests
|
||||
5. React Native MMKV: Database & persistence
|
||||
6. libsodium: Encryption
|
||||
1. React Native `0.82`
|
||||
2. React `19`
|
||||
3. TypeScript + JavaScript
|
||||
4. Zustand (state management)
|
||||
5. Detox (end-to-end testing)
|
||||
6. libsodium (encryption)
|
||||
|
||||
### Project structure
|
||||
|
||||
The app codebase is distributed over two primary directories. `native/` and `app/`.
|
||||
Top-level directories in `apps/mobile/`:
|
||||
|
||||
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
|
||||
- `app/`: Main React Native app source (`components`, `common`, `hooks`, `navigation`, `screens`, `services`, `stores`, `utils`, etc.)
|
||||
- `android/`: Android native project
|
||||
- `ios/`: iOS native project
|
||||
- `e2e/`: Detox test suite and config
|
||||
- `patches/`: `patch-package` patches
|
||||
- `scripts/`: Mobile-specific scripts
|
||||
|
||||
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
|
||||
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
|
||||
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
|
||||
- `hooks/`: Hooks for different app logic
|
||||
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
|
||||
- `screens`: Navigator screens.
|
||||
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
|
||||
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
|
||||
- `utils`: General purpose stuff such as constant values, utility functions, etc.
|
||||
## Running E2E tests (Detox)
|
||||
|
||||
There are several other folders at the root:
|
||||
Detox device defaults in this repo:
|
||||
|
||||
- `share/`: Code for the iOS Share Extension and Android widget.
|
||||
- `e2e/`: Detox End to end tests
|
||||
- `patches/`: Patches for various react native dependencies.
|
||||
|
||||
### Running the tests
|
||||
|
||||
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
|
||||
- Android emulator: `Pixel_5_API_36`
|
||||
- iOS simulator: `iPhone 17 Pro Max`
|
||||
|
||||
### Android
|
||||
|
||||
To run the tests on Android, you will need to create an emulator device on your system:
|
||||
Build and run Android Detox tests:
|
||||
|
||||
```
|
||||
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
|
||||
```
|
||||
|
||||
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
|
||||
|
||||
Once you have created an emulator device, build the Android apks:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm run build:android
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
|
||||
```
|
||||
npm run test:android
|
||||
```
|
||||
|
||||
For debug configuration:
|
||||
|
||||
```bash
|
||||
npm run build:android:debug
|
||||
npm run start:metro
|
||||
npm run test:android:debug
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
|
||||
|
||||
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
|
||||
Build and run iOS Detox tests:
|
||||
|
||||
```bash
|
||||
npm run build:ios
|
||||
npm run test:ios
|
||||
```
|
||||
|
||||
If simulator tooling is missing, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
|
||||
|
||||
```bash
|
||||
brew tap wix/brew
|
||||
brew install applesimutils
|
||||
```
|
||||
|
||||
Now build the iOS app for testing:
|
||||
## Release commands
|
||||
|
||||
```
|
||||
npm run build:ios
|
||||
```
|
||||
|
||||
Finally, run the tests:
|
||||
Android release helpers:
|
||||
|
||||
```bash
|
||||
npm run release:android
|
||||
npm run release:android:bundle
|
||||
```
|
||||
npm run test:ios
|
||||
```
|
||||
|
||||
All tests on iOS are configured to run on `iPhone 8` simulator.
|
||||
|
||||
@@ -5,6 +5,7 @@ apply plugin: 'kotlin-parcelize'
|
||||
|
||||
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
|
||||
|
||||
|
||||
import com.android.build.OutputFile
|
||||
import groovy.json.JsonSlurper
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
@@ -94,6 +95,18 @@ def fdroidBuild() {
|
||||
return project.hasProperty("fdroidBuild") && project.fdroidBuild == "true"
|
||||
}
|
||||
|
||||
def prBuildNumber() {
|
||||
def value = project.getProperties().get("prBuildNumber")
|
||||
return value ? value : "1000"
|
||||
}
|
||||
|
||||
def stagingReleaseBuild() {
|
||||
// Enable with STAGING_BUILDtrue or -PstagingReleaseBuild=true
|
||||
def fromEnv = System.getenv("STAGING_BUILD") == "true"
|
||||
def fromProp = project.hasProperty("stagingReleaseBuild") && project.stagingReleaseBuild == "true"
|
||||
return fromEnv || fromProp
|
||||
}
|
||||
|
||||
def getNpmVersion() {
|
||||
def inputFile = file("$rootDir/../package.json")
|
||||
def jsonPackage = new JsonSlurper().parseText(inputFile.text)
|
||||
@@ -118,14 +131,17 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
namespace "com.streetwriters.notesnook"
|
||||
defaultConfig {
|
||||
applicationId "com.streetwriters.notesnook"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3091
|
||||
if (project.hasProperty("prBuildNumber")) {
|
||||
versionCode Integer.parseInt(prBuildNumber())
|
||||
} else {
|
||||
versionCode 3103
|
||||
}
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -192,6 +208,8 @@ android {
|
||||
output.versionCodeOverride =
|
||||
defaultConfig.versionCode * 5 + versionCodes.get(abi)
|
||||
println("Fdroid Version code: ${output.versionCodeOverride} for abi ${versionCodes.get(abi)}");
|
||||
} else if (stagingReleaseBuild()) {
|
||||
output.versionCodeOverride = defaultConfig.versionCode + versionCodes.get(abi)
|
||||
} else {
|
||||
if (isBuildingAAB) {
|
||||
output.versionCodeOverride = 4 * 1048576 + defaultConfig.versionCode
|
||||
@@ -233,7 +251,6 @@ dependencies {
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -199,11 +199,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".OnClearFromRecentService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
|
||||
<service
|
||||
|
||||
@@ -39,28 +39,32 @@ public class BootTaskService extends HeadlessJsTaskService {
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel("com.streetwriters.notesnook",
|
||||
"Default",
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel("com.streetwriters.notesnook",
|
||||
"Default",
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
|
||||
notification = new NotificationCompat.Builder(this, channel.getId())
|
||||
.setContentTitle("Sync on boot")
|
||||
.setContentText("")
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.build();
|
||||
if (android.os.Build.VERSION.SDK_INT >= 34) {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification);
|
||||
}
|
||||
|
||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
|
||||
notification = new NotificationCompat.Builder(this, channel.getId())
|
||||
.setContentTitle("Sync on boot")
|
||||
.setContentText("")
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.build();
|
||||
if (android.os.Build.VERSION.SDK_INT >= 34) {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else {
|
||||
this.startForeground(
|
||||
1,
|
||||
notification);
|
||||
}
|
||||
|
||||
} catch (Exception ignored) {
|
||||
stopSelf(startId);
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
|
||||
@@ -28,10 +28,6 @@ public class MainActivity extends ReactActivity {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
|
||||
try {
|
||||
startService(new Intent(getBaseContext(), OnClearFromRecentService.class));
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
public class OnClearFromRecentService extends Service {
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
try {
|
||||
SharedPreferences appStateDetails = getApplicationContext().getSharedPreferences("appStateDetails", MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = appStateDetails.edit();
|
||||
edit.remove("appState");
|
||||
edit.apply();
|
||||
stopSelf();
|
||||
} catch (UnsatisfiedLinkError | Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,21 +93,6 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setAppState(final String appState) {
|
||||
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = appStateDetails.edit();
|
||||
edit.putString("appState", appState);
|
||||
edit.apply();
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public String getAppState() {
|
||||
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
|
||||
String appStateValue = appStateDetails.getString("appState", "");
|
||||
return appStateValue.isEmpty() ? null : appStateValue;
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public int getWidgetId() {
|
||||
return NotePreviewConfigureActivity.appWidgetId;
|
||||
|
||||
@@ -4,4 +4,10 @@
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
</domain-config>
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -36,6 +36,8 @@ allprojects {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
- Add notebooks, tags and colors to Home Screen Shortcuts
|
||||
- Change day format and use /day in notes
|
||||
- Add Setting to change default editor line height
|
||||
- Set a custom title for monographs
|
||||
- Add webpage title and date clipped to web clips
|
||||
- Configure Week to start from Sunday or Monday
|
||||
- Change Note's creation date
|
||||
- Set expiry date on notes
|
||||
- Temporarily disable password change and recovery options
|
||||
- Note history now includes note title
|
||||
- Minor bug fixes
|
||||
- Bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
Thank you for using Notesnook!
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "Notesnook",
|
||||
"displayName": "Notesnook"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { PropsWithChildren, useEffect } from "react";
|
||||
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
@@ -66,6 +66,9 @@ const App = (props: { configureMode: "note-preview" }) => {
|
||||
useEffect(() => {
|
||||
SettingsService.onFirstLaunch();
|
||||
changeSystemBarColors();
|
||||
SettingsService.setPrivacyScreen(
|
||||
SettingsService.getProperty("privacyScreen")
|
||||
);
|
||||
setTimeout(async () => {
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
@@ -106,8 +109,10 @@ let currTheme =
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
return function AppWithThemeProvider(props: any) {
|
||||
export const withTheme = (
|
||||
Element: (props: PropsWithChildren) => JSX.Element
|
||||
) => {
|
||||
return function AppWithThemeProvider(props: PropsWithChildren) {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
state.darkTheme,
|
||||
@@ -127,8 +132,12 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
? useThemeStore.setState({
|
||||
darkTheme: theme
|
||||
})
|
||||
: useThemeStore.setState({
|
||||
lightTheme: theme
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -138,9 +147,9 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
useThemeStore
|
||||
.getState()
|
||||
.setColorScheme(colorScheme as "light" | "dark");
|
||||
useThemeStore.setState({
|
||||
colorScheme: colorScheme as "light" | "dark"
|
||||
});
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
|
||||
@@ -109,8 +109,8 @@ class RNSqliteConnection implements DatabaseConnection {
|
||||
query.kind === "SelectQueryNode"
|
||||
? "query"
|
||||
: query.kind === "RawNode"
|
||||
? "raw"
|
||||
: "exec";
|
||||
? "raw"
|
||||
: "exec";
|
||||
|
||||
const result = await this.db.executeAsync(sql, parameters as any[]);
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ import {
|
||||
getCacheSize,
|
||||
hashBase64,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64
|
||||
writeEncryptedBase64,
|
||||
bulkDeleteFiles
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import {
|
||||
@@ -61,5 +62,6 @@ export const FileStorage: IFileStorage = {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
bulkExists
|
||||
bulkExists,
|
||||
bulkDeleteFiles
|
||||
};
|
||||
|
||||
@@ -30,7 +30,14 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import {
|
||||
ABYTES,
|
||||
cacheDir,
|
||||
cacheDirOld,
|
||||
getRandomId,
|
||||
isSuccessStatusCode,
|
||||
parseS3Error
|
||||
} from "./utils";
|
||||
|
||||
export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
filename: string,
|
||||
@@ -102,16 +109,41 @@ export async function writeEncryptedBase64(
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteLocalFile(filename: string) {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const path = cacheDir + `/${filename}`;
|
||||
const exists = await RNFetchBlob.fs.exists(path);
|
||||
if (Platform.OS === "ios" && !exists) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
if (await RNFetchBlob.fs.exists(appGroupPath)) {
|
||||
RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (exists) {
|
||||
RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error, "deleteLocalFile");
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
await createCacheDir();
|
||||
const localFilePath = cacheDir + `/${filename}`;
|
||||
if (!requestOptions) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
deleteLocalFile(filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -122,9 +154,7 @@ export async function deleteFile(
|
||||
const status = response.info().status;
|
||||
const ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
deleteLocalFile(filename);
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
@@ -135,6 +165,49 @@ export async function deleteFile(
|
||||
}
|
||||
}
|
||||
|
||||
export async function bulkDeleteFiles(
|
||||
filenames: string[],
|
||||
requestOptions?: RequestOptions
|
||||
) {
|
||||
await createCacheDir();
|
||||
if (!requestOptions) {
|
||||
filenames.forEach((filename) => {
|
||||
deleteLocalFile(filename);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const { url, headers } = requestOptions;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...headers,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
names: filenames
|
||||
})
|
||||
});
|
||||
|
||||
const result = isSuccessStatusCode(response.status);
|
||||
if (result) {
|
||||
filenames.forEach((filename) => {
|
||||
deleteLocalFile(filename);
|
||||
});
|
||||
} else {
|
||||
throw await response.text();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(
|
||||
typeof e === "string" ? parseS3Error(e as string) : (e as Error),
|
||||
"Could not bulk delete files"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
@@ -236,7 +309,9 @@ export async function deleteDCacheFiles() {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
/** Empty */
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCachePathForFile(filename: string) {
|
||||
|
||||
@@ -33,6 +33,10 @@ import {
|
||||
} from "./utils";
|
||||
import Upload from "@ammarahmed/react-native-upload";
|
||||
import { CloudUploader } from "react-native-nitro-cloud-uploader";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
// Upload constants
|
||||
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
@@ -197,6 +201,20 @@ export async function uploadFile(
|
||||
const remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === FileSizeResult.Error) return false;
|
||||
|
||||
const featureResult = await isFeatureAvailable(
|
||||
"fileSize",
|
||||
fileInfo.size || 0
|
||||
);
|
||||
|
||||
if (!featureResult.isAllowed) {
|
||||
ToastManager.show({
|
||||
heading: strings.fileTooLarge(),
|
||||
message: featureResult.error,
|
||||
type: "error"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
remoteFileSize > FileSizeResult.Empty &&
|
||||
remoteFileSize === fileInfo.size
|
||||
@@ -212,6 +230,9 @@ export async function uploadFile(
|
||||
);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
const status = await PermissionsAndroid.request(
|
||||
"android.permission.POST_NOTIFICATIONS"
|
||||
);
|
||||
@@ -221,6 +242,10 @@ export async function uploadFile(
|
||||
type: "info"
|
||||
});
|
||||
}
|
||||
await sleep(500);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}
|
||||
|
||||
let uploaded = false;
|
||||
|
||||
@@ -161,10 +161,10 @@ export async function checkUpload(
|
||||
size === 0
|
||||
? `File size is 0.`
|
||||
: size === -1
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
@@ -193,3 +193,7 @@ export async function checkAndCreateDir(path: string) {
|
||||
export const santizeUri = (uri: string) => {
|
||||
return Platform.OS === "ios" ? decodeURI(uri).replace("file:///", "/") : uri;
|
||||
};
|
||||
|
||||
export function isSuccessStatusCode(statusCode: number) {
|
||||
return statusCode >= 200 && statusCode <= 299;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Announcement = () => {
|
||||
state.announcements,
|
||||
state.remove
|
||||
]);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return !announcement || selectionMode ? null : (
|
||||
|
||||
@@ -32,7 +32,7 @@ import { Action } from "../../stores/use-message-store";
|
||||
|
||||
export const Cta = (props: BodyItemProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
let buttons =
|
||||
const buttons =
|
||||
props.item.actions.filter((item) => allowedOnPlatform(item.platforms)) ||
|
||||
[];
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
eOnLoadNote
|
||||
} from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { openNote } from "../list-items/note/wrapper";
|
||||
@@ -59,6 +58,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Navigation from "../../services/navigation";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -177,26 +177,52 @@ const Actions = ({
|
||||
{
|
||||
name: strings.delete(),
|
||||
onPress: async () => {
|
||||
const relations = await db.relations.to(attachment, "note").get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused = useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(true, tab.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
close?.();
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: strings.deleteAttachment(),
|
||||
paragraph: strings.deleteAttachmentConfirm(),
|
||||
positiveText: strings.yes(),
|
||||
negativeText: strings.no(),
|
||||
positiveType: "errorShade",
|
||||
positivePress: async () => {
|
||||
try {
|
||||
const relations = await db.relations
|
||||
.to(attachment, "note")
|
||||
.get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
ToastManager.show({
|
||||
type: "success",
|
||||
message: strings.attachmentDeleted()
|
||||
});
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused =
|
||||
useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(
|
||||
true,
|
||||
tab.id
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
@@ -304,9 +330,9 @@ const Actions = ({
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
await sleep(150);
|
||||
close?.();
|
||||
eSendEvent(eCloseAttachmentDialog);
|
||||
await sleep(300);
|
||||
Navigation.navigate("FluidPanelsView");
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
style={{
|
||||
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LegendList } from "@legendapp/list";
|
||||
import {
|
||||
Attachment,
|
||||
FilteredSelector,
|
||||
@@ -28,7 +27,7 @@ import {
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, FlatList, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import create from "zustand";
|
||||
@@ -313,6 +312,10 @@ export const AttachmentDialog = ({
|
||||
});
|
||||
};
|
||||
|
||||
db.attachments.orphaned.items().then((r) => {
|
||||
console.log(r);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSheet ? (
|
||||
@@ -509,7 +512,7 @@ export const AttachmentDialog = ({
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<LegendList
|
||||
<FlatList
|
||||
renderScrollComponent={(props) => <ScrollView {...props} />}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
@@ -543,7 +546,6 @@ export const AttachmentDialog = ({
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={loading ? [] : attachments?.placeholders || []}
|
||||
extraData={attachments}
|
||||
renderItem={renderItem}
|
||||
|
||||
@@ -73,7 +73,15 @@ export const ChangePassword = () => {
|
||||
throw new Error(strings.backupFailed() + `: ${result.error}`);
|
||||
}
|
||||
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
const passwordChanged = await db.user.changePassword(
|
||||
oldPassword.current,
|
||||
password.current
|
||||
);
|
||||
|
||||
if (!passwordChanged) {
|
||||
throw new Error("Could not change user account password.");
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: strings.passwordChangedSuccessfully(),
|
||||
type: "success",
|
||||
|
||||
@@ -39,7 +39,7 @@ export function hideAuth(context?: AuthParams["context"]) {
|
||||
initialAuthMode.current === AuthMode.welcomeLogin ||
|
||||
context === "intro"
|
||||
) {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -28,40 +27,41 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const email = useRef<string>(undefined);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: userEmail || ""
|
||||
})
|
||||
);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const sendRecoveryEmail = async () => {
|
||||
if (!email.current || error) {
|
||||
ToastManager.show({
|
||||
heading: strings.emailRequired(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (formRef.current.validateField("email")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
let lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
|
||||
const lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
|
||||
if (
|
||||
lastRecoveryEmailTime &&
|
||||
Date.now() - lastRecoveryEmailTime < 60000 * 3
|
||||
) {
|
||||
throw new Error(strings.pleaseWaitBeforeSendEmail());
|
||||
}
|
||||
await db.user.recoverAccount(email.current.toLowerCase());
|
||||
await db.user.recoverAccount(values.email.toLowerCase());
|
||||
SettingsService.set({
|
||||
lastRecoveryEmailTime: Date.now()
|
||||
});
|
||||
@@ -76,105 +76,85 @@ export const ForgotPassword = () => {
|
||||
setSent(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryEmailFailed(),
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
formRef.current.setError("email", (e as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionSheet
|
||||
onBeforeShow={(data) => (email.current = data)}
|
||||
onClose={() => {
|
||||
setSent(false);
|
||||
setLoading(false);
|
||||
}}
|
||||
onOpen={() => {
|
||||
emailInputRef.current?.setNativeProps({
|
||||
text: email.current
|
||||
});
|
||||
}}
|
||||
indicatorStyle={{
|
||||
width: 100
|
||||
}}
|
||||
gestureEnabled
|
||||
id="forgotpassword_sheet"
|
||||
>
|
||||
{sent ? (
|
||||
<View
|
||||
{sent ? (
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 50
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 50
|
||||
width: null,
|
||||
height: null
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
name="email"
|
||||
size={50}
|
||||
/>
|
||||
<Heading>{strings.recoveryEmailSent()}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
width: null,
|
||||
height: null
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
name="email"
|
||||
size={50}
|
||||
/>
|
||||
<Heading>{strings.recoveryEmailSent()}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{strings.recoveryEmailSentDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
{strings.recoveryEmailSentDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
<Seperator />
|
||||
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
loading={loading}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
sendRecoveryEmail();
|
||||
}}
|
||||
>
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
<Seperator />
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={sendRecoveryEmail}
|
||||
type="accent"
|
||||
title={loading ? null : strings.next()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ActionSheet>
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={sendRecoveryEmail}
|
||||
type="accent"
|
||||
title={loading ? null : strings.next()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,10 +22,9 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import { RouteProp, 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, ToastManager } from "../../services/event-manager";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -38,8 +37,9 @@ import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
@@ -64,14 +64,13 @@ export const Login = ({
|
||||
const {
|
||||
step,
|
||||
setStep,
|
||||
password,
|
||||
email,
|
||||
emailInputRef,
|
||||
passwordInputRef,
|
||||
loading,
|
||||
setLoading,
|
||||
setError,
|
||||
login
|
||||
login,
|
||||
error,
|
||||
formRef
|
||||
} = useLogin(async () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
await sleep(500);
|
||||
@@ -94,6 +93,11 @@ export const Login = ({
|
||||
});
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
const onContinue = () => {
|
||||
login();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
setStep(LoginSteps.emailAuth);
|
||||
@@ -110,7 +114,6 @@ export const Login = ({
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<ForgotPassword />
|
||||
<Dialog context="two_factor_verify" />
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
@@ -200,27 +203,27 @@ export const Login = ({
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
onContinue();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
@@ -229,11 +232,10 @@ export const Login = ({
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
@@ -244,9 +246,9 @@ export const Login = ({
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => {
|
||||
login();
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -257,13 +259,14 @@ export const Login = ({
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
message:
|
||||
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
|
||||
if (loading) return;
|
||||
presentSheet({
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
// if (loading || !email.current) return;
|
||||
// SheetManager.show("forgotpassword_sheet");
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
@@ -278,8 +281,7 @@ export const Login = ({
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
onContinue();
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
@@ -333,6 +335,25 @@ export const Login = ({
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -43,29 +43,26 @@ import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getObfuscatedEmail } from "../../utils/functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
|
||||
export const SessionExpired = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const { step, password, email, passwordInputRef, loading, login } = useLogin(
|
||||
() => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
const { step, passwordInputRef, loading, login, formRef } = useLogin(() => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, true);
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
@@ -92,19 +89,19 @@ export const SessionExpired = () => {
|
||||
|
||||
const open = React.useCallback(async () => {
|
||||
try {
|
||||
let res = await db.tokenManager.getToken();
|
||||
const res = await db.tokenManager.getToken();
|
||||
if (!res) throw new Error("no token found");
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
|
||||
const key = await db.user.getEncryptionKey();
|
||||
const key = await db.user.getDataEncryptionKeys();
|
||||
if (!key) throw new Error("No encryption key found.");
|
||||
|
||||
Sync.run("global", false, "full", async (complete) => {
|
||||
if (!complete) {
|
||||
let user = await db.user.getUser();
|
||||
const user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
formRef.current.setValue("email", user.email);
|
||||
setVisible(true);
|
||||
setFocused(false);
|
||||
return;
|
||||
@@ -115,16 +112,16 @@ export const SessionExpired = () => {
|
||||
setVisible(false);
|
||||
});
|
||||
} catch (e) {
|
||||
let user = await db.user.getUser();
|
||||
const user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
formRef.current.setValue("email", user.email);
|
||||
setFocused(false);
|
||||
setVisible(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}
|
||||
}, [email]);
|
||||
}, [formRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = eSubscribeEvent(eLoginSessionExpired, open);
|
||||
@@ -155,6 +152,7 @@ export const SessionExpired = () => {
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
>
|
||||
<Dialog context="two_factor_verify" />
|
||||
<View
|
||||
style={{
|
||||
width: focused ? "100%" : "99.9%",
|
||||
@@ -192,17 +190,17 @@ export const SessionExpired = () => {
|
||||
}}
|
||||
>
|
||||
{strings.sessionExpiredDesc(
|
||||
getObfuscatedEmail(email.current as string)
|
||||
getObfuscatedEmail(formRef.current.getValue("email") as string)
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{step === LoginSteps.passwordAuth ? (
|
||||
<Input
|
||||
<FormInput
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
formRef={formRef}
|
||||
name="password"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
@@ -210,7 +208,7 @@ export const SessionExpired = () => {
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
onSubmitEditing={() => {
|
||||
login();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
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 { useUserStore } from "../../stores/use-user-store";
|
||||
@@ -39,13 +38,14 @@ import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Loading } from "../loading";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
@@ -62,13 +62,17 @@ export const Signup = ({
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef<string>(undefined);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
})
|
||||
);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const password = useRef<string>(undefined);
|
||||
const confirmPasswordInputRef = useRef<TextInput>(null);
|
||||
const confirmPassword = useRef<string>(undefined);
|
||||
const [error, setError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
@@ -76,30 +80,18 @@ export const Signup = ({
|
||||
const isTablet = width > 600;
|
||||
const route = useRoute<RouteProp<RouteParams, "Auth">>();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
setErrorMessage(undefined);
|
||||
if (!formRef.current.validate()) return;
|
||||
if (loading) return;
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(email.current!.toLowerCase(), password.current!);
|
||||
let user = await db.user.getUser();
|
||||
await db.user.signup(values.email.toLowerCase(), values.password);
|
||||
const user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
clearMessage();
|
||||
@@ -115,12 +107,14 @@ export const Signup = ({
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.signupFailed(),
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (
|
||||
(e as Error).message === "Unable to create an account on this email."
|
||||
) {
|
||||
formRef.current.setError("email", (e as Error).message);
|
||||
} else {
|
||||
setErrorMessage((e as Error).message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -215,60 +209,55 @@ export const Signup = ({
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
loading={loading}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
keyboardType="email-address"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
blurOnSubmit={false}
|
||||
onSubmit={() => {
|
||||
if (!email.current) return;
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
defaultValue={password.current}
|
||||
loading={loading}
|
||||
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;
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="confirmPassword"
|
||||
formRef={formRef}
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
defaultValue={confirmPassword.current}
|
||||
loading={loading}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
@@ -276,17 +265,22 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current!}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={() => {
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField(
|
||||
"password",
|
||||
strings.passwordNotMatched()
|
||||
)
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
signup();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
@@ -320,6 +314,25 @@ export const Signup = ({
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
|
||||
{errorMessage ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{errorMessage}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View
|
||||
|
||||
@@ -27,14 +27,15 @@ import useTimer from "../../hooks/use-timer";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
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";
|
||||
|
||||
type MFAInfo = {
|
||||
primaryMethod: string;
|
||||
@@ -52,7 +53,8 @@ const TwoFactorVerification = ({
|
||||
method: string;
|
||||
code: string;
|
||||
},
|
||||
callback: (result: any) => void
|
||||
callback: (result: any) => void,
|
||||
onerror: (e: Error) => void
|
||||
) => Promise<void>;
|
||||
mfaInfo: MFAInfo;
|
||||
onCancel: () => void;
|
||||
@@ -66,15 +68,26 @@ const TwoFactorVerification = ({
|
||||
method: mfaInfo?.primaryMethod,
|
||||
isPrimary: true
|
||||
});
|
||||
const { seconds, start, reset } = useTimer(currentMethod.method!);
|
||||
const { seconds, start, reset, secondsRef } = useTimer(currentMethod.method!);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6 || !currentMethod.method)
|
||||
if (!code.current || code.current.length < 6) {
|
||||
setError(
|
||||
new Error("Please provide a valid multi-factor authentication code.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentMethod.method) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
inputRef.current?.blur();
|
||||
await onMfaLogin(
|
||||
{
|
||||
@@ -86,6 +99,9 @@ const TwoFactorVerification = ({
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
(e) => {
|
||||
setError(e);
|
||||
}
|
||||
);
|
||||
setLoading(false);
|
||||
@@ -131,7 +147,7 @@ const TwoFactorVerification = ({
|
||||
};
|
||||
|
||||
const onSendCode = useCallback(async () => {
|
||||
if (seconds || sending) return;
|
||||
if (secondsRef.current || sending) return;
|
||||
setSending(true);
|
||||
try {
|
||||
await db.mfa.sendCode(currentMethod.method as "sms" | "email");
|
||||
@@ -139,15 +155,18 @@ const TwoFactorVerification = ({
|
||||
setSending(false);
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
ToastManager.error(e as Error, "Error sending 2FA Code", "local");
|
||||
setError(
|
||||
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
|
||||
);
|
||||
}
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
}, [currentMethod.method, secondsRef, sending, start]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentMethod.method === "sms" || currentMethod.method === "email") {
|
||||
onSendCode();
|
||||
}
|
||||
}, [currentMethod.method, onSendCode]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentMethod.method]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -234,6 +253,7 @@ const TwoFactorVerification = ({
|
||||
fwdRef={inputRef}
|
||||
textAlign="center"
|
||||
onChangeText={(value) => {
|
||||
setError(undefined);
|
||||
code.current = value;
|
||||
}}
|
||||
cursorColor={colors.selected.accent}
|
||||
@@ -241,6 +261,7 @@ const TwoFactorVerification = ({
|
||||
selectionColor={colors.selected.accent}
|
||||
onSubmitEditing={onNext}
|
||||
height={60}
|
||||
marginBottom={0}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
textAlign: "center",
|
||||
@@ -254,10 +275,26 @@ const TwoFactorVerification = ({
|
||||
containerStyle={{
|
||||
minWidth: "50%"
|
||||
}}
|
||||
wrapperStyle={{
|
||||
height: 60
|
||||
}}
|
||||
/>
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
maxWidth: 250
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error?.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
title={loading ? null : strings.next()}
|
||||
@@ -338,7 +375,8 @@ TwoFactorVerification.present = (
|
||||
method: string;
|
||||
code: string;
|
||||
},
|
||||
callback: (result: any) => void
|
||||
callback: (result: any) => void,
|
||||
onerror: (e: Error) => void
|
||||
) => Promise<void>,
|
||||
data: MFAInfo,
|
||||
onCancel: () => void,
|
||||
|
||||
@@ -28,6 +28,7 @@ import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { createFormRef } from "../ui/input/form-input";
|
||||
|
||||
export const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -39,49 +40,37 @@ export const useLogin = (
|
||||
onFinishLogin?: () => void,
|
||||
sessionExpired = false
|
||||
) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const email = useRef<string>(undefined);
|
||||
const password = useRef<string>(undefined);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
|
||||
const validateInfo = () => {
|
||||
if (
|
||||
(!password.current && step === LoginSteps.passwordAuth) ||
|
||||
(!email.current && step === LoginSteps.emailAuth)
|
||||
) {
|
||||
ToastManager.show({
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
password: ""
|
||||
})
|
||||
);
|
||||
|
||||
const login = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
try {
|
||||
if (loading) return;
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
if (!email.current) {
|
||||
if (formRef.current.validateField("email")) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
const mfaInfo = await db.user.authenticateEmail(
|
||||
formRef.current.getValue("email")
|
||||
);
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa: any, callback: (success: boolean) => void) => {
|
||||
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
@@ -103,6 +92,9 @@ export const useLogin = (
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
ToastManager.error(new Error("Token expired, try logging in again"));
|
||||
} else {
|
||||
onerror(e as Error);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -119,13 +111,14 @@ export const useLogin = (
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
if (!email.current || !password.current) {
|
||||
if (!formRef.current.validate()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const values = formRef.current.getValues();
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
values.email,
|
||||
values.password,
|
||||
undefined,
|
||||
sessionExpired
|
||||
);
|
||||
@@ -142,12 +135,11 @@ export const useLogin = (
|
||||
const finishWithError = async (e: Error) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.loginFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (e.message === "Password is incorrect.") {
|
||||
formRef.current.setError("password", e.message);
|
||||
} else {
|
||||
setError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const finishLogin = async () => {
|
||||
@@ -174,13 +166,12 @@ export const useLogin = (
|
||||
login,
|
||||
step,
|
||||
setStep,
|
||||
email,
|
||||
password,
|
||||
passwordInputRef,
|
||||
emailInputRef,
|
||||
loading,
|
||||
setLoading,
|
||||
error,
|
||||
setError
|
||||
setError,
|
||||
formRef
|
||||
};
|
||||
};
|
||||
|
||||
@@ -161,10 +161,10 @@ const FloatingButton = ({
|
||||
icon
|
||||
? icon
|
||||
: route.name === "Notebooks"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
}
|
||||
color={color || colors.primary.accent}
|
||||
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import { useRef } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
ColorValue,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
@@ -137,7 +136,7 @@ const BaseDialog = ({
|
||||
? background
|
||||
: transparent
|
||||
? "transparent"
|
||||
: "rgba(0,0,0,0.3)"
|
||||
: "rgba(0,0,0,0.1)"
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Text, View, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button, ButtonProps } from "../ui/button";
|
||||
import { PressableProps } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
@@ -17,10 +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 { KeyboardTypeOptions } from "react-native";
|
||||
import { KeyboardTypeOptions, TextInput } from "react-native";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
|
||||
import { ButtonProps } from "../ui/button";
|
||||
import { FieldValidator, FormRef } from "../ui/input/form-input";
|
||||
import { RefObject } from "react";
|
||||
|
||||
export type DialogInfo = {
|
||||
title?: string;
|
||||
@@ -43,6 +45,18 @@ export type DialogInfo = {
|
||||
| "errorShade";
|
||||
icon?: string;
|
||||
paragraphColor: string;
|
||||
form?: {
|
||||
formRef: FormRef;
|
||||
items: {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
label?: string;
|
||||
validators: FieldValidator[];
|
||||
defaultValue?: string;
|
||||
ref: RefObject<TextInput | null>;
|
||||
}[];
|
||||
onFormSubmit?: (form: FormRef) => Promise<boolean>;
|
||||
};
|
||||
input: boolean;
|
||||
inputPlaceholder: string;
|
||||
defaultValue: string;
|
||||
|
||||
@@ -18,7 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
RefObject
|
||||
} from "react";
|
||||
import { TextInput, View, ViewStyle } from "react-native";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
@@ -34,6 +40,7 @@ import { sleep } from "../../utils/time";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { FormInput, type FormRef } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import BaseDialog from "./base-dialog";
|
||||
@@ -53,9 +60,34 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
});
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [dialogInfo, setDialogInfo] = useState<DialogInfo>();
|
||||
const formRef = useRef(dialogInfo?.form?.formRef);
|
||||
formRef.current = dialogInfo?.form?.formRef;
|
||||
|
||||
const onPressPositive = async () => {
|
||||
if (dialogInfo?.positivePress) {
|
||||
// Handle form submission if form is available
|
||||
if (dialogInfo?.form && formRef.current) {
|
||||
inputRef.current?.blur();
|
||||
setLoading(true);
|
||||
try {
|
||||
const isValid = await formRef.current.validate();
|
||||
if (!isValid) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogInfo.form.onFormSubmit) {
|
||||
const result = await dialogInfo.form.onFormSubmit(formRef.current);
|
||||
if (result === false) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/** Empty */
|
||||
}
|
||||
setLoading(false);
|
||||
} else if (dialogInfo?.positivePress) {
|
||||
// Handle old input-based submission
|
||||
inputRef.current?.blur();
|
||||
setLoading(true);
|
||||
let result = false;
|
||||
@@ -76,6 +108,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
formRef.current = undefined;
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -85,6 +118,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(data.check?.defaultValue);
|
||||
formRef.current = data?.form?.formRef;
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -94,6 +128,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
const hide = React.useCallback(() => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
formRef.current = undefined;
|
||||
setVisible(false);
|
||||
setDialogInfo(undefined);
|
||||
dialogInfo?.onClose?.();
|
||||
@@ -134,19 +169,30 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
? false
|
||||
: dialogInfo.statusBarTranslucent
|
||||
}
|
||||
bounce={!dialogInfo.input}
|
||||
bounce={!dialogInfo.input && !dialogInfo.form}
|
||||
closeOnTouch={!dialogInfo.disableBackdropClosing}
|
||||
background={dialogInfo.background}
|
||||
transparent={
|
||||
dialogInfo.transparent === undefined ? true : dialogInfo.transparent
|
||||
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
|
||||
}
|
||||
onShow={async () => {
|
||||
if (dialogInfo.input) {
|
||||
if (dialogInfo.input && !dialogInfo.form) {
|
||||
inputRef.current?.setNativeProps({
|
||||
text: dialogInfo.defaultValue
|
||||
});
|
||||
await sleep(300);
|
||||
inputRef.current?.focus();
|
||||
} else if (dialogInfo.form) {
|
||||
const items = dialogInfo.form?.items;
|
||||
const firstItem = items[0];
|
||||
for (const item of items) {
|
||||
if (item.defaultValue) {
|
||||
item.ref.current?.setNativeProps({
|
||||
text: dialogInfo.defaultValue
|
||||
});
|
||||
}
|
||||
}
|
||||
firstItem?.ref?.current?.focus();
|
||||
}
|
||||
}}
|
||||
visible={true}
|
||||
@@ -170,7 +216,36 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
{dialogInfo.input ? (
|
||||
{dialogInfo.form ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP / 2
|
||||
}}
|
||||
>
|
||||
{dialogInfo.form.items.map((item, index) => (
|
||||
<FormInput
|
||||
key={item.name}
|
||||
fwdRef={item.ref}
|
||||
name={item.name}
|
||||
autoFocus={index === 0}
|
||||
placeholder={item.placeholder}
|
||||
formRef={formRef as RefObject<FormRef>}
|
||||
validators={item.validators}
|
||||
defaultValue={item.defaultValue}
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
onSubmitEditing={() => {
|
||||
const nextItem = dialogInfo?.form?.items?.[index + 1];
|
||||
if (nextItem) {
|
||||
nextItem?.ref.current?.focus();
|
||||
} else {
|
||||
onPressPositive();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : dialogInfo.input ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
@@ -184,7 +259,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
//defaultValue={dialogInfo.defaultValue}
|
||||
defaultValue={dialogInfo.defaultValue}
|
||||
onSubmit={() => {
|
||||
onPressPositive();
|
||||
}}
|
||||
@@ -237,7 +312,10 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
onPressPositive={
|
||||
(dialogInfo.positivePress || dialogInfo.form?.onFormSubmit) &&
|
||||
onPressPositive
|
||||
}
|
||||
loading={loading}
|
||||
positiveTitle={dialogInfo.positiveText}
|
||||
negativeTitle={dialogInfo.negativeText}
|
||||
|
||||
@@ -366,7 +366,9 @@ export const AppLockPassword = () => {
|
||||
SettingsService.getProperty("biometricsAuthEnabled") === false
|
||||
) {
|
||||
SettingsService.setProperty("appLockEnabled", false);
|
||||
SettingsService.setPrivacyScreen(SettingsService.get());
|
||||
SettingsService.setPrivacyScreen(
|
||||
SettingsService.getProperty("privacyScreen")
|
||||
);
|
||||
ToastManager.show({
|
||||
message: strings.applockDisabled(),
|
||||
type: "success"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,6 @@ import React, {
|
||||
RefObject,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
@@ -39,8 +38,6 @@ import Animated, {
|
||||
WithSpringConfig,
|
||||
withTiming
|
||||
} from "react-native-reanimated";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { getAppState } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
@@ -84,18 +81,9 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
}: TabProps,
|
||||
ref
|
||||
) {
|
||||
const appState = useMemo(() => getAppState(), []);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const fullscreen = useSettingStore((state) => state.fullscreen);
|
||||
const translateX = useSharedValue(
|
||||
widths
|
||||
? appState &&
|
||||
appState?.movedAway === false &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
? widths.sidebar + widths.list
|
||||
: widths.sidebar
|
||||
: 0
|
||||
);
|
||||
const translateX = useSharedValue(widths ? widths.sidebar : 0);
|
||||
const startX = useSharedValue(0);
|
||||
const currentTab = useSharedValue(1);
|
||||
const previousTab = useSharedValue(1);
|
||||
@@ -125,10 +113,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
translateX.value = 0;
|
||||
} else {
|
||||
if (prevWidths.current?.sidebar !== widths.sidebar) {
|
||||
translateX.value =
|
||||
appState && appState?.movedAway === false
|
||||
? editorPosition
|
||||
: widths.sidebar;
|
||||
translateX.value = widths.sidebar;
|
||||
if (translateX.value === editorPosition) {
|
||||
onChangeTab?.({ i: 2, from: 1 });
|
||||
}
|
||||
@@ -136,15 +121,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
}
|
||||
isLoaded.current = true;
|
||||
prevWidths.current = widths;
|
||||
}, [
|
||||
deviceMode,
|
||||
widths,
|
||||
fullscreen,
|
||||
translateX,
|
||||
editorPosition,
|
||||
appState,
|
||||
onChangeTab
|
||||
]);
|
||||
}, [deviceMode, widths, fullscreen, translateX, editorPosition, onChangeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
|
||||
|
||||
@@ -17,7 +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 { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
|
||||
import {
|
||||
GroupHeader,
|
||||
GroupingKey,
|
||||
GroupOptions,
|
||||
ItemType
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
@@ -40,7 +45,9 @@ type SectionHeaderProps = {
|
||||
color?: string;
|
||||
screen?: RouteName;
|
||||
groupOptions: GroupOptions;
|
||||
group: GroupingKey;
|
||||
onOpenJumpToDialog: () => void;
|
||||
itemCount?: number;
|
||||
};
|
||||
|
||||
export const SectionHeader = React.memo<
|
||||
@@ -53,11 +60,13 @@ export const SectionHeader = React.memo<
|
||||
color,
|
||||
screen,
|
||||
groupOptions,
|
||||
onOpenJumpToDialog
|
||||
group,
|
||||
onOpenJumpToDialog,
|
||||
itemCount
|
||||
}: SectionHeaderProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const isCompactModeEnabled = useIsCompactModeEnabled(
|
||||
dataType as "note" | "notebook"
|
||||
dataType as "note" | "notebook" | "searchResult"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -104,7 +113,9 @@ export const SectionHeader = React.memo<
|
||||
color={color || colors.primary.accent}
|
||||
>
|
||||
{!item.title || item.title === ""
|
||||
? strings.pinned().toUpperCase()
|
||||
? screen === "Search"
|
||||
? strings.results(itemCount || 0)
|
||||
: strings.pinned().toUpperCase()
|
||||
: item.title.toUpperCase()}
|
||||
</Heading>
|
||||
</Pressable>
|
||||
@@ -133,6 +144,7 @@ export const SectionHeader = React.memo<
|
||||
<Sort
|
||||
screen={screen}
|
||||
type={dataType}
|
||||
group={group}
|
||||
hideGroupOptions={
|
||||
screen === "Reminders" || screen === "Search"
|
||||
}
|
||||
@@ -150,7 +162,8 @@ export const SectionHeader = React.memo<
|
||||
hidden={
|
||||
dataType !== "note" &&
|
||||
dataType !== "notebook" &&
|
||||
screen !== "Notes"
|
||||
screen !== "Notes" &&
|
||||
screen !== "Search"
|
||||
}
|
||||
style={{
|
||||
width: 25,
|
||||
@@ -163,9 +176,11 @@ export const SectionHeader = React.memo<
|
||||
}
|
||||
onPress={() => {
|
||||
SettingsService.set({
|
||||
[dataType !== "notebook"
|
||||
? "notesListMode"
|
||||
: "notebooksListMode"]: !isCompactModeEnabled
|
||||
[dataType === "notebook"
|
||||
? "notebooksListMode"
|
||||
: dataType === "searchResult"
|
||||
? "searchListMode"
|
||||
: "notesListMode"]: !isCompactModeEnabled
|
||||
? "compact"
|
||||
: "normal"
|
||||
});
|
||||
@@ -191,6 +206,7 @@ export const SectionHeader = React.memo<
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.item.title !== next.item.title) return false;
|
||||
if (prev.itemCount !== next.itemCount) return false;
|
||||
if (prev.groupOptions?.groupBy !== next.groupOptions.groupBy) return false;
|
||||
if (prev.groupOptions?.sortDirection !== next.groupOptions.sortDirection)
|
||||
return false;
|
||||
|
||||
@@ -252,14 +252,16 @@ const NoteItem = ({
|
||||
{reminder ? (
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
disabled
|
||||
color={color?.colorCode}
|
||||
textStyle={{
|
||||
fontSize: AppFontSize.xxxs
|
||||
fontSize: AppFontSize.xxs
|
||||
}}
|
||||
iconSize={AppFontSize.xxxs}
|
||||
short
|
||||
iconSize={AppFontSize.xxs}
|
||||
style={{
|
||||
height: "auto"
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -32,9 +32,11 @@ import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
|
||||
import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import { RouteParams } from "../../../stores/use-navigation-store";
|
||||
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,
|
||||
@@ -63,11 +65,19 @@ export const openNote = async (
|
||||
component: <NotePreview note={item} content={content} />
|
||||
});
|
||||
} else {
|
||||
if (!useTabStore.getState().hasTabForNote(note.id!)) {
|
||||
editorController.current.commands.setLoading(
|
||||
true,
|
||||
useTabStore.getState().currentTab
|
||||
);
|
||||
}
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
setTimeout(() => {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
}, 32);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eOnLoadNote } from "../../../utils/events";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
type SearchResultProps = {
|
||||
item: HighlightedResult;
|
||||
};
|
||||
@@ -38,6 +39,9 @@ type SearchResultProps = {
|
||||
export const SearchResult = (props: SearchResultProps) => {
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useSettingStore(
|
||||
(state) => state.settings.searchListMode === "compact"
|
||||
);
|
||||
|
||||
const openNote = async (index?: number) => {
|
||||
const note = await db.notes.note(props.item.id);
|
||||
@@ -87,7 +91,7 @@ export const SearchResult = (props: SearchResultProps) => {
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{props.item.content?.length ? (
|
||||
{props.item.content?.length && !compactMode ? (
|
||||
<IconButton
|
||||
name={!expanded ? "chevron-right" : "chevron-down"}
|
||||
onPress={() => setExpanded((prev) => !prev)}
|
||||
@@ -130,6 +134,7 @@ export const SearchResult = (props: SearchResultProps) => {
|
||||
</View>
|
||||
|
||||
{expanded &&
|
||||
!compactMode &&
|
||||
props.item.content.map((content, index) => (
|
||||
<Pressable
|
||||
key={props.item.id + index}
|
||||
|
||||
@@ -53,6 +53,7 @@ type ListProps = {
|
||||
isRenderedInActionSheet?: boolean;
|
||||
CustomListComponent?: React.JSX.ElementType;
|
||||
placeholder?: PlaceholderData;
|
||||
groupType: GroupingKey;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
@@ -73,16 +74,7 @@ export default function List(props: ListProps) {
|
||||
props.dataType === "notebook" ||
|
||||
notebooksListMode === "compact";
|
||||
|
||||
const groupType =
|
||||
props.renderedInRoute === "Notes"
|
||||
? "home"
|
||||
: props.renderedInRoute === "Favorites"
|
||||
? "favorites"
|
||||
: props.renderedInRoute === "Trash" || props.dataType === "trash"
|
||||
? "trash"
|
||||
: `${props.dataType}s`;
|
||||
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
const groupOptions = useGroupOptions(props.groupType);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, "full", () => {
|
||||
@@ -94,7 +86,7 @@ export default function List(props: ListProps) {
|
||||
(item: number | boolean, index: number) => {
|
||||
return props.data?.type(index);
|
||||
},
|
||||
[]
|
||||
[props.data]
|
||||
);
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
@@ -105,7 +97,7 @@ export default function List(props: ListProps) {
|
||||
isSheet={props.isRenderedInActionSheet || false}
|
||||
items={props.data}
|
||||
groupOptions={groupOptions}
|
||||
group={groupType as GroupingKey}
|
||||
group={props.groupType as GroupingKey}
|
||||
renderedInRoute={props.renderedInRoute}
|
||||
customAccentColor={props.customAccentColor}
|
||||
dataType={props.dataType}
|
||||
@@ -115,7 +107,7 @@ export default function List(props: ListProps) {
|
||||
},
|
||||
[
|
||||
groupOptions,
|
||||
groupType,
|
||||
props.groupType,
|
||||
props.customAccentColor,
|
||||
props.data,
|
||||
props.dataType,
|
||||
|
||||
@@ -53,7 +53,7 @@ import TagItem from "../list-items/tag";
|
||||
import { SearchResult } from "../list-items/search-result";
|
||||
|
||||
type ListItemWrapperProps<TItem = Item> = {
|
||||
group?: GroupingKey;
|
||||
group: GroupingKey;
|
||||
items: VirtualizedGrouping<TItem> | undefined;
|
||||
isSheet: boolean;
|
||||
index: number;
|
||||
@@ -182,6 +182,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
item={groupHeader}
|
||||
index={index}
|
||||
dataType={item.type}
|
||||
group={group}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
@@ -218,6 +219,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
item={groupHeader}
|
||||
index={index}
|
||||
dataType={item.type}
|
||||
group={group}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
@@ -245,6 +247,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
screen={props.renderedInRoute}
|
||||
item={groupHeader}
|
||||
index={index}
|
||||
group={group}
|
||||
dataType={item.type}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
@@ -271,6 +274,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
screen={props.renderedInRoute}
|
||||
item={groupHeader}
|
||||
index={index}
|
||||
group={group}
|
||||
dataType={item.type}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
@@ -297,9 +301,11 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
screen={props.renderedInRoute}
|
||||
item={groupHeader}
|
||||
index={index}
|
||||
group={group}
|
||||
dataType={item.type}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
itemCount={items?.placeholders.length}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: props.scrollRef,
|
||||
|
||||
@@ -30,7 +30,7 @@ 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 { useIsFeatureAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../sheets/paywall";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -142,7 +142,7 @@ function ReorderableList<T extends { id: string }>({
|
||||
|
||||
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
|
||||
return items;
|
||||
}, [data, customizableSidebarFeature?.isAllowed]);
|
||||
}, [customizableSidebarFeature?.isAllowed, data, itemOrderState]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import {
|
||||
EncryptedContentItem,
|
||||
isEncryptedContent,
|
||||
Note,
|
||||
UnencryptedContentItem
|
||||
} from "@notesnook/core";
|
||||
@@ -41,7 +40,8 @@ import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
openVault
|
||||
openVault,
|
||||
VaultRequestType
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Sync from "../../services/sync";
|
||||
@@ -59,6 +59,7 @@ import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Cipher } from "@notesnook/crypto";
|
||||
|
||||
const MergeConflicts = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -73,9 +74,11 @@ const MergeConflicts = () => {
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
|
||||
const applyChanges = async () => {
|
||||
let contentToSave = selectedContent;
|
||||
const contentToSave = selectedContent;
|
||||
if (!contentToSave) return;
|
||||
let note = await db.notes.note(contentToSave.noteId);
|
||||
const note = await db.notes.note(
|
||||
(content.current as UnencryptedContentItem).noteId
|
||||
);
|
||||
if (!note) return;
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
@@ -83,12 +86,21 @@ const MergeConflicts = () => {
|
||||
dateEdited: contentToSave.dateEdited
|
||||
});
|
||||
|
||||
const noteLocked = await db.vaults.itemExists(note);
|
||||
const noteContent = await db.content.findByNoteId(note.id);
|
||||
|
||||
if (noteLocked) {
|
||||
await db.vault.save({
|
||||
...contentToSave,
|
||||
sessionId: `${Date.now()}`
|
||||
if (noteContent?.locked) {
|
||||
const selectedContent = contentToSave.conflicted
|
||||
? noteContent
|
||||
: noteContent.conflicted;
|
||||
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
dateResolved: noteContent?.conflicted?.dateModified || Date.now(),
|
||||
sessionId: `${Date.now()}`,
|
||||
data: selectedContent?.data as Cipher<"base64">,
|
||||
type: selectedContent?.type,
|
||||
locked: true,
|
||||
conflicted: undefined
|
||||
});
|
||||
} else {
|
||||
await db.content.add({
|
||||
@@ -128,14 +140,15 @@ const MergeConflicts = () => {
|
||||
let noteContent: UnencryptedContentItem;
|
||||
if (isLocked) {
|
||||
openVault({
|
||||
requestType: VaultRequestType.CustomAction,
|
||||
item: item,
|
||||
novault: true,
|
||||
customActionTitle: "Unlock note",
|
||||
customActionParagraph: "Unlock note to merge conflicts",
|
||||
title: strings.unlockNote(),
|
||||
customActionTitle: strings.unlockNote(),
|
||||
customActionParagraph: strings.unlockNoteToMergeConflicts(),
|
||||
buttonTitle: strings.unlock(),
|
||||
onUnlock: async (item, password) => {
|
||||
if (!item || !password) return;
|
||||
const currentContent = await db.content.get(item.contentId!);
|
||||
|
||||
try {
|
||||
noteContent = {
|
||||
...(await db.content.get(item.contentId!)),
|
||||
@@ -450,7 +463,8 @@ const MergeConflicts = () => {
|
||||
<ReadonlyEditor
|
||||
editorId="conflictSecondary"
|
||||
onLoad={async (loadContent) => {
|
||||
const note = await db.notes.note(content.current?.noteId!);
|
||||
if (!content.current?.noteId) return;
|
||||
const note = await db.notes.note(content.current?.noteId);
|
||||
if (!note) return;
|
||||
loadContent({
|
||||
id: note.id,
|
||||
|
||||
@@ -63,22 +63,25 @@ const HistoryItem = ({
|
||||
}${_end_time}`;
|
||||
};
|
||||
|
||||
const preview = useCallback(async (item: HistorySession) => {
|
||||
const content = await db.noteHistory.content(item.id);
|
||||
presentSheet({
|
||||
component: (
|
||||
<NotePreview
|
||||
session={{
|
||||
...item,
|
||||
session: getDate(item.dateCreated, item.dateModified)
|
||||
}}
|
||||
content={content}
|
||||
note={note}
|
||||
/>
|
||||
),
|
||||
context: "note_history"
|
||||
});
|
||||
}, []);
|
||||
const preview = useCallback(
|
||||
async (item: HistorySession) => {
|
||||
const content = await db.noteHistory.content(item.id);
|
||||
presentSheet({
|
||||
component: (
|
||||
<NotePreview
|
||||
session={{
|
||||
...item,
|
||||
session: getDate(item.dateCreated, item.dateModified)
|
||||
}}
|
||||
content={content}
|
||||
note={note}
|
||||
/>
|
||||
),
|
||||
context: "note_history"
|
||||
});
|
||||
},
|
||||
[note]
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -138,7 +141,7 @@ export default function NoteHistory({
|
||||
({ index }: { index: number }) => (
|
||||
<HistoryItem index={index} items={history} note={note} />
|
||||
),
|
||||
[history]
|
||||
[history, note]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
isEncryptedContent,
|
||||
Note,
|
||||
NoteContent,
|
||||
SessionContentItem,
|
||||
TrashOrItem
|
||||
} from "@notesnook/core";
|
||||
|
||||
|
||||
@@ -18,14 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getFeaturesTable } from "@notesnook/common";
|
||||
import {
|
||||
EV,
|
||||
EVENTS,
|
||||
Plan,
|
||||
SKUResponse,
|
||||
SubscriptionPlan,
|
||||
User
|
||||
} from "@notesnook/core";
|
||||
import { EVENTS, Plan, SubscriptionPlan, User } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
@@ -55,6 +48,7 @@ import {
|
||||
TECHLORE_SVG,
|
||||
XDA_SVG
|
||||
} from "../../assets/images/assets";
|
||||
import { db } from "../../common/database";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import usePricingPlans, {
|
||||
PlanOverView,
|
||||
@@ -118,7 +112,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
}
|
||||
setStep(Steps.buy);
|
||||
}
|
||||
}, [routeParams.state]);
|
||||
}, [pricingPlans, routeParams.state]);
|
||||
|
||||
useEffect(() => {
|
||||
let listener: NativeEventSubscription;
|
||||
@@ -136,15 +130,15 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
return () => {
|
||||
listener?.remove();
|
||||
};
|
||||
}, [isFocused, step]);
|
||||
}, [isFocused, routeParams.context, step]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = EV.subscribe(
|
||||
const sub = db.eventManager.subscribe(
|
||||
EVENTS.userSubscriptionUpdated,
|
||||
(sub: User["subscription"]) => {
|
||||
if (sub.plan === SubscriptionPlan.FREE) return;
|
||||
if (routeParams.context === "signup") {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
@@ -153,7 +147,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
return () => {
|
||||
sub?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [routeParams.context]);
|
||||
|
||||
const is5YearPlanSelected = (
|
||||
isGithubRelease
|
||||
@@ -182,8 +176,9 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
>
|
||||
<IconButton
|
||||
name="close"
|
||||
color={colors.primary.icon}
|
||||
onPress={() => {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -196,7 +191,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
|
||||
return;
|
||||
}
|
||||
if (routeParams.context === "signup") {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
@@ -654,7 +649,7 @@ After trying all the privacy security oriented note taking apps, for the price a
|
||||
type="accent"
|
||||
onPress={() => {
|
||||
if (routeParams.context === "signup") {
|
||||
Navigation.replace("FluidPanelsView", {});
|
||||
Navigation.navigate("FluidPanelsView", {});
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
@@ -934,7 +929,10 @@ const PricingPlanCard = ({
|
||||
setStep: (step: number) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
|
||||
const regionalDiscount =
|
||||
annualBilling && plan.id === "pro"
|
||||
? pricingPlans?.regionalDiscount
|
||||
: undefined;
|
||||
const { width } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
@@ -957,26 +955,6 @@ const PricingPlanCard = ({
|
||||
annualBilling
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pricingPlans?.isGithubRelease || !annualBilling) return;
|
||||
pricingPlans
|
||||
?.getRegionalDiscount(
|
||||
plan.id,
|
||||
pricingPlans.isGithubRelease
|
||||
? (WebPlan?.period as string)
|
||||
: `notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
|
||||
)
|
||||
.then((value) => {
|
||||
setRegionaDiscount(value);
|
||||
});
|
||||
}, [annualBilling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!annualBilling) {
|
||||
setRegionaDiscount(undefined);
|
||||
}
|
||||
}, [annualBilling]);
|
||||
|
||||
const isSubscribed =
|
||||
product?.productId &&
|
||||
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
|
||||
|
||||
@@ -27,7 +27,11 @@ import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { db } from "../../common/database";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import {
|
||||
eSendEvent,
|
||||
sendItemUpdateEvent,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useMenuStore } from "../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
@@ -63,6 +67,7 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
|
||||
useRelationStore.getState().update();
|
||||
setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
sendItemUpdateEvent(item.id, "color");
|
||||
eSendEvent(refreshNotesPage);
|
||||
};
|
||||
|
||||
@@ -127,7 +132,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
}
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
setVisible(true);
|
||||
}, []);
|
||||
}, [colorFeature]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -140,6 +145,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
useRelationStore.getState().update();
|
||||
useMenuStore.getState().setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
sendItemUpdateEvent(color.id, "color");
|
||||
eSendEvent(refreshNotesPage);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DateMeta = ({ item }: { item: Item }) => {
|
||||
const [dateCreated, setDateCreated] = useState(item.dateCreated);
|
||||
|
||||
function getDateMeta() {
|
||||
let keys = Object.keys(item);
|
||||
const keys = Object.keys(item);
|
||||
if (keys.includes("dateEdited"))
|
||||
keys.splice(
|
||||
keys.findIndex((k) => k === "dateModified"),
|
||||
|
||||
@@ -33,7 +33,6 @@ import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Dialog } from "../dialog";
|
||||
|
||||
const TOP_BAR_ITEMS: ActionId[] = [
|
||||
"pin",
|
||||
@@ -319,7 +318,9 @@ export const Items = ({
|
||||
[
|
||||
colors.error.icon,
|
||||
colors.primary.accent,
|
||||
colors.primary.border,
|
||||
colors.secondary.icon,
|
||||
colors.static.orange,
|
||||
columnItemWidth,
|
||||
topBarSorting
|
||||
]
|
||||
@@ -327,8 +328,9 @@ export const Items = ({
|
||||
|
||||
const getTopBarItemChunksOfFour = () => {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < topBarItems.length; i += 5) {
|
||||
chunks.push(topBarItems.slice(i, i + 5));
|
||||
const itemCount = shouldShrink ? 4 : 5;
|
||||
for (let i = 0; i < topBarItems.length; i += itemCount) {
|
||||
chunks.push(topBarItems.slice(i, i + itemCount));
|
||||
}
|
||||
return chunks;
|
||||
};
|
||||
@@ -375,7 +377,8 @@ export const Items = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: 5
|
||||
gap: 5,
|
||||
width: width
|
||||
}}
|
||||
>
|
||||
{item.map(renderTopBarItem)}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const TagStrip = ({ item, close }) => {
|
||||
.then((tags) => {
|
||||
setTags(tags);
|
||||
});
|
||||
}, []);
|
||||
}, [item]);
|
||||
|
||||
return tags?.length > 0 ? (
|
||||
<View
|
||||
|
||||
@@ -42,8 +42,6 @@ import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import ExportNotesSheet from "../sheets/export-notes";
|
||||
|
||||
import { MoveNotebook } from "../../screens/move-notebook";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import ManageTags from "../../screens/manage-tags";
|
||||
|
||||
@@ -16,13 +16,14 @@ 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 { Plan, SKUResponse } from "@notesnook/core";
|
||||
import { Plan } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Linking,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
import Config from "react-native-config";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { WebView } from "react-native-webview";
|
||||
import { db } from "../../../common/database";
|
||||
import usePricingPlans from "../../../hooks/use-pricing-plans";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
@@ -211,16 +211,19 @@ export const BuyPlan = (props: {
|
||||
? strings["5yearPlanConditions"]()
|
||||
: [
|
||||
strings.trialPlanConditions[0](
|
||||
billingDuration?.duration as number
|
||||
billingDuration?.duration as number as never
|
||||
),
|
||||
strings.trialPlanConditions[1](0)
|
||||
...(isGithubRelease
|
||||
? []
|
||||
: [strings.trialPlanConditions[1](Platform.OS as never)])
|
||||
]
|
||||
).map((item) => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
gap: 10,
|
||||
flex: 1
|
||||
}}
|
||||
key={item}
|
||||
>
|
||||
@@ -229,7 +232,13 @@ export const BuyPlan = (props: {
|
||||
size={AppFontSize.lg}
|
||||
name="check"
|
||||
/>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Paragraph>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -243,8 +252,8 @@ export const BuyPlan = (props: {
|
||||
is5YearPlanSelected
|
||||
? strings.purchase()
|
||||
: pricingPlans?.userCanRequestTrial
|
||||
? strings.subscribeAndStartTrial()
|
||||
: strings.subscribe()
|
||||
? strings.subscribeAndStartTrial()
|
||||
: strings.subscribe()
|
||||
}
|
||||
onPress={async () => {
|
||||
if (isGithubRelease) {
|
||||
@@ -321,7 +330,10 @@ const ProductItem = (props: {
|
||||
productId: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
|
||||
const regionalDiscount =
|
||||
props.productId === "notesnook.pro.yearly"
|
||||
? props.pricingPlans.regionalDiscount
|
||||
: undefined;
|
||||
const product =
|
||||
props.pricingPlans?.currentPlan?.subscriptions?.[
|
||||
regionalDiscount?.sku || props.productId
|
||||
@@ -356,29 +368,21 @@ const ProductItem = (props: {
|
||||
(product as RNIap.Subscription)?.productId
|
||||
) ||
|
||||
props.pricingPlans.user?.subscription?.productId ===
|
||||
(product as Plan).id);
|
||||
(product as Plan)?.id);
|
||||
|
||||
useEffect(() => {
|
||||
props.pricingPlans
|
||||
?.getRegionalDiscount(
|
||||
props.pricingPlans.currentPlan?.id as string,
|
||||
props.pricingPlans.isGithubRelease
|
||||
? ((product as Plan)?.period as string)
|
||||
: props.productId
|
||||
)
|
||||
.then((value) => {
|
||||
if (
|
||||
value &&
|
||||
value.sku?.startsWith(
|
||||
(props.pricingPlans.selectedProduct as RNIap.Subscription)
|
||||
?.productId
|
||||
)
|
||||
) {
|
||||
props.pricingPlans.selectProduct(value?.sku as string);
|
||||
}
|
||||
setRegionaDiscount(value);
|
||||
});
|
||||
}, []);
|
||||
const discountValue =
|
||||
(isAnnual && !isGithubRelease) ||
|
||||
(isGithubRelease && (product as Plan)?.discount?.amount)
|
||||
? regionalDiscount
|
||||
? regionalDiscount.discount
|
||||
: isGithubRelease
|
||||
? (product as Plan).discount?.amount
|
||||
: props.pricingPlans.compareProductPrice(
|
||||
props.pricingPlans.currentPlan?.id as string,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -420,12 +424,11 @@ const ProductItem = (props: {
|
||||
{isAnnual
|
||||
? strings.yearly()
|
||||
: is5YearProduct
|
||||
? strings.fiveYearPlan()
|
||||
: strings.monthly()}
|
||||
? strings.fiveYearPlan()
|
||||
: strings.monthly()}
|
||||
</Heading>
|
||||
|
||||
{(isAnnual && !isGithubRelease) ||
|
||||
(isGithubRelease && (product as Plan)?.discount?.amount) ? (
|
||||
{discountValue ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.red,
|
||||
@@ -436,18 +439,7 @@ const ProductItem = (props: {
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={AppFontSize.xs}>
|
||||
{strings.bestValue()} -{" "}
|
||||
{strings.percentOff(
|
||||
(regionalDiscount
|
||||
? regionalDiscount.discount
|
||||
: isGithubRelease
|
||||
? (product as Plan).discount?.amount
|
||||
: props.pricingPlans.compareProductPrice(
|
||||
props.pricingPlans.currentPlan?.id as string,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
|
||||
)) as string
|
||||
)}
|
||||
{strings.bestValue()} - {strings.percentOff(`${discountValue}`)}
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -102,9 +102,17 @@ const TabItemComponent = (props: {
|
||||
{props.tab.session?.noteLocked ? (
|
||||
<>
|
||||
{props.tab.session?.locked ? (
|
||||
<Icon size={AppFontSize.md} name="lock" />
|
||||
<Icon
|
||||
size={AppFontSize.md}
|
||||
name="lock"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
) : (
|
||||
<Icon size={AppFontSize.md} name="lock-open-outline" />
|
||||
<Icon
|
||||
size={AppFontSize.md}
|
||||
name="lock-open-outline"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -21,7 +21,6 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
@@ -17,18 +17,18 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Debug } from "@notesnook/core";
|
||||
import { Debug, IssueReportResponse } from "@notesnook/core";
|
||||
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Linking, Platform, Text, TextInput, View } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../../services/event-manager";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size/index";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
@@ -37,17 +37,26 @@ import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import Config from "react-native-config";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
export const Issue = ({
|
||||
defaultTitle,
|
||||
defaultBody,
|
||||
issueTitle
|
||||
}: {
|
||||
defaultTitle?: string;
|
||||
defaultBody?: string;
|
||||
issueTitle?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const body = useStoredRef("issueBody");
|
||||
const body = useStoredRef("issueBody", "");
|
||||
const title = useStoredRef("issueTitle", defaultTitle);
|
||||
const [done, setDone] = useState(false);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const bodyRef = useRef();
|
||||
const bodyRef = useRef<TextInput>(null);
|
||||
const initialLayout = useRef(false);
|
||||
const issueUrl = useRef();
|
||||
const issueReportResponse = useRef<IssueReportResponse>(undefined);
|
||||
|
||||
const onPress = async () => {
|
||||
if (loading) return;
|
||||
@@ -57,7 +66,7 @@ export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
issueUrl.current = await Debug.report({
|
||||
issueReportResponse.current = await Debug.report({
|
||||
title: title.current,
|
||||
body:
|
||||
body.current +
|
||||
@@ -72,7 +81,7 @@ Logged in: ${user ? "yes" : "no"}
|
||||
Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
|
||||
userId: user?.id
|
||||
});
|
||||
if (!issueUrl.current) {
|
||||
if (!issueReportResponse.current) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Failed to report issue on github",
|
||||
@@ -88,12 +97,46 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: e.message,
|
||||
heading: (e as Error).message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getResponseInfo(response?: IssueReportResponse) {
|
||||
if (!response || "error" in response) return;
|
||||
switch (response.type) {
|
||||
case "email": {
|
||||
return {
|
||||
title: strings.yourSupportRequestHasBeenForwarded(),
|
||||
message: strings.supportEmailMessage()
|
||||
};
|
||||
}
|
||||
case "discussion": {
|
||||
const url = response.url;
|
||||
return {
|
||||
title: strings.thankYouForFeedback(),
|
||||
positiveButtonText: strings.copyLink(),
|
||||
message: strings.featureRequestMessage(url),
|
||||
url: url
|
||||
};
|
||||
}
|
||||
case "issue": {
|
||||
const url = response.url;
|
||||
return {
|
||||
title: strings.thankYouForReporting(),
|
||||
positiveButtonText: strings.copyLink(),
|
||||
message: strings.bugReportMessage(url),
|
||||
url: url
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const responseInfo = getResponseInfo(issueReportResponse.current);
|
||||
|
||||
console.log(responseInfo, issueReportResponse.current);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -105,38 +148,28 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading>{strings.issueCreatedHeading()}</Heading>
|
||||
<Heading>{responseInfo?.title}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
selectable={true}
|
||||
>
|
||||
{strings.issueCreatedDesc[0]()}
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
>
|
||||
{issueUrl.current}
|
||||
</Paragraph>
|
||||
. {strings.issueCreatedDesc[1]()}
|
||||
{responseInfo?.message}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={strings.openIssue()}
|
||||
title={responseInfo?.positiveButtonText || "Done"}
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
if (responseInfo?.url) {
|
||||
Linking.openURL(responseInfo?.url);
|
||||
}
|
||||
eSendEvent(eCloseSheet);
|
||||
}}
|
||||
type="accent"
|
||||
width="100%"
|
||||
@@ -248,10 +281,7 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://discord.gg/zQBK97EE22",
|
||||
colors
|
||||
);
|
||||
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -118,8 +118,8 @@ const ListBlockItem = ({
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: !item.content || item.content.trim() === ""
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -28,7 +28,11 @@ import {
|
||||
ToastManager
|
||||
} from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import {
|
||||
eAfterSync,
|
||||
eCloseSheet,
|
||||
eMenuItemUpdate
|
||||
} from "../../../utils/events";
|
||||
import { SideMenuItem } from "../../../utils/menu-items";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
@@ -37,12 +41,25 @@ import AppIcon from "../../ui/AppIcon";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import PaywallSheet from "../paywall";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { db } from "../../../common/database";
|
||||
import { useTrashStore } from "../../../stores/use-trash-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const featuresAvailable = useAreFeaturesAvailable([
|
||||
"customHomepage",
|
||||
"customizableSidebar"
|
||||
]);
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
const trash = useTrashStore((state) => state.items);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAppLoading) {
|
||||
useTrashStore.getState().refresh();
|
||||
}
|
||||
}, [isAppLoading]);
|
||||
|
||||
return !featuresAvailable ? null : (
|
||||
<View
|
||||
style={{
|
||||
@@ -100,7 +117,38 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
|
||||
},
|
||||
icon: "sort-ascending",
|
||||
locked: !featuresAvailable?.customizableSidebar.isAllowed
|
||||
}
|
||||
},
|
||||
...(item.id === "Trash"
|
||||
? [
|
||||
{
|
||||
title: strings.clearTrash(),
|
||||
onPress: async () => {
|
||||
if (!trash || trash?.length === 0) return;
|
||||
eSendEvent(eCloseSheet);
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: strings.clearTrashConfirm(),
|
||||
paragraph: strings.clearTrashDesc(),
|
||||
positiveText: strings.clear(),
|
||||
positivePress: async () => {
|
||||
await db.trash.clear();
|
||||
useTrashStore.getState().clear();
|
||||
eSendEvent(eMenuItemUpdate);
|
||||
eSendEvent(eAfterSync);
|
||||
ToastManager.show({
|
||||
message: strings.trashCleared(),
|
||||
type: "success"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "delete-sweep-outline",
|
||||
disabled: !trash || trash?.length === 0
|
||||
}
|
||||
]
|
||||
: [])
|
||||
].map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
@@ -112,7 +160,7 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
borderRadius: 0,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
opacity: item.locked ? 0.6 : 1
|
||||
opacity: item.disabled || item.locked ? 0.6 : 1
|
||||
}}
|
||||
onPress={() => {
|
||||
item.onPress();
|
||||
|
||||
@@ -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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Notebook } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
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 { FeatureId, FeatureResult } from "@notesnook/common";
|
||||
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
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 {
|
||||
FeatureUsage,
|
||||
formatBytes,
|
||||
@@ -76,10 +95,10 @@ export function PlanLimits() {
|
||||
{item.total === Infinity
|
||||
? strings.unlimited()
|
||||
: item.id === "storage"
|
||||
? `${formatBytes(item.used)}/${formatBytes(
|
||||
item.total
|
||||
)} ${strings.used()}`
|
||||
: `${item.used}/${item.total} ${strings.used()}`}
|
||||
? `${formatBytes(item.used)}/${formatBytes(
|
||||
item.total
|
||||
)} ${strings.used()}`
|
||||
: `${item.used}/${item.total} ${strings.used()}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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 { hosts, Monograph, Note } from "@notesnook/core";
|
||||
import { hosts, Note } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
@@ -28,12 +28,15 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
//@ts-ignore
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
import { presentSheet, ToastManager } from "../../../services/event-manager";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useAttachmentStore } from "../../../stores/use-attachment-store";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
@@ -46,22 +49,17 @@ import Input from "../../ui/input";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import { eMenuItemUpdate } from "../../../utils/events";
|
||||
import { useIsFeatureAvailable } from "@notesnook/common";
|
||||
|
||||
async function fetchMonographData(noteId: string) {
|
||||
const monographId = db.monographs.monograph(noteId);
|
||||
const monograph = monographId
|
||||
? await db.monographs.get(monographId)
|
||||
: undefined;
|
||||
const analyticsFeature = await isFeatureAvailable("monographAnalytics");
|
||||
const analytics =
|
||||
monographId && analyticsFeature
|
||||
? await db.monographs.analytics(monographId)
|
||||
: undefined;
|
||||
return {
|
||||
monograph,
|
||||
monographId,
|
||||
analytics
|
||||
monographId
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +116,7 @@ const PublishNoteSheet = ({
|
||||
|
||||
await monographData.execute();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(eMenuItemUpdate);
|
||||
setPublishLoading(false);
|
||||
}
|
||||
requestInAppReview();
|
||||
@@ -144,6 +143,7 @@ const PublishNoteSheet = ({
|
||||
await db.monographs.unpublish(note.id);
|
||||
monographData.execute();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(eMenuItemUpdate);
|
||||
setPublishLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -381,9 +381,6 @@ const PublishNoteSheet = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
|
||||
<Paragraph>
|
||||
{monographData?.result?.analytics?.totalViews || 0}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -169,7 +169,7 @@ class RecoveryKeySheet extends React.Component {
|
||||
};
|
||||
|
||||
onOpen = async () => {
|
||||
let k = await db.user.getEncryptionKey();
|
||||
let k = await db.user.getMasterKey();
|
||||
this.user = await db.user.getUser();
|
||||
if (k) {
|
||||
this.setState({
|
||||
|
||||
@@ -104,8 +104,8 @@ const ListBlockItem = ({
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: !item.content || item.content.trim() === ""
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
|
||||
@@ -121,6 +121,17 @@ export const RelationsList = ({
|
||||
<List
|
||||
data={items}
|
||||
loading={false}
|
||||
groupType={
|
||||
referenceType === "note"
|
||||
? "notes"
|
||||
: referenceType === "tag"
|
||||
? "tags"
|
||||
: referenceType === "notebook"
|
||||
? "notebooks"
|
||||
: referenceType === "reminder"
|
||||
? "reminders"
|
||||
: "notes"
|
||||
}
|
||||
dataType={referenceType as any}
|
||||
isRenderedInActionSheet={true}
|
||||
/>
|
||||
|
||||
@@ -181,6 +181,7 @@ export default function ReminderNotify({
|
||||
data={references}
|
||||
loading={false}
|
||||
dataType="note"
|
||||
groupType="notes"
|
||||
isRenderedInActionSheet={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -45,25 +45,16 @@ import Paragraph from "../../ui/typography/paragraph";
|
||||
const Sort = ({
|
||||
type,
|
||||
screen,
|
||||
hideGroupOptions
|
||||
hideGroupOptions,
|
||||
group: groupType
|
||||
}: {
|
||||
type: ItemType;
|
||||
screen?: RouteName;
|
||||
group: GroupingKey;
|
||||
hideGroupOptions?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const groupType =
|
||||
screen === "Archive"
|
||||
? "archive"
|
||||
: screen === "Search"
|
||||
? "search"
|
||||
: screen === "Notes"
|
||||
? "home"
|
||||
: screen === "Trash" || type === "trash"
|
||||
? "trash"
|
||||
: ((type + "s") as GroupingKey);
|
||||
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings.getGroupOptions(groupType)
|
||||
);
|
||||
@@ -157,10 +148,15 @@ const Sort = ({
|
||||
>
|
||||
{Object.keys(SORT).map((item) => {
|
||||
const sortOptionVisibility = {
|
||||
relevance: screen === "Search",
|
||||
dueDate: screen === "Reminders",
|
||||
dateModified: screen === "Tags" || screen === "Reminders",
|
||||
dateEdited: screen !== "Tags" && screen !== "Reminders"
|
||||
dateCreated: groupType !== "trash",
|
||||
relevance: groupType === "search",
|
||||
dueDate: groupType === "reminders",
|
||||
dateModified: groupType === "reminders" || groupType === "tags",
|
||||
dateEdited:
|
||||
groupType !== "tags" &&
|
||||
groupType !== "reminders" &&
|
||||
groupType !== "trash",
|
||||
dateDeleted: groupType === "trash"
|
||||
};
|
||||
|
||||
// Check if this sort option should be skipped for the current screen
|
||||
@@ -188,10 +184,7 @@ const Sort = ({
|
||||
onPress={async () => {
|
||||
const _groupOptions: GroupOptions = {
|
||||
...groupOptions,
|
||||
sortBy:
|
||||
type === "trash"
|
||||
? "dateDeleted"
|
||||
: (item as SortOptions["sortBy"])
|
||||
sortBy: item as SortOptions["sortBy"]
|
||||
};
|
||||
await updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
|
||||
@@ -114,8 +114,8 @@ export const UserSheet = () => {
|
||||
progress ? `(${progress.current})` : ""
|
||||
}`
|
||||
: lastSyncStatus === SyncStatus.Failed
|
||||
? strings.syncFailed()
|
||||
: strings.synced()}{" "}
|
||||
? strings.syncFailed()
|
||||
: strings.synced()}{" "}
|
||||
{!syncing ? (
|
||||
<TimeSince
|
||||
style={{
|
||||
@@ -139,8 +139,8 @@ export const UserSheet = () => {
|
||||
!user || lastSyncStatus === SyncStatus.Failed
|
||||
? colors.error.icon
|
||||
: isOffline
|
||||
? colors.static.orange
|
||||
: colors.success.icon
|
||||
? colors.static.orange
|
||||
: colors.success.icon
|
||||
}
|
||||
/>
|
||||
</Paragraph>
|
||||
|
||||
@@ -79,6 +79,7 @@ type SimpleTabViewProps = {
|
||||
const createSceneMap = (
|
||||
scenes: Record<string, React.ComponentType<any>>
|
||||
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return ({ route }: { route: SimpleRoute }) => {
|
||||
const SceneComponent = scenes[route.key];
|
||||
if (!SceneComponent) return null;
|
||||
@@ -432,6 +433,7 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
name="plus"
|
||||
testID="sidebar-add-button"
|
||||
size={AppFontSize.lg - 2}
|
||||
top={10}
|
||||
color={colors.primary.icon}
|
||||
onPress={async () => {
|
||||
if (props.navigationState.index === 1) {
|
||||
@@ -484,6 +486,7 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
? "sort-ascending"
|
||||
: "sort-descending"
|
||||
}
|
||||
top={10}
|
||||
testID="sidebar-sort-button"
|
||||
color={colors.primary.icon}
|
||||
onPress={() => {
|
||||
@@ -495,6 +498,11 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
? "notebook"
|
||||
: "tag"
|
||||
}
|
||||
group={
|
||||
props.navigationState.index === 1
|
||||
? "notebooks"
|
||||
: "tags"
|
||||
}
|
||||
hideGroupOptions
|
||||
/>
|
||||
)
|
||||
@@ -519,6 +527,7 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
width: 28,
|
||||
height: 28
|
||||
}}
|
||||
top={10}
|
||||
testID="sidebar-theme-button"
|
||||
color={colors.primary.icon}
|
||||
name={isDark ? "weather-night" : "weather-sunny"}
|
||||
|
||||
@@ -24,18 +24,22 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useTotalNotes } from "../../hooks/use-db-item";
|
||||
|
||||
import { db } from "../../common/database";
|
||||
import { eSubscribeEvent } from "../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
subscribeToItemUpdate
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import useNavigationStore, {
|
||||
RouteParams
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { eAfterSync } from "../../utils/events";
|
||||
import { eAfterSync, eMenuItemUpdate } from "../../utils/events";
|
||||
import { SideMenuItem } from "../../utils/menu-items";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useSideBarDraggingStore } from "./dragging-store";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
|
||||
export function MenuItem({
|
||||
item,
|
||||
@@ -56,13 +60,19 @@ export function MenuItem({
|
||||
const totalNotes = useTotalNotes(
|
||||
item.dataType as "notebook" | "tag" | "color"
|
||||
);
|
||||
const update = useRelationStore((state) => state.updater);
|
||||
const getTotalNotesRef = useRef(totalNotes.getTotalNotes);
|
||||
getTotalNotesRef.current = totalNotes.getTotalNotes;
|
||||
|
||||
const menuItemCount = !item.data
|
||||
? itemCount
|
||||
: totalNotes.totalNotes(item.data.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (item.data) {
|
||||
getTotalNotesRef.current([item.data?.id]);
|
||||
}
|
||||
}, [item.data, update]);
|
||||
|
||||
useEffect(() => {
|
||||
const onSyncComplete = async () => {
|
||||
try {
|
||||
@@ -84,7 +94,9 @@ export function MenuItem({
|
||||
setItemCount(await db.notes.archived.count());
|
||||
break;
|
||||
case "Trash":
|
||||
setItemCount((await db.trash.all()).length);
|
||||
{
|
||||
setItemCount((await db.trash.all()).length);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -94,10 +106,20 @@ export function MenuItem({
|
||||
/** Empty */
|
||||
}
|
||||
};
|
||||
const event = eSubscribeEvent(eAfterSync, onSyncComplete);
|
||||
const events = [eSubscribeEvent(eAfterSync, onSyncComplete)];
|
||||
|
||||
if (!item.data) {
|
||||
events.push(eSubscribeEvent(eMenuItemUpdate, onSyncComplete));
|
||||
}
|
||||
|
||||
if (item.data?.id) {
|
||||
events.push(
|
||||
subscribeToItemUpdate(item?.data?.id, item?.data?.type, onSyncComplete)
|
||||
);
|
||||
}
|
||||
onSyncComplete();
|
||||
return () => {
|
||||
event?.unsubscribe();
|
||||
events?.forEach((e) => e?.unsubscribe());
|
||||
};
|
||||
}, [item.data, item.id]);
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ import { StoreApi, UseBoundStore } from "zustand";
|
||||
import { useTotalNotes } from "../../hooks/use-db-item";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
eUnSubscribeEvent,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { TreeItem } from "../../stores/create-notebook-tree-stores";
|
||||
import { SelectionStore } from "../../stores/item-selection-store";
|
||||
@@ -35,6 +36,7 @@ import AppIcon from "../ui/AppIcon";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
|
||||
export const NotebookItem = ({
|
||||
index,
|
||||
@@ -70,13 +72,14 @@ export const NotebookItem = ({
|
||||
const notebook = item.notebook;
|
||||
const isFocused = focused;
|
||||
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const getTotalNotesRef = React.useRef(getTotalNotes);
|
||||
getTotalNotesRef.current = getTotalNotes;
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
useEffect(() => {
|
||||
getTotalNotesRef.current([item.notebook.id]);
|
||||
}, [item.notebook]);
|
||||
}, [item.notebook, updater]);
|
||||
|
||||
useEffect(() => {
|
||||
const onNotebookUpdate = (id?: string) => {
|
||||
@@ -98,10 +101,11 @@ export const NotebookItem = ({
|
||||
item.depth === 0
|
||||
? undefined
|
||||
: item.depth < 6
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
width: "100%",
|
||||
marginTop: 2
|
||||
marginTop: 2,
|
||||
opacity: item.disabled ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
@@ -187,8 +191,8 @@ export const NotebookItem = ({
|
||||
!item.hasChildren || disableExpand
|
||||
? "book-outline"
|
||||
: expanded
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -241,6 +245,7 @@ export const NotebookItem = ({
|
||||
name="plus"
|
||||
size={AppFontSize.md}
|
||||
testID={`add-notebook-${index}`}
|
||||
color={colors.primary.icon}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
|
||||
@@ -76,7 +76,7 @@ export const PinnedSection = React.memo(
|
||||
onPress: onPress,
|
||||
onLongPress: onLongPress
|
||||
})) as SideMenuItem[],
|
||||
[menuPins, onPress]
|
||||
[menuPins, onLongPress, onPress]
|
||||
);
|
||||
|
||||
const renderItem = React.useCallback(({ item }: { item: SideMenuItem }) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
useSideMenuNotebookTreeStore
|
||||
} from "./stores";
|
||||
import { LegendList } from "@legendapp/list";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
useSideMenuNotebookSelectionStore.setState({
|
||||
multiSelect: true
|
||||
});
|
||||
@@ -52,6 +53,7 @@ export const SideMenuNotebooks = () => {
|
||||
const [filteredNotebooks, setFilteredNotebooks] = React.useState(notebooks);
|
||||
const searchTimer = React.useRef<NodeJS.Timeout>(undefined);
|
||||
const lastQuery = React.useRef<string>(undefined);
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const loadRootNotebooks = React.useCallback(async () => {
|
||||
if (!filteredNotebooks) return;
|
||||
const _notebooks: Notebook[] = [];
|
||||
@@ -66,9 +68,6 @@ export const SideMenuNotebooks = () => {
|
||||
|
||||
const updateNotebooks = React.useCallback(() => {
|
||||
if (lastQuery.current) {
|
||||
// useSideMenuNotebookTreeStore.setState({
|
||||
// isSearching: true
|
||||
// });
|
||||
db.lookup
|
||||
.notebooks(lastQuery.current)
|
||||
.sorted(db.settings.getGroupOptions("notebooks"))
|
||||
@@ -76,16 +75,13 @@ export const SideMenuNotebooks = () => {
|
||||
setFilteredNotebooks(filtered);
|
||||
});
|
||||
} else {
|
||||
// useSideMenuNotebookTreeStore.setState({
|
||||
// isSearching: false
|
||||
// });
|
||||
setFilteredNotebooks(notebooks);
|
||||
}
|
||||
}, [notebooks]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNotebooks();
|
||||
}, [updateNotebooks]);
|
||||
}, [updateNotebooks,updater]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ import { SideMenuHeader } from "./side-menu-header";
|
||||
import { SideMenuListEmpty } from "./side-menu-list-empty";
|
||||
import { useSideMenuTagsSelectionStore } from "./stores";
|
||||
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
|
||||
const TagItem = (props: {
|
||||
tags: VirtualizedGrouping<Tag>;
|
||||
@@ -55,12 +56,13 @@ const TagItem = (props: {
|
||||
const totalNotes = useTotalNotes("tag");
|
||||
const totalNotesRef = React.useRef(totalNotes);
|
||||
totalNotesRef.current = totalNotes;
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.id) {
|
||||
totalNotesRef.current?.getTotalNotes([item?.id]);
|
||||
}
|
||||
}, [item]);
|
||||
}, [item, updater]);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -303,7 +305,7 @@ export const SideMenuTags = () => {
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}, 500);
|
||||
}, 100);
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
@@ -38,10 +38,10 @@ import {
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
@@ -119,11 +119,7 @@ export const Toast = ({ context = "global" }) => {
|
||||
width: DDS.isTab ? dimensions.width / 2 : "100%",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
bottom:
|
||||
Platform.OS === "android"
|
||||
? Math.max(insets.bottom, 40)
|
||||
: Math.max(insets.bottom, 40) +
|
||||
(keyboard.keyboardShown ? keyboard.keyboardHeight : 0),
|
||||
bottom: insets.bottom + 15,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
elevation: 15
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ColorValue, TextProps } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import EvilIcon from "react-native-vector-icons/EvilIcons";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
|
||||
export interface IconProps extends TextProps {
|
||||
@@ -43,11 +44,22 @@ export interface IconProps extends TextProps {
|
||||
*
|
||||
*/
|
||||
color?: ColorValue | number | undefined;
|
||||
|
||||
iconFamily?: "evilicons" | "material";
|
||||
}
|
||||
|
||||
export default function AppIcon(props: IconProps) {
|
||||
export default function AppIcon({
|
||||
iconFamily = "material",
|
||||
...props
|
||||
}: IconProps) {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
return iconFamily === "evilicons" ? (
|
||||
<EvilIcon
|
||||
size={AppFontSize.md}
|
||||
color={colors.primary.icon}
|
||||
{...(props as any)}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
size={AppFontSize.md}
|
||||
color={colors.primary.icon}
|
||||
|
||||
@@ -107,7 +107,7 @@ export const IconButton = ({
|
||||
color={
|
||||
restProps.disabled
|
||||
? RGB_Linear_Shade(-0.05, hexToRGBA(colors.secondary.background))
|
||||
: colors.static[color as never] || color
|
||||
: colors.static[color as never] || color || colors.primary.icon
|
||||
}
|
||||
size={size}
|
||||
/>
|
||||
|
||||
408
apps/mobile/app/components/ui/input/form-input.tsx
Normal file
408
apps/mobile/app/components/ui/input/form-input.tsx
Normal file
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
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, { RefObject, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
TextInput,
|
||||
TextInputProps,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
} from "react-native";
|
||||
import isEmail from "validator/lib/isEmail";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import AppIcon from "../AppIcon";
|
||||
|
||||
export type FormValues = Record<string, string>;
|
||||
export type FormErrors = Partial<Record<string, string>>;
|
||||
export type FieldValidator = (
|
||||
value: string,
|
||||
values: FormValues
|
||||
) => string | undefined;
|
||||
export type ValidationSchema = Partial<Record<string, FieldValidator[]>>;
|
||||
|
||||
export interface FormRef {
|
||||
values: FormValues;
|
||||
errors: FormErrors;
|
||||
setValue: (name: string, value: string) => void;
|
||||
getValue: (name: string) => string;
|
||||
getValues: () => FormValues;
|
||||
setError: (name: string, error?: string) => void;
|
||||
getError: (name: string) => string | undefined;
|
||||
clearErrors: () => void;
|
||||
registerField: (name: string, validators?: FieldValidator[]) => void;
|
||||
unregisterField: (name: string) => void;
|
||||
validateField: (name: string) => string | undefined;
|
||||
validate: () => boolean;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
}
|
||||
|
||||
export function createFormRef(initialValues: FormValues = {}): FormRef {
|
||||
const listeners = new Set<() => void>();
|
||||
const values: FormValues = { ...initialValues };
|
||||
const errors: FormErrors = {};
|
||||
const schema: ValidationSchema = {};
|
||||
|
||||
const notify = () => {
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
return {
|
||||
values,
|
||||
errors,
|
||||
setValue(name, value) {
|
||||
values[name] = value;
|
||||
if (errors[name]) {
|
||||
delete errors[name];
|
||||
notify();
|
||||
}
|
||||
},
|
||||
getValue(name) {
|
||||
return values[name] ?? "";
|
||||
},
|
||||
getValues() {
|
||||
return { ...values };
|
||||
},
|
||||
setError(name, error) {
|
||||
if (!error) {
|
||||
delete errors[name];
|
||||
} else {
|
||||
errors[name] = error;
|
||||
}
|
||||
notify();
|
||||
},
|
||||
getError(name) {
|
||||
return errors[name];
|
||||
},
|
||||
clearErrors() {
|
||||
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||
notify();
|
||||
},
|
||||
registerField(name, validators = []) {
|
||||
schema[name] = validators;
|
||||
if (values[name] === undefined) values[name] = "";
|
||||
},
|
||||
unregisterField(name) {
|
||||
delete schema[name];
|
||||
delete errors[name];
|
||||
notify();
|
||||
},
|
||||
validateField(name) {
|
||||
const fieldValidators = schema[name] || [];
|
||||
const value = values[name] ?? "";
|
||||
for (const validator of fieldValidators) {
|
||||
const error = validator(value, values);
|
||||
if (error) {
|
||||
errors[name] = error;
|
||||
notify();
|
||||
return error;
|
||||
}
|
||||
}
|
||||
delete errors[name];
|
||||
notify();
|
||||
return undefined;
|
||||
},
|
||||
validate() {
|
||||
const nextErrors = validateForm(values, schema);
|
||||
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||
Object.keys(nextErrors).forEach((key) => {
|
||||
errors[key] = nextErrors[key];
|
||||
});
|
||||
notify();
|
||||
return !hasFormErrors(nextErrors);
|
||||
},
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function validateForm(
|
||||
values: FormValues,
|
||||
schema: ValidationSchema
|
||||
): FormErrors {
|
||||
const errors: FormErrors = {};
|
||||
|
||||
Object.keys(schema).forEach((field) => {
|
||||
const fieldValidators = schema[field] || [];
|
||||
const value = values[field] ?? "";
|
||||
for (const validator of fieldValidators) {
|
||||
const error = validator(value, values);
|
||||
if (error) {
|
||||
errors[field] = error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function hasFormErrors(errors: FormErrors) {
|
||||
return Object.keys(errors).length > 0;
|
||||
}
|
||||
|
||||
export const validators = {
|
||||
required:
|
||||
(message = "This field is required") =>
|
||||
(value: string) =>
|
||||
value?.trim() ? undefined : message,
|
||||
|
||||
email:
|
||||
(message = "Please enter a valid email") =>
|
||||
(value: string) =>
|
||||
!value?.trim() || isEmail(value.trim()) ? undefined : message,
|
||||
|
||||
minLength: (length: number, message?: string) => (value: string) =>
|
||||
!value || value.length >= length
|
||||
? undefined
|
||||
: message || `Must be at least ${length} characters`,
|
||||
|
||||
url:
|
||||
(message = "Please enter a valid URL") =>
|
||||
(value: string) =>
|
||||
!value?.trim() || isURL(value.trim(), { allow_underscores: true })
|
||||
? undefined
|
||||
: message,
|
||||
|
||||
matchField:
|
||||
(fieldName: string, message = "Values do not match") =>
|
||||
(value: string, values: FormValues) =>
|
||||
value === values[fieldName] ? undefined : message
|
||||
};
|
||||
|
||||
interface FormInputProps extends TextInputProps {
|
||||
name: string;
|
||||
formRef: RefObject<FormRef>;
|
||||
validators?: FieldValidator[];
|
||||
fwdRef?: RefObject<TextInput | null>;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
customColor?: ColorValue;
|
||||
marginBottom?: number;
|
||||
marginRight?: number;
|
||||
button?: {
|
||||
icon: string;
|
||||
color: ColorValue;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
size?: number;
|
||||
};
|
||||
buttons?: React.ReactNode;
|
||||
buttonLeft?: React.ReactNode;
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
inputStyle?: TextInputProps["style"];
|
||||
containerStyle?: ViewStyle;
|
||||
wrapperStyle?: ViewStyle;
|
||||
}
|
||||
|
||||
export function FormInput({
|
||||
name,
|
||||
formRef,
|
||||
validators: fieldValidators = [],
|
||||
fwdRef,
|
||||
loading,
|
||||
error,
|
||||
secureTextEntry,
|
||||
customColor,
|
||||
marginBottom = 10,
|
||||
marginRight,
|
||||
button,
|
||||
buttonLeft,
|
||||
buttons,
|
||||
height = 45,
|
||||
fontSize = AppFontSize.sm,
|
||||
inputStyle = {},
|
||||
containerStyle = {},
|
||||
wrapperStyle = {},
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPress,
|
||||
onChangeText,
|
||||
...restProps
|
||||
}: FormInputProps) {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [secureEntry, setSecureEntry] = useState(true);
|
||||
const [, setVersion] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const form = formRef.current;
|
||||
form.registerField(name, fieldValidators);
|
||||
const unsubscribe = form.subscribe(() => {
|
||||
setVersion((v) => v + 1);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
form.unregisterField(name);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formRef, name]);
|
||||
|
||||
const value = formRef.current.getValue(name);
|
||||
const fieldError = error || formRef.current.getError(name);
|
||||
|
||||
const borderColor = useMemo(() => {
|
||||
if (fieldError) return colors.error.accent;
|
||||
if (focused) return customColor || colors.selected.border;
|
||||
return colors.primary.border;
|
||||
}, [colors, customColor, fieldError, focused]);
|
||||
|
||||
const style: ViewStyle = {
|
||||
borderWidth: 1,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderColor,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
...containerStyle
|
||||
};
|
||||
|
||||
const textStyle: TextInputProps["style"] = {
|
||||
paddingHorizontal: 0,
|
||||
fontSize,
|
||||
color:
|
||||
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
fontFamily: "Inter-Regular",
|
||||
...(inputStyle as ViewStyle)
|
||||
};
|
||||
|
||||
const handleChangeText = (nextValue: string) => {
|
||||
formRef.current.setValue(name, nextValue);
|
||||
onChangeText?.(nextValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
importantForAccessibility="yes"
|
||||
style={{
|
||||
marginBottom,
|
||||
marginRight,
|
||||
...wrapperStyle
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
disabled={!loading}
|
||||
onPress={onPress}
|
||||
activeOpacity={1}
|
||||
style={style}
|
||||
>
|
||||
{buttonLeft}
|
||||
|
||||
<TextInput
|
||||
{...restProps}
|
||||
defaultValue={value}
|
||||
ref={fwdRef}
|
||||
editable={!loading && restProps.editable !== false}
|
||||
onChangeText={handleChangeText}
|
||||
onFocus={(e) => {
|
||||
setFocused(true);
|
||||
onFocus?.(e);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setFocused(false);
|
||||
onBlur?.(e);
|
||||
}}
|
||||
keyboardAppearance={isDark ? "dark" : "light"}
|
||||
style={textStyle}
|
||||
secureTextEntry={secureTextEntry && secureEntry}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
height: 35 > height ? height : 35,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{secureTextEntry && (
|
||||
<IconButton
|
||||
name="eye"
|
||||
size={20}
|
||||
top={10}
|
||||
bottom={10}
|
||||
onPress={() => {
|
||||
fwdRef?.current?.blur();
|
||||
setSecureEntry(!secureEntry);
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
marginLeft: 5
|
||||
}}
|
||||
color={
|
||||
secureEntry ? colors.secondary.icon : colors.primary.accent
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{buttons}
|
||||
|
||||
{button && (
|
||||
<IconButton
|
||||
testID={button.testID}
|
||||
name={button.icon}
|
||||
size={button.size || AppFontSize.xl}
|
||||
top={10}
|
||||
bottom={10}
|
||||
onPress={button.onPress}
|
||||
color={button.color}
|
||||
style={{
|
||||
marginRight: -8
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{fieldError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ marginTop: 5, color: colors.error.icon }}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{fieldError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormInput;
|
||||
@@ -268,8 +268,8 @@ export const Pressable = ({
|
||||
const opacity = customOpacity
|
||||
? customOpacity
|
||||
: type === "accent"
|
||||
? 1
|
||||
: colorOpacity;
|
||||
? 1
|
||||
: colorOpacity;
|
||||
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 8;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user