Compare commits

..

103 Commits

Author SHA1 Message Date
Ammar Ahmed
77c009e83d mobile: fix toast message shows under the sheet in global context 2026-07-21 12:14:08 +05:00
Ammar Ahmed
637cf914ee mobile: fix app gets stuck after deleting history item on iOS 2026-07-21 10:25:12 +05:00
Ammar Ahmed
658e915589 mobile: release v3.4.7 2026-07-20 13:12:20 +05:00
Ammar Ahmed
d4658aa032 Merge pull request #10095 from kashaf-ansari-dev/fix-reminder-notification-permission
Improve the Android reminder notification permission flow by properly handling notification and exact alarm permissions.
2026-07-17 09:10:57 +05:00
Ammar Ahmed
489c793806 Merge pull request #10041 from kashaf-ansari-dev/feat/9723-clear-note-history
mobile: add option to clear note version history
2026-07-15 09:52:04 +05:00
kashaf-ansari-dev
eeae993736 mobile: add version history deletion strings
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
c8cf4ce52a mobile: fix individual note history version deletion
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
f6c31d19d8 mobile: add missing context prop in dialog
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
a94e8d6852 mobile: render clear-history dialog with local context
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
2edbc3bf2e mobile: remove clear version history action for multiple selected notes
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
e4a7d63e07 mobile: add option to clear note version history
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:30 +05:00
Ammar Ahmed
76fd61af4f Merge pull request #10085 from streetwriters/fix/651
mobile: fix unable to change plan on buy-plan screen
2026-07-14 08:43:53 +05:00
Ammar Ahmed
98386a6d71 Merge pull request #10094 from streetwriters/mobile-release/3.4.6
mobile: release v3.4.6
2026-07-13 22:44:13 +05:00
kashaf-ansari-dev
539a630f77 mobile: improve reminder permission settings flow on Android
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 18:07:40 +05:00
kashaf-ansari-dev
6b65f8031d mobile: fix reminder creation after granting notification permission
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 14:58:31 +05:00
01zulfi
3c85cddfe7 Merge pull request #10093 from streetwriters/release/3.4.4
web: bump version to 3.4.4
2026-07-13 14:14:25 +05:00
Ammar Ahmed
e426426468 mobile: release v3.4.6 2026-07-13 12:56:49 +05:00
01zulfi
109e46bd06 web: bump version to 3.4.4
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-13 12:28:07 +05:00
Ammar Ahmed
281a973c37 mobile: fix unable to change plan on buy-plan screen 2026-07-11 12:00:15 +05:00
Ammar Ahmed
2738207c20 Merge pull request #9998 from kashaf-ansari-dev/fix-9967-linking-note-blocks-loading-state
mobile: prevent upgrade prompt flash in note block linking list
2026-07-10 18:41:04 +05:00
01zulfi
1aa73c1f23 web: update editor's font size dynamically when default font size setting changes (#10047)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-10 09:39:21 +05:00
Masatoshi Ogiwara
9ec22724e9 fix: typo in some documents (#10071) 2026-07-09 10:34:09 +05:00
Kashaf Ansari
4a7aa27bd9 mobile: add keep-screen-on preference in settings-behaviour (#10043)
* mobile: add keep-screen-on preference in settings-behaviour

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: default Keep Screen On to true and clarify description

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-09 09:49:38 +05:00
kashaf-ansari-dev
6051614d96 mobile: add bottom spacing for create link button
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-08 18:23:23 +05:00
kashaf-ansari-dev
295a461dda mobile: wire upgrade plan button to paywall on link-note
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-08 17:55:00 +05:00
Chloe Oletto
a4cbe83211 vericrypt: Update to support DEK. (#10005)
* vericrypt: Update to support DEK.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* vericrypt: Clear account data on restart.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update apps/vericrypt/src/components/decrypted-result.tsx

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Update data encryption documentation to be accurate with the current client versions.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Explain what happens better.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Fix typo in encryption key generation section

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update docs/help/contents/how-is-my-data-encrypted.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: update wording

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-07-08 10:24:17 +05:00
Masatoshi Ogiwara
afd63db043 docs(help): typo in documents (#10064) 2026-07-08 08:41:17 +05:00
Ammar Ahmed
aadb6e3a01 mobile: prepare v3.4.5 for release (#10060) 2026-07-07 09:30:31 +05:00
Ammar Ahmed
7c3fdab6ee Merge pull request #10053 from kashaf-ansari-dev/fix-7348-unlink-notebook-on-relink
mobile: unlink note from previous notebook in single-select mode
2026-07-07 09:25:56 +05:00
Ammar Ahmed
14f727d6e6 Merge pull request #10016 from kashaf-ansari-dev/fix-9742-duplicate-pinned-note
core: do not preserve pin state when duplicating notes
2026-07-07 09:25:23 +05:00
01zulfi
688a646181 Merge pull request #10058 from streetwriters/release/3.4.3
web: bump version to 3.4.3
2026-07-06 15:10:47 +05:00
Kashaf Ansari
7a7f0db941 mobile: use FileViewer for image sharing on Android (#10018)
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-06 13:37:29 +05:00
Kashaf Ansari
96be35c105 mobile: fix long tag names overflow and display in tag items (#9950)
* mobile: fix long tag names overflow and display in tag items

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: fix long title in side menu

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: fix long notebook title display

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: prevent tag list overlap on first render

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-06 13:36:31 +05:00
01zulfi
36bacce3ac web: bump version to 3.4.3
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-06 10:47:13 +05:00
kashaf-ansari-dev
c89e333c11 mobile: unlink note from previous notebook in single-select mode
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-03 13:12:10 +05:00
Abdullah Atta
d521eb3bd8 desktop: regenerate package-lock (#10051) 2026-07-03 05:45:48 +05:00
Abdullah Atta
20bf090c7e desktop: fix source generation for electron (#10046)
* desktop: fix source generation for electron

* ci: upload generated-sources as build artifact
2026-07-03 05:14:11 +05:00
01zulfi
6cc9b8d1e1 web: add top margin to unlock view error text (#10030)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-02 08:45:49 +05:00
01zulfi
e931423eba web: show success toast when copying note link in context menu (#10027)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-02 08:45:41 +05:00
01zulfi
743d47e9e9 web: verify old password before creating backup during password update (#10031)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-02 08:45:26 +05:00
Ammar Ahmed
37c96177a7 Mobile release/3.4.4 (#10040)
* mobile: add bump-version script

* mobile: bump version to v3.4.4
2026-07-01 15:54:10 +05:00
Abdullah Atta
834479d095 Release v3.4.2 (#10039)
* web: bump version to 3.4.2

* desktop: fix `Invalid filename` error on backup creation
2026-07-01 12:51:34 +05:00
Abdullah Atta
643515c6c4 desktop: fix Invalid filename error on backup creation 2026-07-01 12:28:45 +05:00
Abdullah Atta
0b0a4f1b6f web: bump version to 3.4.2 2026-07-01 11:52:50 +05:00
Ammar Ahmed
48d88069c0 core: make db migration more resilient and recoverable (#10037)
* core: do not throw error if inboxitemshistory table already exists during migration

* core: add ensure column check to avoid `column already exists` errors

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-07-01 11:51:24 +05:00
Abdullah Atta
9d7754f1e1 ci: fix macos x64 build (#10038) 2026-07-01 08:54:05 +05:00
Abdullah Atta
b2ed115011 Merge pull request #10034 from streetwriters/release/3.4.1
Release v3.4.1
2026-06-30 20:32:34 +05:00
Abdullah Atta
3447e8133c web: bump version to 3.4.1 2026-06-30 20:15:54 +05:00
Abdullah Atta
4473d8ec67 ci: fix linux builds 2026-06-30 20:14:53 +05:00
01zulfi
08cf21b05a web: compress image in realtime for better UX in attach files dialog (#9899)
* web: compress image in realtime for better UX in attach files dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: refactor

* web: fix "done" button is disabled

* theme: improve switch styles

* web: improve bulk attachment processing & ux

* web: fix `attachFile` is not a function

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-06-30 19:31:42 +05:00
01zulfi
10dccba5b6 docs: include ColorNote & UpNote pages in importer section (#10029)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 15:22:00 +05:00
01zulfi
23c9a98013 docs: remove inbox api beta warning (#10028)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 13:43:04 +05:00
01zulfi
473fae4a30 Merge pull request #9995 from streetwriters/release/3.4.0
web: bump version to 3.4.0
2026-06-30 13:16:48 +05:00
Ammar Ahmed
e9808c08f0 mobile: bump version 2026-06-30 11:13:02 +05:00
Abdullah Atta
4bb46ea149 ci: fix build for windows 2026-06-30 10:02:44 +05:00
Ammar Ahmed
01811372fc mobile: prepare for release 2026-06-30 09:43:29 +05:00
Abdullah Atta
6d2097259b editor: remove .replaceAll usage to support older Android 9 devices 2026-06-30 09:43:29 +05:00
Abdullah Atta
4c3e065928 core: add comment about why we aren't lowercasing email for old password generation 2026-06-30 09:43:29 +05:00
Abdullah Atta
e45b72a8c4 ci: install sqlite modules in apps/desktop dir 2026-06-30 09:43:29 +05:00
Abdullah Atta
aceb6db8b8 core: always lowercase email address when resetting password 2026-06-30 09:43:29 +05:00
Abdullah Atta
5a0ca0bed9 ci: fix sqlite modules not being found in desktop builds 2026-06-30 09:43:29 +05:00
Abdullah Atta
8dd5779308 ci: fix desktop tests 2026-06-30 09:43:29 +05:00
Abdullah Atta
dc958098ea desktop: fix tests 2026-06-30 09:43:29 +05:00
Ammar Ahmed
bf536eb183 mobile: fix event subscription 2026-06-30 09:43:29 +05:00
Ammar Ahmed
eb3eea031e mobile: add support for copying links as html links with proper title. 2026-06-30 09:43:29 +05:00
Ammar Ahmed
d376e00a1e editor: cleanup nn:// links when pasted
Ensure that we remove any styles added to deep links. This is done especially for iOS where pasted link html get's wrapped into custom div with some hard coded styles making the link not match style with the rest of content.
2026-06-30 09:43:29 +05:00
Ammar Ahmed
aca36b60b7 editor: fix note links not detected on paste/edit 2026-06-30 09:43:29 +05:00
Ammar Ahmed
033666795c mobile: fix screenguard module error 2026-06-30 09:43:29 +05:00
Abdullah Atta
c0872c98a8 desktop: fix deep linking not working 2026-06-30 09:43:29 +05:00
01zulfi
f2173a42d9 web: add confirmation before deleting failed inbox items
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:29 +05:00
Ammar Ahmed
cbddea4518 mobile: show toast message on vault locked/unlocked events 2026-06-30 09:43:03 +05:00
Ammar Ahmed
c69304781a mobile: fix password input is not shown in vault dialog 2026-06-30 09:43:03 +05:00
Ammar Ahmed
412631bf67 core: dispatch vaultUnlocked event when password is updated. 2026-06-30 09:43:03 +05:00
Abdullah Atta
8c2be00920 core: do not send PushCompletedV2 if no items sent 2026-06-30 09:43:03 +05:00
01zulfi
553e2060be editor: copy html for internal links in copy link tool
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
Abdullah Atta
7603de25c6 ci: fix Could not find any Visual Studio installation to use 2026-06-30 09:43:03 +05:00
01zulfi
146216949d web: update publish view popup when monographs sync (#10007)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
Abdullah Atta
c4d8abef16 setup: add node-gyp-build to postinstall whitelist 2026-06-30 09:43:03 +05:00
Abdullah Atta
baf823264b core: remove debug tests 2026-06-30 09:43:03 +05:00
Abdullah Atta
7f0a517ee1 config: add better-sqlite3 to postinstall whitelist 2026-06-30 09:43:03 +05:00
01zulfi
da48cf0bd1 web: don't show monograph view count when self destruct enabled
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
Ammar Ahmed
4c41e13c5d mobile: fix ts errors 2026-06-30 09:43:03 +05:00
01zulfi
82e2953bc8 web: fix paragraph color in inbox settings for dark mode
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
01zulfi
fc244b1196 core: delete linked inboxHistoryItem's on permanent note deletion
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
01zulfi
98ecad3450 web: bump version to 3.4.0
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:43:03 +05:00
Abdullah Atta
54dcbd9be6 web: bump importer to 2.4.5 2026-06-30 09:42:42 +05:00
Abdullah Atta
d26c99c487 web: continue importing if an error occurs but highlight the error clearly 2026-06-30 09:42:42 +05:00
01zulfi
0922d9d833 web: support colornote importer (#9985)
* docs: add import from colornote page
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: add support for colornote importer
* add colornote password input field
* add support for importing reminders
* fix archived notes import

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* docs: update import from colornote page
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: use PromptDialog instead of field in colornote importer's password option
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* docs: code review suggestion
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* intl: update strings
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:42:42 +05:00
01zulfi
0cdeee80fa docs: add import from upnote page (#9272)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-30 09:42:42 +05:00
kashaf-ansari-dev
3aa23d3215 core: do not preserve pin state when duplicating notes
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-25 17:33:36 +05:00
Abdullah Atta
936b6cb4aa Merge branch 'master' into beta 2026-06-23 08:34:57 +05:00
kashaf-ansari-dev
e0b9932aba mobile: prevent upgrade prompt flash in note block linking list
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-19 22:31:56 +05:00
Abdullah Atta
f9f974c9f0 Merge pull request #9429 from streetwriters/web/fix-vault-deletion-for-not-logged-in-user
web: don't ask for account password for vault deletion for not logged in users
2026-06-19 08:46:18 +05:00
01zulfi
e3816234ff monograph: show error message for empty password on unlock (#9927)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-18 14:17:33 +05:00
01zulfi
8b36ae6dee core: fix "table already exists" error in migrations (#9987)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-18 09:45:22 +05:00
Abdullah Atta
0bd030dc78 Merge pull request #9984 from streetwriters/fix/disable-ignore-edit-save
Editor transactions with `ignoreEdit` will no longer trigger save
2026-06-18 09:17:32 +05:00
Ammar Ahmed
63eb42eb9e mobile: disable save when ignoreEdit is true 2026-06-18 08:40:52 +05:00
Abdullah Atta
eb75df360f web: editor transactions with ignoreEdit will no longer trigger save 2026-06-17 21:44:34 +05:00
Ammar Ahmed
89f3a7945b mobile: update styling for checkbox 2026-06-17 09:21:05 +05:00
Ammar Ahmed
7af1387691 mobile: fix delete all notes check truncated 2026-06-17 09:18:21 +05:00
Ammar Ahmed
1d292d740c mobile: fix vault asks for fingerprint permission on creating vault 2026-06-17 09:03:20 +05:00
Ammar Ahmed
4f884a8411 mobile: do not ask for password on vault deletion if user not logged in 2026-06-17 09:03:19 +05:00
01zulfi
3122e8a379 web: don't ask for account password for vault deletion for not logged in users
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-17 09:02:30 +05:00
506 changed files with 25386 additions and 38527 deletions

View File

@@ -210,7 +210,7 @@ jobs:
build-windows:
name: Build for Windows
needs: build
runs-on: windows-latest
runs-on: windows-2022
outputs:
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:

View File

@@ -107,7 +107,7 @@ jobs:
target_commitish: ${{ github.ref }}
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
if: inputs.build-linux
run: |
sudo apt-get update
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
@@ -127,6 +127,13 @@ jobs:
files: ./generated-sources.json
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
- name: Upload generated-sources.json as artifact
uses: actions/upload-artifact@v4
if: inputs.build-linux
with:
name: generated-sources
path: ./generated-sources.json
build-macos:
name: Build for macOS
needs: build
@@ -182,7 +189,9 @@ jobs:
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite-better-trigram
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Install provisioning profile
@@ -264,6 +273,7 @@ jobs:
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu x64 sqlite-better-trigram
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build Electron wrapper
@@ -331,6 +341,7 @@ jobs:
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build Electron wrapper
@@ -363,7 +374,7 @@ jobs:
name: Build for Windows
needs: build
if: inputs.build-windows
runs-on: windows-latest
runs-on: windows-2022
steps:
- name: Check out Git repository

View File

@@ -18,11 +18,12 @@ on:
- ".github/workflows/desktop.tests.yml"
jobs:
build:
name: Build
runs-on: ubuntu-22.04
test-macos-x64:
name: Test macOS x64
runs-on: macos-15-intel
steps:
- name: Checkout
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
@@ -33,44 +34,10 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Generate desktop build
run: npm run tx @notesnook/web:build:desktop
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
test-macos-x64:
name: Test macOS x64
needs: build
runs-on: macos-15-intel
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
- name: Install sqlite-better-trigram for all arch
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
npm i --cpu x64 sqlite-better-trigram
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Run tests x64
@@ -87,7 +54,6 @@ jobs:
test-macos:
name: Test macOS
needs: build
runs-on: macos-latest
steps:
@@ -97,23 +63,15 @@ jobs:
- 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
npm run bootstrap -- --scope=web
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build app
- name: Install sqlite-better-trigram for all arch
run: |
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Run tests arm64
@@ -130,7 +88,6 @@ jobs:
test-linux:
name: Test for Linux
needs: build
runs-on: ubuntu-22.04
steps:
@@ -140,29 +97,15 @@ jobs:
- 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
npm run bootstrap -- --scope=web
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Run tests
@@ -179,8 +122,7 @@ jobs:
test-windows:
name: Test for Windows
needs: build
runs-on: windows-latest
runs-on: windows-2022
steps:
- name: Check out Git repository
@@ -189,34 +131,17 @@ jobs:
- 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
npm run bootstrap -- --scope=web
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: node scripts/execute.mjs @notesnook/desktop:release
- name: Build app
run: |
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: npm run test
working-directory: ./apps/desktop

View File

@@ -1,51 +0,0 @@
## Notesnook Design Agent
### Purpose
- Implement Figma designs from user-provided links into the Notesnook app with high visual fidelity.
- Use related components and files provided by the user as the source of truth for local patterns.
- Preserve behavior, navigation, and existing data flow while updating UI.
### When To Use This Agent
- Use this agent when the task is design-to-code for app UI.
- Prefer this agent over a general coding agent when the user provides a Figma link and asks for redesign implementation.
### Scope
- Primary focus: apps/mobile.
- Secondary scope: shared UI dependencies needed for the design update (tokens, icons, strings, and generated assets).
- Do not perform unrelated refactors.
### Tool Preferences
- Prefer Figma design-context tools to extract layout, spacing rhythm, typography intent, and asset references from the exact node.
- Prefer direct workspace edits and targeted validation for touched files.
- Avoid introducing new UI frameworks or web-specific styling approaches.
### Repo Conventions
- Use theme and token systems first: useThemeColors, spacing/radius constants, and shared typography primitives.
- Reuse existing UI primitives and patterns before creating new abstractions.
- Keep dark and light mode compatibility by default.
- Keep changes minimal, isolated, and review-friendly.
### Icon Conventions
- Add new icon SVGs to packages/icons/svgs with lowercase kebab-case names.
- Regenerate icon assets from apps/mobile using npx react-native-nano-icons.
- Use generated icon names through AppIcon with icon family notesnook where applicable.
### Figma To Code Workflow
1. Read the exact Figma node from the provided link.
2. Compare against existing repo patterns and referenced components.
3. Implement with React Native conventions already used in the app.
4. Preserve behavior and interactions unless explicitly changed.
5. Validate compile and lint state for edited files.
### Output Expectations
- Provide a concise summary of what changed.
- List modified files and any generated artifacts.
- Call out follow-up steps only when needed.

View File

@@ -17,10 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test } from "@nn/test";
import { test, expect } from "@nn/test";
import { gt, lt } from "semver";
import { AppModel } from "../../web/__e2e__/models/app.model.js";
import { expect } from "playwright/test";
test.extend({ options: { version: "3.0.0" } })(
"update starts downloading if version is outdated",

View File

@@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { mergeTests } from "@playwright/test";
import { test } from "@playwright/test";
import { mergeTests, test } from "@playwright/test";
import type { CommonFixtures, CommonWorkerFixtures } from "./common-fixtures";
import { commonFixtures } from "./common-fixtures";
import { platformTest } from "./platform-fixtures";

View File

@@ -21,7 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { AppContext, buildAndLaunchApp, TestOptions } from "./utils";
import path from "path";
import { version } from "../../package.json";
import type { Browser, ElectronApplication, Page } from "@playwright/test";
import type { ElectronApplication, Page } from "@playwright/test";
import type { TraceViewerFixtures } from "./trace-viewer-fixtures";
import { traceViewerFixtures } from "./trace-viewer-fixtures";
import { PageTestFixtures, PageWorkerFixtures } from "./page-test-api";

View File

@@ -1,42 +1,35 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
/* eslint-disable header/header */
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Copyright (C) 2023 Streetwriters (Private) Limited
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/// @ts-ignore
import { oop, client } from "playwright-core/lib/coreBundle";
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.
export type TestModeName = "default" | "driver";
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-ignore
import { start } from "playwright-core/lib/outofprocess";
// @ts-ignore
import type { Playwright } from "playwright-core/lib/client/playwright";
export type TestModeName =
| "default"
| "driver"
| "service"
| "service2"
| "wsl";
const { start } = oop;
interface TestMode {
setup(): Promise<Playwright>;
setup(): Promise<client.Playwright>;
teardown(): Promise<void>;
}
export class DriverTestMode implements TestMode {
private _impl: { playwright: Playwright; stop: () => Promise<void> };
private _impl: { playwright: client.Playwright; stop: () => Promise<void> };
async setup() {
this._impl = await start({

View File

@@ -20,7 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { execSync } from "child_process";
import { cp } from "fs/promises";
import path, { join, resolve } from "path";
import { _electron as electron, ElectronApplication, Page } from "playwright";
import {
_electron as electron,
ElectronApplication,
Page
} from "@playwright/test";
import { existsSync } from "fs";
import { mkdir, writeFile } from "node:fs/promises";
@@ -30,7 +34,7 @@ const root = path.resolve(__dirname, "..", "..");
const SOURCE_DIR = resolve(root, "output", productName);
export interface AppContext {
app: import("playwright").ElectronApplication;
app: ElectronApplication;
userDataDir: string;
outputDir: string;
relaunch: () => Promise<void>;

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.4.0-beta.1",
"version": "3.4.4",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -54,18 +54,18 @@
"zod": "3.24.3"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@playwright/test": "1.61.1",
"@streetwriters/kysely": "^0.27.4",
"@types/node": "22.15.3",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^37.0.0",
"electron": "^37.10.3",
"electron-builder": "^26.8.1",
"esbuild": "0.21.5",
"fkill": "^10.0.3",
"node-abi": "^4.5.0",
"node-gyp-build": "^4.8.4",
"playwright": "1.58.2",
"playwright-core": "1.61.1",
"prebuildify": "^6.0.1",
"slugify": "1.6.6",
"tree-kill": "^1.2.2",
@@ -86,7 +86,7 @@
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
"postinstall": "patch-package",
"test": "playwright test --tsconfig tsconfig.json"
"test": "playwright test --tsconfig __tests__/tsconfig.json --project notesnook-desktop"
},
"author": {
"name": "Streetwriters (Private) Limited",

View File

@@ -0,0 +1,24 @@
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
index 7d8468f..ab252ef 100644
--- a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
+++ b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
@@ -16010,6 +16010,7 @@ var init_validator = __esm({
args: tOptional(tArray(tString)),
chromiumSandbox: tOptional(tBoolean),
cwd: tOptional(tString),
+ baseURL: tOptional(tString),
env: tOptional(tArray(tType("NameValue"))),
timeout: tFloat,
acceptDownloads: tOptional(tEnum(["accept", "deny", "internal-browser-default"])),
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
index 0bff9d3..d195697 100644
--- a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
+++ b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
@@ -21813,6 +21813,7 @@ export interface Electron {
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
*/
args?: Array<string>;
+ baseURL?: string;
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory

View File

@@ -1,24 +0,0 @@
diff --git a/node_modules/playwright-core/lib/protocol/validator.js b/node_modules/playwright-core/lib/protocol/validator.js
index b36c979..9e8ca15 100644
--- a/node_modules/playwright-core/lib/protocol/validator.js
+++ b/node_modules/playwright-core/lib/protocol/validator.js
@@ -2536,6 +2536,7 @@ import_validatorPrimitives.scheme.ElectronLaunchParams = (0, import_validatorPri
executablePath: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
args: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)(import_validatorPrimitives.tString)),
cwd: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
+ baseURL: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
env: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)((0, import_validatorPrimitives.tType)("NameValue"))),
timeout: import_validatorPrimitives.tFloat,
acceptDownloads: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tEnum)(["accept", "deny", "internal-browser-default"])),
diff --git a/node_modules/playwright-core/types/types.d.ts b/node_modules/playwright-core/types/types.d.ts
index f6e6165..4efd4d4 100644
--- a/node_modules/playwright-core/types/types.d.ts
+++ b/node_modules/playwright-core/types/types.d.ts
@@ -19233,6 +19233,7 @@ export interface Electron {
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
*/
args?: Array<string>;
+ baseURL?: string;
/**
* Toggles bypassing page's Content-Security-Policy. Defaults to `false`.

View File

@@ -46,7 +46,7 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions> = {
globalTimeout: 5400000,
workers: process.env.CI ? 1 : undefined,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 0,
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI
? [["dot"], ["json", { outputFile: path.join(outputDir, "report.json") }]]
: "line",

View File

@@ -33,17 +33,38 @@ function generateId() {
return Math.random().toString(36).slice(2);
}
function getStreamOrThrow(id: string, operation: "write" | "close") {
const stream = activeStreams.get(id);
if (!stream) {
throw new Error(
`Backup stream not found during ${operation}. The stream may have already been closed or was never opened. Stream id: ${id}`
);
}
return stream;
}
export const backupsRouter = t.router({
open: t.procedure
.input(z.object({ filename: z.string() }))
.mutation(({ input }) => {
const { filename } = input;
if (!filename.trim()) {
throw new Error("Invalid backup filename: filename cannot be empty.");
}
if (filename.includes(path.sep) || filename.includes("\\"))
throw new Error("Invalid filename");
throw new Error(
`Invalid backup filename: expected a plain file name without path separators, received "${filename}".`
);
const resolvedBackupDir = resolvePath(config.backupDirectory);
const backupPath = path.resolve(resolvedBackupDir, filename);
if (!backupPath.startsWith(resolvedBackupDir))
throw new Error("Invalid filename");
const relativeBackupPath = path.relative(resolvedBackupDir, backupPath);
if (
relativeBackupPath.startsWith("..") ||
path.isAbsolute(relativeBackupPath)
)
throw new Error(
`Invalid backup filename: resolved path "${backupPath}" is outside the configured backup directory "${resolvedBackupDir}". The configured backup directory may be invalid.`
);
mkdirSync(resolvedBackupDir, { recursive: true });
const stream = createWriteStream(backupPath, { encoding: "utf-8" });
@@ -55,8 +76,7 @@ export const backupsRouter = t.router({
write: t.procedure
.input(z.object({ id: z.string(), chunk: z.string() }))
.mutation(({ input }) => {
const stream = activeStreams.get(input.id);
if (!stream) throw new Error("Stream not found");
const stream = getStreamOrThrow(input.id, "write");
return new Promise<void>((resolve, reject) => {
stream.write(Buffer.from(input.chunk, "base64"), (err) =>
err ? reject(err) : resolve()
@@ -67,8 +87,7 @@ export const backupsRouter = t.router({
close: t.procedure
.input(z.object({ id: z.string() }))
.mutation(({ input }) => {
const stream = activeStreams.get(input.id);
if (!stream) throw new Error("Stream not found");
const stream = getStreamOrThrow(input.id, "close");
return new Promise<void>((resolve) => {
stream.end(() => {
activeStreams.delete(input.id);

View File

@@ -33,7 +33,7 @@ import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
import { getTheme, setTheme, Theme } from "../utils/theme";
import { existsSync } from "fs";
import { resolvePath } from "../utils/resolve-path";
import { normalizePathString, resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak, isPortable, isSnap } from "../utils";
@@ -60,7 +60,13 @@ export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
isSnap: t.procedure.query(() => isSnap()),
isPortable: t.procedure.query(() => isPortable()),
backupDirectory: t.procedure.query(() => config.backupDirectory),
backupDirectory: t.procedure.query(() => {
const backupDirectory = normalizePathString(config.backupDirectory);
if (backupDirectory !== config.backupDirectory) {
config.backupDirectory = backupDirectory;
}
return backupDirectory;
}),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {

View File

@@ -40,6 +40,7 @@ import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
import { Messages, setI18nGlobal } from "@notesnook/intl";
import { i18n } from "@lingui/core";
import { PATHS } from "./constants";
import { normalizePathString } from "./utils/resolve-path";
const locale =
process.env.NODE_ENV === "development"
@@ -294,7 +295,7 @@ async function migrateBackupDirectory() {
`localStorage.getItem("backupStorageLocation")`
);
if (!oldPath || oldPath === PATHS.backupsDirectory) return;
config.backupDirectory = oldPath;
config.backupDirectory = normalizePathString(oldPath);
} catch (e) {
console.error("Failed to migrate backup directory", e);
const pressedButton = dialog.showMessageBoxSync(globalThis.window, {

View File

@@ -20,11 +20,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { app } from "electron";
import { isAbsolute, join } from "path";
export function normalizePathString(_path: string) {
let normalizedPath = _path.trim();
try {
const parsedPath = JSON.parse(normalizedPath);
if (typeof parsedPath === "string") normalizedPath = parsedPath;
} catch {
// ignore invalid JSON and keep the original path string
}
if (normalizedPath === "~") return app.getPath("home");
if (/^~[\\/]/.test(normalizedPath)) {
return join(app.getPath("home"), normalizedPath.slice(2));
}
return normalizedPath;
}
export function resolvePath(_path: string) {
if (isAbsolute(_path)) return _path;
const normalizedPath = normalizePathString(_path);
if (isAbsolute(normalizedPath)) return normalizedPath;
return join(
..._path.split("/").map((segment) => {
...normalizedPath.split("/").map((segment) => {
let resolved = segment;
try {
resolved = app.getPath(resolved as any);

View File

@@ -1,9 +0,0 @@
{
"iconSets": [
{
"inputDir": "../../packages/icons/svgs",
"fontFamily": "notesnook-icons",
"outputDir": "./fonts"
}
]
}

View File

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3108
versionCode 3113
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -1,7 +1,4 @@
- Inbox API is now in beta. Learn more about it at https://help.notesnook.com/inbox-api/getting-started
- Added close all tabs button in editor
- Deeplinking support for nn:// urls
- Set default image compression mode in Settings -> Behavior
- Minor bug fixes and performance improvements
- Add option to clear note version history
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -35,7 +35,7 @@ import { Toast } from "./components/toast";
import { useAppEvents } from "./hooks/use-app-events";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { RootNavigation } from "./navigation/navigation-stack";
import { themeTrpcClient } from "./screens/settings/components/theme-selector";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
@@ -44,7 +44,6 @@ import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
import { useSettingStore } from "./stores/use-setting-store";
import { THEME_DARK, THEME_LIGHT } from "./common/design/theme";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
@@ -121,30 +120,30 @@ export const withTheme = (
]);
useEffect(() => {
// setTimeout(() => {
// const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
// if (!currentTheme) return;
// themeTrpcClient.updateTheme
// .query({
// version: currentTheme.version,
// compatibilityVersion: THEME_COMPATIBILITY_VERSION,
// id: currentTheme.id
// })
// .then((theme) => {
// if (theme) {
// theme.colorScheme === "dark"
// ? useThemeStore.setState({
// darkTheme: theme
// })
// : useThemeStore.setState({
// lightTheme: theme
// });
// }
// })
// .catch(() => {
// /* empty */
// });
// }, 1000);
setTimeout(() => {
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (!currentTheme) return;
themeTrpcClient.updateTheme
.query({
version: currentTheme.version,
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
id: currentTheme.id
})
.then((theme) => {
if (theme) {
theme.colorScheme === "dark"
? useThemeStore.setState({
darkTheme: theme
})
: useThemeStore.setState({
lightTheme: theme
});
}
})
.catch(() => {
/* empty */
});
}, 1000);
const listener = Appearance.addChangeListener(({ colorScheme }) => {
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
@@ -160,11 +159,11 @@ export const withTheme = (
}, []);
useEffect(() => {
const nextTheme = colorScheme === "dark" ? THEME_DARK : THEME_LIGHT;
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? THEME_DARK : THEME_LIGHT);
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
currTheme = nextTheme;
}
}, [colorScheme, darkTheme, lightTheme]);

File diff suppressed because one or more lines are too long

View File

@@ -1,56 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const FontSizes = {
XXS: 10,
XS: 12,
SM: 14,
MD: 16,
LG: 18,
XL: 20,
XXL: 32
};
export const FontFamily = {
REGULAR: "Inter-Regular", // 400
MEDIUM: "Inter-Medium", // 500
SEMI_BOLD: "Inter-SemiBold", // 600
BOLD: "Inter-Bold" // 700
};
const LineHeightMultipliers = {
"100%": 1,
"110%": 1.1,
"120%": 1.2,
"130%": 1.3,
"140%": 1.4,
"150%": 1.5
};
export type LineHeightVariants =
| "100%"
| "110%"
| "120%"
| "130%"
| "140%"
| "150%";
export const getLineHeight = (
fontSize: keyof typeof FontSizes,
type: LineHeightVariants
) => {
return FontSizes[fontSize] * LineHeightMultipliers[type];
};

View File

@@ -1,39 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const Spacing = {
LEVEL_0: 4,
LEVEL_1: 8,
LEVEL_2: 12,
LEVEL_3: 16,
LEVEL_4: 20,
LEVEL_5: 24,
LEVEL_6: 28,
LEVEL_7: 32,
LEVEL_8: 40
};
export const Radius = {
XXS: 4,
XS: 8,
S: 12,
MD: 16,
LG: 20,
LG_2: 32,
XXL: 40
};

View File

@@ -1,283 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ThemeDefinition } from "@notesnook/theme";
export const THEME_LIGHT: any = {
$schema:
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json",
name: "Notesnook Light",
id: "default-light",
version: 2.1,
license: "GPL-3.0-or-later",
homepage: "https://notesnook.com",
description: "The default light theme for Notesnook app.",
colorScheme: "light",
codeBlockCSS: ``,
compatibilityVersion: 1,
authors: [
{
name: "Streetwriters",
email: "support@streetwriters.co",
url: "https://streetwriters.co"
}
],
scopes: {
base: {
primary: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#666666",
background: "#ffffff",
border: "#E2E2E2",
heading: "#181818",
icon: "#181818",
separator: "#F0F0F0",
placeholder: "#858585",
hover: "#eee",
backdrop: "#0000001a",
buttonForeground: "#181818"
},
secondary: {
accent: "#0D9E46",
accentForeground: "#ffffff",
paragraph: "#858585",
background: "#F7F7F7",
border: "#EFEFEF",
heading: "#4B4B4B",
icon: "#666666",
separator: "#E8E8E8",
placeholder: "#999999",
hover: "#dbdbdb",
backdrop: "#0000001a",
buttonForeground: "#181818"
},
tertiary: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#777",
background: "#ECECEC",
border: "#E8E8E8",
heading: "#666",
icon: "#777",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#dbdbdb",
backdrop: "#0000001a"
},
disabled: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#A6A6A6",
background: "#ffffff",
border: "#E8E8E8",
heading: "#212121",
icon: "#DADADA",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#00000010",
backdrop: "#0000001a"
},
selected: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#181818",
background: "#00883610",
border: "#008836",
heading: "#212121",
icon: "#181818",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#eee",
backdrop: "#0000001a"
},
error: {
accent: "#f54b42",
accentForeground: "#ffffff",
paragraph: "#FF242E",
background: "#FFEDEE",
border: "#F7999D",
heading: "#f54b42",
icon: "#FB2C36",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#00000010",
backdrop: "#0000001a"
},
success: {
accent: "#4F8A10",
accentForeground: "#ffffff",
paragraph: "#4F8A10",
background: "#85ce68",
border: "#E8E8E8",
heading: "#4F8A10",
icon: "#4F8A10",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#00000010",
backdrop: "#0000001a"
}
},
statusBar: {
primary: { paragraph: "#5E5E5E", background: "#f7f7f7" }
},
navigationMenu: {
primary: {
background: "#f7f7f7",
paragraph: "#666",
icon: "#666",
hover: "#eee"
},
secondary: { background: "#EEEEEE" },
selected: {
background: "#00883610",
hover: "#eee"
}
},
contextMenu: { primary: { background: "#f7f7f7" } },
editor: { selected: { background: "#e1e1e1" } },
sheet: { selected: { paragraph: "#008836" } },
list: {
secondary: {
heading: "#A6A6A6"
}
}
}
};
export const THEME_DARK: any = {
name: "Notesnook Dark",
id: "default-dark",
version: 2.1,
license: "GPL-3.0-or-later",
homepage: "https://notesnook.com",
description: "The default dark theme for the Notesnook app",
colorScheme: "dark",
codeBlockCSS: ``,
compatibilityVersion: 1,
authors: [
{
name: "Streetwriters",
email: "support@streetwriters.co",
url: "https://streetwriters.co"
}
],
scopes: {
base: {
primary: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#F2F2F2",
background: "#181818",
border: "#2A2A2A",
heading: "#FFFFFF",
icon: "#FFFFFF",
separator: "#383838",
placeholder: "#404040",
hover: "#2b2b2b",
backdrop: "#35353580",
buttonForeground: "#FFFFFF"
},
secondary: {
accent: "#0D9E46",
accentForeground: "#ffffff",
paragraph: "#A1A1A1",
background: "#252525",
border: "#0A5729",
heading: "#C0BFBF",
icon: "#808080",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580",
buttonForeground: "#BFBFBF"
},
tertiary: {
accent: "#0D9E46",
accentForeground: "#ffffff",
paragraph: "#818589",
background: "#2C2C2C",
border: "#2b2b2b",
heading: "#808080",
icon: "#808080",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
disabled: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#505050",
background: "#ffffff",
border: "#383838",
heading: "#212121",
icon: "#FFFFFF",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
selected: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#FBFBFB",
background: "#494949",
border: "#008836",
heading: "#e3e3e3",
icon: "#008836",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
error: {
accent: "#f54b42",
accentForeground: "#ffffff",
paragraph: "#f54b42",
background: "#250b0a",
border: "#F7999D",
heading: "#f54b42",
icon: "#f54b42",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
success: {
accent: "#4F8A10",
accentForeground: "#ffffff",
paragraph: "#4F8A10",
background: "#132204",
border: "#383838",
heading: "#4F8A10",
icon: "#4F8A10",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
}
},
list: { selected: { background: "#202020" } },
contextMenu: { primary: { background: "#202020" } },
sheet: { selected: { paragraph: "#008836" } }
},
$schema:
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json"
};

View File

@@ -55,7 +55,6 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view
import { strings } from "@notesnook/intl";
import { editorController } from "../../screens/editor/tiptap/utils";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
const getUser = () => {
const user = MMKV.getString("user");
@@ -97,6 +96,9 @@ const AppLocked = () => {
const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false);
const { height } = useWindowDimensions();
const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType
);
const appLockHasPasswordSecurity = useSettingStore(
(state) => state.settings.appLockHasPasswordSecurity
);
@@ -155,7 +157,8 @@ const AppLocked = () => {
if (!appLockHasPasswordSecurity) {
await setAppLockVerificationCipher(password.current);
SettingsService.set({
appLockHasPasswordSecurity: true
appLockHasPasswordSecurity: true,
applockKeyboardType: "default"
});
DatabaseLogger.info("App lock migrated to password security");
}
@@ -164,7 +167,7 @@ const AppLocked = () => {
password.current = undefined;
} else {
ToastManager.show({
heading: strings.invalid("password"),
heading: strings.invalid(keyboardType),
type: "error",
context: "local"
});
@@ -287,7 +290,9 @@ const AppLocked = () => {
<Input
fwdRef={passwordInputRef}
secureTextEntry
keyboardType={"default"}
keyboardType={
appLockHasPasswordSecurity ? keyboardType : "default"
}
onLayout={async () => {
if (
!biometricsAuthEnabled ||
@@ -298,7 +303,13 @@ const AppLocked = () => {
}, 32);
}
}}
placeholder={PASSWORD_PLACEHOLDER}
placeholder={
appLockHasPasswordSecurity
? keyboardType === "numeric"
? strings.enterApplockPassword()
: strings.enterApplockPin()
: strings.enterAccountPassword()
}
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { formatBytes } from "@notesnook/common";
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { RefObject, useEffect, useState } from "react";
@@ -27,7 +26,6 @@ import { TextInput, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
import { Radius, Spacing } from "../../common/design/spacing";
import filesystem from "../../common/filesystem";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
@@ -39,7 +37,6 @@ import {
eSendEvent,
presentSheet
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import {
@@ -49,19 +46,20 @@ import {
eOnLoadNote
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import SheetProvider from "../sheet-provider";
import AppIcon from "../ui/AppIcon";
import { createFormRef, validators } from "../ui/input/form-input";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import LineSeparator from "../ui/seperator/line-separator";
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";
import { createFormRef, validators } from "../ui/input/form-input";
const Actions = ({
attachment,
@@ -96,7 +94,7 @@ const Actions = ({
downloadAttachment(attachment.hash, context === "global");
fwdRef.current?.hide();
},
icon: "download-simple"
icon: "download"
},
{
name: strings.network.reupload(),
@@ -116,7 +114,7 @@ const Actions = ({
type: attachment.mimeType.startsWith("image") ? "image" : "file"
});
},
icon: "upload-simple"
icon: "upload"
},
{
name: strings.fileCheck(),
@@ -151,7 +149,7 @@ const Actions = ({
name: undefined
});
},
icon: "file"
icon: "file-check"
},
{
name: strings.rename(),
@@ -199,7 +197,7 @@ const Actions = ({
});
}, 500);
},
icon: "pencil-simple"
icon: "form-textbox"
},
{
name: strings.delete(),
@@ -251,7 +249,7 @@ const Actions = ({
});
}, 500);
},
icon: "trash"
icon: "delete-outline"
}
];
@@ -269,7 +267,6 @@ const Actions = ({
onMomentumScrollEnd={() => {
fwdRef?.current?.handleChildScrollEnd();
}}
bounces={false}
nestedScrollEnabled={true}
style={{
maxHeight: "100%"
@@ -280,34 +277,42 @@ const Actions = ({
<SheetProvider context={contextId} />
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_4,
gap: Spacing.LEVEL_1
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
marginBottom:
notes && notes.length > 0 ? 0 : DefaultAppStyles.GAP_VERTICAL
}}
>
<Heading fontSize="LG">{filename}</Heading>
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
size={AppFontSize.lg}
>
{filename}
</Heading>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1,
alignItems: "center"
marginBottom: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
gap: 10
}}
>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{attachment.mimeType}
</Paragraph>
<Paragraph fontSize="XS"></Paragraph>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{formatBytes(attachment.size)}
</Paragraph>
<Paragraph fontSize="XS"></Paragraph>
{notes.length ? (
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.notes(notes.length)}
</Paragraph>
) : null}
<Paragraph fontSize="XS"></Paragraph>
<Paragraph
onPress={() => {
Clipboard.setString(attachment.hash);
@@ -317,123 +322,82 @@ const Actions = ({
context: "local"
});
}}
fontSize="XS"
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
<AppIcon
name="copy"
size={10}
color={colors.secondary.paragraph}
iconFamily="notesnook"
/>{" "}
{attachment.hash}
</Paragraph>
</View>
<LineSeparator paddingVertical={Spacing.LEVEL_2} />
<DateMeta item={attachment} />
</View>
{notes && notes.length > 0 ? (
<>
<LineSeparator
paddingHorizontal={Spacing.LEVEL_3}
paddingVertical={Spacing.LEVEL_3}
/>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_2
}}
>
<>
<Heading size={AppFontSize.sm}>
{strings.notes(notes.length)}
</Heading>
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<>
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
size={AppFontSize.sm}
>
{strings.listOf()} {strings.dataTypesPlural.note()}:
</Heading>
{notes.map((item) => (
<Pressable
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
close?.();
eSendEvent(eCloseAttachmentDialog);
Navigation.navigate("FluidPanelsView");
openNote(item, (item as any).type === "trash");
}}
style={{
alignItems: "flex-start"
}}
key={item.id}
>
<Paragraph fontSize="XS">{item.title}</Paragraph>
</Pressable>
))}
</>
</View>
</>
{notes.map((item) => (
<Pressable
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
close?.();
eSendEvent(eCloseAttachmentDialog);
Navigation.navigate("FluidPanelsView");
openNote(item, (item as any).type === "trash");
}}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
alignItems: "flex-start",
paddingHorizontal: DefaultAppStyles.GAP
}}
key={item.id}
>
<Paragraph size={AppFontSize.xs}>{item.title}</Paragraph>
</Pressable>
))}
</>
</View>
) : null}
<LineSeparator
paddingHorizontal={Spacing.LEVEL_3}
paddingVertical={Spacing.LEVEL_3}
/>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
{actions.map((item) => (
<Pressable
key={item.name}
onPress={item.onPress}
type="plain"
style={{
borderRadius: 0,
flexDirection: "row",
width: "100%",
gap: Spacing.LEVEL_1,
justifyContent: "flex-start",
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
width: 32,
height: 32,
backgroundColor: colors.secondary.background,
borderRadius: Radius.XS,
justifyContent: "center",
alignItems: "center"
}}
>
<AppIcon
color={
item.icon === "trash"
? colors.error.accent
: colors.primary.icon
}
name={item.icon}
iconFamily="notesnook"
size={16}
/>
</View>
<Paragraph
color={
item.icon === "trash"
? colors.error.accent
: colors.primary.heading
}
fontSize="SM"
fontFamily="MEDIUM"
>
{item.name}
</Paragraph>
</Pressable>
))}
</View>
{actions.map((item) => (
<Button
key={item.name}
buttonType={{
text:
item.name === "Delete"
? colors.error.paragraph
: colors.primary.paragraph
}}
onPress={item.onPress}
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type="plain"
fontSize={AppFontSize.sm}
style={{
borderRadius: 0,
justifyContent: "flex-start",
alignSelf: "flex-start",
width: "100%"
}}
/>
))}
<View
style={{

View File

@@ -17,29 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes, getFormattedDate } from "@notesnook/common";
import { formatBytes } from "@notesnook/common";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { Radius, Spacing } from "../../common/design/spacing";
import { TouchableOpacity, View } from "react-native";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
import AppIcon from "../ui/AppIcon";
import { Pressable } from "../ui/pressable";
import { ProgressBarComponent } from "../ui/svg/lazy";
import Heading from "../ui/typography/heading";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { IconButton } from "../ui/icon-button";
import { strings } from "@notesnook/intl";
import { Pressable } from "../ui/pressable";
import { DefaultAppStyles } from "../../utils/styles";
function getAttachmentIcon(attachment: Attachment) {
const mimeType = attachment.mimeType || "";
if (mimeType.startsWith("image/")) return "image";
if (mimeType.startsWith("video/")) return "video-camera";
if (mimeType.startsWith("audio/")) return "music-notes";
return "file-text";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
@@ -78,15 +75,11 @@ export const AttachmentItem = ({
onPress={onPress}
style={{
flexDirection: "row",
alignItems: "center",
marginVertical: 5,
justifyContent: "space-between",
gap: Spacing.LEVEL_2,
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: Radius.S,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_2
padding: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
minHeight: 45
}}
>
{!attachment ? null : (
@@ -96,112 +89,107 @@ export const AttachmentItem = ({
flexShrink: 1,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_2
gap: 10
}}
>
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
backgroundColor: colors.secondary.background,
justifyContent: "center",
alignItems: "center"
alignItems: "center",
marginLeft: -5,
borderWidth: 1,
borderColor: colors.secondary.border,
paddingHorizontal: 2,
width: 30,
height: 30,
borderRadius: defaultBorderRadius
}}
>
<AppIcon
name={getAttachmentIcon(attachment)}
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
<Paragraph
adjustsFontSizeToFit
size={8}
color={colors.secondary.paragraph}
style={{
maxWidth: 30
}}
numberOfLines={1}
>
{getFileExtension(attachment.filename).toUpperCase() ||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1,
width: "100%",
gap: Spacing.LEVEL_1
flexShrink: 1
}}
>
<Heading
fontSize="MD"
lineBreakMode="middle"
color={colors.primary.heading}
>
book.pdf
</Heading>
<View
<Paragraph
size={AppFontSize.sm}
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
flexWrap: "wrap"
}}
numberOfLines={1}
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.failed ? (
<AppIcon
size={13}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
{!hideWhenNotDownloading ? (
<Paragraph
fontSize="XS"
numberOfLines={1}
color={colors.secondary.paragraph}
>
{getFormattedDate(attachment.dateModified)} |{" "}
{formatBytes(attachment.size)}
</Paragraph>
) : null}
</View>
{attachment.filename}
</Paragraph>
{currentProgress ? (
<View
style={{
width: "100%",
gap: Spacing.LEVEL_0,
flexGrow: 1
}}
{!hideWhenNotDownloading ? (
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
>
<ProgressBarComponent
height={6}
width={null}
animated={true}
useNativeDriver
progress={currentProgress.value}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
<Paragraph color={colors.secondary.paragraph} fontSize="XS">
{currentProgress.percent} |{" "}
{currentProgress.type === "upload"
? strings.uploading()
: strings.downloading()}
</Paragraph>
</View>
{strings.fileSize()}: {formatBytes(attachment.size)}
</Paragraph>
) : null}
</View>
</View>
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs().cancel(attachment.hash);
setCurrentProgress(undefined);
}}
style={{
justifyContent: "center",
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<ProgressCircleComponent
size={AppFontSize.xxl}
progress={
currentProgress?.value ? currentProgress?.value / 100 : 0
}
showsText
textStyle={{
fontSize: 10
}}
color={colors.primary.accent}
formatText={(progress) => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton
onPress={onPress}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
</>
)}
</>
)}
<IconButton
color={colors.secondary.icon}
name="dots-three"
iconFamily="notesnook"
size={20}
onPress={onPress}
style={{
justifyContent: "center",
height: undefined,
width: undefined,
borderRadius: 100,
alignItems: "center"
}}
/>
</Pressable>
);
};

View File

@@ -27,32 +27,32 @@ import {
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, FlatList, ScrollView, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
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";
import { db } from "../../common/database";
import { FontSizes } from "../../common/design/font";
import { Radius, Spacing } from "../../common/design/spacing";
import filesystem from "../../common/filesystem";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { AttachmentItem } from "../../components/attachments/attachment-item";
import { presentDialog } from "../../components/dialog/functions";
import { Header } from "../../components/header";
import AppIcon from "../../components/ui/AppIcon";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import LineSeparator from "../../components/ui/seperator/line-separator";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager } from "../../services/event-manager";
import { NavigationProps } from "../../services/navigation";
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { AttachmentGroupProgress } from "../settings/components/attachment-group-progress";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { Header } from "../header";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateModified",
sortBy: "dateEdited",
sortDirection: "desc"
};
@@ -124,13 +124,17 @@ const attachmentTypes = [
}
];
const Attachments = (props: NavigationProps<"Attachments">) => {
const note = props.route.params?.note;
export const AttachmentDialog = ({
note,
isSheet
}: {
note?: Note;
isSheet: boolean;
}) => {
const { colors } = useThemeColors();
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>(undefined);
const [searchVisible, setSearchVisible] = useState(false);
const [loading, setLoading] = useState(true);
const searchTimer = useRef<NodeJS.Timeout>(undefined);
const [currentFilter, setCurrentFilter] = useState("all");
@@ -150,16 +154,24 @@ const Attachments = (props: NavigationProps<"Attachments">) => {
if (note) {
db.attachments
.ofNote(note.id, "all")
.sorted(DEFAULT_SORTING)
.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => {
setLoading(false);
setAttachments(attachments);
});
} else {
db.attachments.all.sorted(DEFAULT_SORTING).then((attachments) => {
setAttachments(attachments);
setLoading(false);
});
db.attachments.all
.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => {
setAttachments(attachments);
setLoading(false);
});
}
}, [note]);
@@ -194,7 +206,7 @@ const Attachments = (props: NavigationProps<"Attachments">) => {
errorOnly={currentFilter === "errors"}
attachments={attachments}
id={index}
context="global"
context={!isSheet ? "global" : "attachments-list"}
/>
);
@@ -225,7 +237,7 @@ const Attachments = (props: NavigationProps<"Attachments">) => {
ToastManager.show({
message: strings.attachmentRecheckCancelled(),
type: "info",
context: "global"
context: isSheet ? "local" : "global"
});
return;
}
@@ -271,7 +283,7 @@ const Attachments = (props: NavigationProps<"Attachments">) => {
: db.attachments.images;
break;
case "video":
items = note
items = items = note
? db.attachments.ofNote(note.id, "videos")
: db.attachments.videos;
break;
@@ -289,99 +301,198 @@ const Attachments = (props: NavigationProps<"Attachments">) => {
items = db.attachments.orphaned;
break;
case "errors":
items = note
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.all;
}
return await items.sorted(DEFAULT_SORTING);
};
const downloadAll = () => {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
return await items.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
});
};
const currentType = attachmentTypes.find(
(item) => item.filterBy === currentFilter
);
const sectionTitle =
currentFilter === "all"
? strings.allAttachments()
: currentType?.title || strings.allAttachments();
db.attachments.orphaned.items().then((r) => {
console.log(r);
});
return (
<SafeAreaView
style={{ flex: 1, backgroundColor: colors.primary.background }}
>
<Header
renderedInRoute="Attachments"
id="Attachments"
title={strings.dataTypesPluralCamelCase.attachment()}
canGoBack
style={{
backgroundColor: colors.primary.background
}}
/>
<>
{isSheet ? (
<>
<SheetProvider context="attachments-list" />
<Dialog context="local" />
</>
) : null}
{!isSheet ? (
<Header
title={strings.manageAttachments()}
renderedInRoute="SettingsGroup"
canGoBack
/>
) : (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
name="check-all"
style={{
marginRight: 10
}}
color={colors.primary.paragraph}
size={AppFontSize.lg}
onPress={onCheck}
/>
<IconButton
name="download"
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
});
}}
size={AppFontSize.lg}
/>
</View>
</View>
)}
<View
style={{
flex: 1,
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_3
width: "100%",
alignSelf: "center",
paddingHorizontal: DefaultAppStyles.GAP,
height: "100%"
}}
>
<Seperator />
<Input
placeholder={strings.filterAttachments()}
onChangeText={onChangeText}
containerStyle={{
borderWidth: 0,
backgroundColor: colors.secondary.background
}}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
}}
button={{
icon: "search",
iconFamily: "notesnook",
color: colors.primary.icon,
size: 16,
onPress: () => {}
}}
/>
<LineSeparator
paddingHorizontal={Spacing.LEVEL_3}
style={{
marginBottom: -Spacing.LEVEL_1
}}
/>
{rechecker.shown ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
borderRadius: 10,
padding: DefaultAppStyles.GAP_SMALL,
borderWidth: 1,
borderColor: colors.primary.border,
gap: 12,
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
gap: 12,
alignItems: "center"
}}
>
{rechecker.isWorking ? (
<ActivityIndicator color={colors.primary.accent} />
) : (
<Icon
name="file-check"
size={30}
color={colors.primary.accent}
/>
)}
<View>
<Paragraph>
{rechecker.isWorking
? note
? strings.checkingNoteAttachments()
: strings.checkingAllAttachments()
: strings.attachmentRecheckComplete()}
</Paragraph>
<Paragraph>
{`${
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
}${strings.passed()}: ${
rechecker.passed
}, ${strings.failed()}: ${rechecker.failed}`}
</Paragraph>
</View>
</View>
<IconButton
type={rechecker.isWorking ? "errorShade" : "plain"}
name={rechecker.isWorking ? "close" : "check"}
size={AppFontSize.lg}
color={
rechecker.isWorking ? colors.error.icon : colors.primary.accent
}
onPress={() => {
useRechecker.getState().setProgress(note?.id || "all", {
...useRechecker.getState().progress[note?.id || "all"],
shown: false,
isWorking: false
});
}}
/>
</View>
) : null}
<View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
gap: Spacing.LEVEL_1
style={{
backgroundColor: colors.primary.background,
flexWrap: "wrap",
flexDirection: "row",
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
contentContainerStyle={{
alignItems: "center"
}}
horizontal
>
{attachmentTypes.map((item) =>
item.filterBy === "orphaned" && note ? null : (
<Pressable
key={item.filterBy}
<Button
type={
currentFilter === item.filterBy ? "selected" : "transparent"
currentFilter === item.filterBy
? "secondaryAccented"
: "plain"
}
onPress={() => {
key={item.title}
title={item.title}
fontSize={AppFontSize.sm}
style={{
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP,
height: 40,
minWidth: 80
}}
onPress={async () => {
const filterBy = item.filterBy;
setCurrentFilter(filterBy);
setLoading(true);
@@ -395,168 +506,52 @@ const Attachments = (props: NavigationProps<"Attachments">) => {
/* empty */
});
}}
style={{
borderRadius: Radius.XXL,
borderWidth: currentFilter === item.filterBy ? 0 : 1,
borderColor: colors.primary.border,
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_1,
width: "auto"
}}
>
<Paragraph
fontFamily="MEDIUM"
fontSize="SM"
color={
currentFilter === item.filterBy
? colors.primary.heading
: colors.secondary.paragraph
}
>
{item.title}
</Paragraph>
</Pressable>
/>
)
)}
</ScrollView>
</View>
{rechecker.shown ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
borderRadius: Radius.S,
padding: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2,
justifyContent: "space-between",
backgroundColor: colors.primary.shade
}}
>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2,
alignItems: "center",
flexShrink: 1
}}
>
<View
style={{
width: 32,
height: 32,
backgroundColor: colors.secondary.background,
borderRadius: Radius.XS,
justifyContent: "center",
alignItems: "center"
}}
>
<AppIcon
name="check-circle"
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
</View>
<View style={{ flexShrink: 1, gap: Spacing.LEVEL_0 }}>
<Heading color={colors.primary.heading} fontSize="SM">
{rechecker.isWorking
? note
? strings.checkingNoteAttachments()
: strings.checkingAllAttachments()
: strings.attachmentRecheckComplete()}
</Heading>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{`${
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
}${strings.passed()}: ${
rechecker.passed
}, ${strings.failed()}: ${rechecker.failed}`}
</Paragraph>
</View>
</View>
<IconButton
type={rechecker.isWorking ? "errorShade" : "plain"}
name={"close"}
iconFamily="notesnook"
size={12}
color={colors.secondary.icon}
onPress={() => {
useRechecker.getState().setProgress(note?.id || "all", {
...useRechecker.getState().progress[note?.id || "all"],
shown: false,
isWorking: false
});
}}
/>
</View>
) : null}
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between"
}}
>
<Heading fontSize="LG">{sectionTitle}</Heading>
<Button
title={strings.verifyFiles()}
icon="shield-check"
iconFamily="notesnook"
type="accent"
fontSize={FontSizes.XS}
loading={rechecker.isWorking}
style={{
paddingVertical: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_2
}}
onPress={onCheck}
/>
</View>
<FlatList
style={{ flex: 1 }}
renderScrollComponent={(props) => <ScrollView {...props} />}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
ListEmptyComponent={
<View
style={{
height: 250,
height: "100%",
justifyContent: "center",
alignItems: "center",
gap: Spacing.LEVEL_2
alignItems: "center"
}}
>
{loading ? (
<ActivityIndicator size={40} color={colors.primary.accent} />
) : (
<>
<AppIcon
name="paperclip"
iconFamily="notesnook"
size={50}
<Icon
name="attachment"
size={60}
color={colors.secondary.icon}
/>
<Paragraph color={colors.secondary.paragraph}>
{strings.noAttachments()}
</Paragraph>
<Paragraph>{strings.noAttachments()}</Paragraph>
</>
)}
</View>
}
ListHeaderComponent={<AllProgress note={note} />}
ListFooterComponent={<View style={{ height: 350 }} />}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
data={loading ? [] : attachments?.placeholders || []}
extraData={attachments}
renderItem={renderItem}
/>
</View>
</SafeAreaView>
</>
);
};
@@ -577,7 +572,7 @@ const AllProgress = ({ note }: { note?: Note }) => {
return (
<View
style={{
gap: Spacing.LEVEL_2,
gap: 10,
width: "100%"
}}
>
@@ -592,4 +587,9 @@ const AllProgress = ({ note }: { note?: Note }) => {
);
};
export default Attachments;
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} isSheet={true} />,
keyboardHandlerDisabled: true
});
};

View File

@@ -23,9 +23,10 @@ import React, { useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import BackupService from "../../services/backup";
import { ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useUserStore } from "../../stores/use-user-store";
import { eOpenRecoveryKeyDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
@@ -35,8 +36,6 @@ import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import { Notice } from "../ui/notice";
import Paragraph from "../ui/typography/paragraph";
import { TextInput } from "react-native-gesture-handler";
import { Spacing } from "../../common/design/spacing";
import RecoveryKeySheet from "../sheets/recovery-key";
export const ChangePassword = () => {
const { colors } = useThemeColors();
@@ -93,7 +92,7 @@ export const ChangePassword = () => {
});
setLoading(false);
Navigation.goBack();
RecoveryKeySheet.present();
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
const message = (e as Error).message;
setLoading(false);
@@ -110,15 +109,12 @@ export const ChangePassword = () => {
<View
style={{
width: "100%",
paddingTop: Spacing.LEVEL_0,
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_2
padding: DefaultAppStyles.GAP
}}
>
<Dialog context="change-password-dialog" />
<FormInput
name="oldPassword"
label={strings.oldPassword()}
formRef={formRef}
fwdRef={oldPasswordInputRef}
loading={loading}
@@ -129,7 +125,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={"•••••••••"}
placeholder={strings.currentPassword()}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
@@ -137,7 +133,6 @@ export const ChangePassword = () => {
<FormInput
name="password"
label={strings.newPassword()}
formRef={formRef}
fwdRef={passwordInputRef}
loading={loading}
@@ -148,28 +143,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={"•••••••••"}
onSubmitEditing={() => {
changePassword();
}}
/>
<FormInput
name="confirmPassword"
label={strings.confirmPassword()}
formRef={formRef}
fwdRef={passwordInputRef}
loading={loading}
validators={[
validators.matchField("password", strings.confirmPasswordRequired())
]}
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={"•••••••••"}
placeholder={strings.newPassword()}
onSubmitEditing={() => {
changePassword();
}}
@@ -194,14 +168,15 @@ export const ChangePassword = () => {
</Paragraph>
) : null}
<Notice
text={strings.changePasswordNotice()}
type="information"
size="small"
/>
<Notice text={strings.changePasswordNotice()} type="alert" />
<View style={{ height: 10 }} />
<Notice text={strings.changePasswordNotice2()} type="alert" />
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
loading={loading}
@@ -209,23 +184,6 @@ export const ChangePassword = () => {
type="accent"
title={loading ? null : strings.changePasswordConfirm()}
/>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1,
alignSelf: "center",
marginTop: Spacing.LEVEL_0
}}
>
<AppIcon
size={15}
name="shield-check"
iconFamily="notesnook"
color={colors.secondary.icon}
/>
<Paragraph>{strings.yourSecurityIsPriority()}</Paragraph>
</View>
</View>
);
};

View File

@@ -24,9 +24,6 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { hideAuth } from "./common";
import { AuthParams } from "../../stores/use-navigation-store";
import { ProgressPills } from "../intro/progress-pills";
import { FontSizes } from "../../common/design/font";
import { Spacing } from "../../common/design/spacing";
export const AuthHeader = (props: { welcome?: boolean }) => {
const { colors } = useThemeColors();
const route = useRoute();
@@ -40,22 +37,19 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
<View
style={{
flexDirection: "row",
paddingHorizontal: Spacing.LEVEL_3,
alignItems: "center",
paddingHorizontal: 12,
width: "100%",
justifyContent: "space-between",
alignItems: "center"
height: 50,
justifyContent: !props.welcome ? "space-between" : "flex-end"
}}
>
{props.welcome ? (
<ProgressPills activePillIndex={1} />
) : (
{props.welcome ? null : (
<IconButton
name="arrow-back"
iconFamily="notesnook"
name="arrow-left"
onPress={() => {
hideAuth((route.params as AuthParams)?.context);
}}
size={24}
color={colors.primary.paragraph}
/>
)}
@@ -66,16 +60,16 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
onPress={() => {
hideAuth();
}}
iconSize={24}
fontSize={FontSizes.SM}
iconSize={16}
type="plain"
fontFamily="REGULAR"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingRight: 0,
paddingHorizontal: 0,
paddingVertical: 0
paddingHorizontal: 6
}}
/>
)}

View File

@@ -17,15 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { NavigationProps } from "../../services/navigation";
import { Toast } from "../toast";
import { AuthMode, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { useThemeColors } from "@notesnook/theme";
import { NavigationProps } from "../../services/navigation";
const Auth = ({ navigation, route }: NavigationProps<"Auth">) => {
const [currentAuthMode, setCurrentAuthMode] = useState(

View File

@@ -21,10 +21,8 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { Spacing } from "../../common/design/spacing";
import { DDS } from "../../services/device-detection";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Navigation from "../../services/navigation";
@@ -35,20 +33,19 @@ import { RouteParams } from "../../stores/use-navigation-store";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { ProgressPills } from "../intro/progress-pills";
import { Dialog } from "../dialog";
import { Progress } from "../sheets/progress";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import FormInput, { validators } from "../ui/input/form-input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { AuthHeader } from "./header";
import TwoFactorVerification from "./two-factor";
import { useLogin } from "./use-login";
import FormInput, { validators } from "../ui/input/form-input";
import AppIcon from "../ui/AppIcon";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
const LoginSteps = {
emailAuth: 1,
@@ -71,9 +68,8 @@ export const Login = ({
passwordInputRef,
loading,
setLoading,
error,
login,
mfaData,
error,
formRef
} = useLogin(async () => {
eSendEvent(eUserLoggedIn, true);
@@ -95,6 +91,8 @@ export const Login = ({
Progress.present();
}
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const onContinue = () => {
login();
@@ -116,257 +114,249 @@ export const Login = ({
return (
<>
<AuthHeader />
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
marginTop: Spacing.LEVEL_4
}}
>
<ProgressPills
count={2}
activePillIndex={step === LoginSteps.emailAuth ? 0 : 1}
/>
</View>
<Dialog context="two_factor_verify" />
<KeyboardAwareScrollView
style={{
width: "100%"
}}
contentContainerStyle={{
minHeight: "99%"
minHeight: "90%"
}}
nestedScrollEnabled
enableAutomaticScroll={true}
keyboardShouldPersistTaps="handled"
>
{step === LoginSteps.emailAuth || step === LoginSteps.passwordAuth ? (
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
height: "100%",
alignSelf: "center"
}}
>
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_6
justifyContent: "flex-end",
paddingHorizontal: DefaultAppStyles.GAP,
borderBottomWidth: 0.8,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : undefined,
borderColor: isTablet ? colors.primary.border : undefined,
borderRadius: isTablet ? 20 : undefined,
marginTop: isTablet ? 50 : undefined,
width: !isTablet ? undefined : "50%",
minHeight: height * 0.4
}}
>
<Heading
style={{
paddingBottom: Spacing.LEVEL_4
}}
fontSize="XL"
>
{strings.loginToYourAccount()}
</Heading>
<View
style={{
width: DDS.isTab
? focused
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
gap: Spacing.LEVEL_2
flexDirection: "row"
}}
>
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
testID="input.email"
label={strings.email()}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
keyboardType="email-address"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
placeholder={strings.email()}
editable={step === LoginSteps.emailAuth && !loading}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
/>
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
label={strings.password()}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={PASSWORD_PLACEHOLDER}
marginBottom={0}
editable={!loading}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onContinue();
}}
/>
<Button
title={strings.forgotPassword()}
<View
style={{
alignSelf: "flex-end",
paddingVertical: 0,
paddingHorizontal: 0
}}
onPress={() => {
if (loading || !formRef.current.getValue("email")) return;
presentSheet({
component: (
<ForgotPassword
userEmail={formRef.current.getValue("email")}
/>
)
});
}}
fontFamily="REGULAR"
fontSize={AppFontSize.sm}
type="plain"
textStyle={{
color: colors.secondary.paragraph
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
marginTop: Spacing.LEVEL_1,
gap: Spacing.LEVEL_2
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
>
<Button
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
style={{
width: "100%"
}}
type="accent"
title={!loading ? strings.continue() : null}
fontSize={AppFontSize.sm}
/>
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
paddingVertical: 0
}}
>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.dontHaveAccount()}{" "}
<Paragraph
fontSize="SM"
style={{ color: colors.primary.accent }}
fontFamily="SEMI_BOLD"
>
{strings.signUp()}
</Paragraph>
</Paragraph>
</TouchableOpacity>
) : null}
{error ? (
<Paragraph
numberOfLines={4}
onPress={() => {}}
color={colors.error.accent}
style={{
textAlign: "center",
marginTop: Spacing.LEVEL_2
}}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{error.message}
</Paragraph>
) : null}
</View>
/>
</View>
<Heading
style={{
marginBottom: 25,
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
extraBold
size={AppFontSize.xxl}
>
{strings.loginToYourAccount()}
</Heading>
</View>
) : (
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
width: DDS.isTab
? focused
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_6
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<TwoFactorVerification
onMfaLogin={async (
mfa: any,
callback: (success: boolean) => void,
onerror: (error: Error) => void
) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
await login();
if (success) {
setLoading(false);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if ((e as Error).message === "invalid_grant") {
setLoading(false);
setStep(LoginSteps.emailAuth);
} else {
onerror(e as Error);
}
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
testID="input.email"
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
keyboardType="email-address"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
placeholder={strings.email()}
editable={step === LoginSteps.emailAuth && !loading}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
if (step === LoginSteps.emailAuth) {
onContinue();
} else {
passwordInputRef.current?.focus();
}
}}
mfaInfo={
mfaData.current || {
primaryMethod: "email",
secondaryMethod: "sms",
token: ""
}
}
onCancel={() => {
setLoading(false);
setStep(LoginSteps.emailAuth);
}}
/>
{step === LoginSteps.passwordAuth && (
<>
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onContinue();
}}
/>
<Button
title={strings.forgotPassword()}
style={{
alignSelf: "flex-end",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
presentSheet({
component: (
<ForgotPassword
userEmail={formRef.current.getValue("email")}
/>
)
});
}}
textStyle={{
textDecorationLine: "underline"
}}
fontSize={AppFontSize.xs}
type="plain"
/>
</>
)}
<View>
<Button
loading={loading}
onPress={() => {
onContinue();
}}
style={{
width: "100%"
}}
type="accent"
title={!loading ? strings.continue() : null}
fontSize={AppFontSize.sm}
/>
{step === LoginSteps.passwordAuth && (
<Button
title={strings.cancelLogin()}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
onPress={() => {
if (loading) return;
setStep(LoginSteps.emailAuth);
setLoading(false);
}}
type="secondaryAccented"
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{strings.dontHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs}
style={{ color: colors.primary.accent }}
>
{strings.signUp()}
</Paragraph>
</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>
</KeyboardAwareScrollView>
</>
);

View File

@@ -45,12 +45,11 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useLogin } from "./use-login";
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";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
export const SessionExpired = () => {
const { colors } = useThemeColors();
@@ -196,22 +195,24 @@ export const SessionExpired = () => {
</Paragraph>
</View>
<FormInput
fwdRef={passwordInputRef}
formRef={formRef}
name="password"
validators={[validators.required(strings.passwordRequired())]}
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={PASSWORD_PLACEHOLDER}
onSubmitEditing={() => {
login();
}}
/>
{step === LoginSteps.passwordAuth ? (
<FormInput
fwdRef={passwordInputRef}
formRef={formRef}
name="password"
validators={[validators.required(strings.passwordRequired())]}
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
onSubmitEditing={() => {
login();
}}
/>
) : null}
<Button
style={{

View File

@@ -21,7 +21,12 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import React, { useRef, useState } from "react";
import { TextInput, TouchableOpacity, View } from "react-native";
import {
TextInput,
TouchableOpacity,
View,
useWindowDimensions
} from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
@@ -41,29 +46,6 @@ import { SignupContext } from "./signup-context";
import { RouteParams } from "../../stores/use-navigation-store";
import SettingsService from "../../services/settings";
import AppIcon from "../ui/AppIcon";
import { Spacing } from "../../common/design/spacing";
import { SETTING_ACCOUNT_SVG } from "../../assets/images/assets";
import { ProgressPills } from "../intro/progress-pills";
import { sleep } from "../../utils/time";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
const getSettingAccountSvg = (dark: boolean) => {
if (dark) return SETTING_ACCOUNT_SVG;
return SETTING_ACCOUNT_SVG.replace('stroke="#1F2722"', 'stroke="#E8F4ED"')
.replace(
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
'fill="#008836" fill-opacity="0.04" stroke="#E3F1E8"'
)
.replace(
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
'fill="#008836" fill-opacity="0.05" stroke="#E8F0EC"'
)
.replace(
'fill="#008836" fill-opacity="0.08" stroke="#233C2D"',
'fill="#008836" fill-opacity="0.06" stroke="#D3E8DB"'
);
};
const SignupSteps = {
signup: 0,
@@ -79,7 +61,7 @@ export const Signup = ({
welcome: boolean;
}) => {
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
const { colors, isDark } = useThemeColors();
const { colors } = useThemeColors();
const formRef = useRef(
createFormRef({
email: "",
@@ -94,6 +76,8 @@ export const Signup = ({
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const route = useRoute<RouteProp<RouteParams, "Auth">>();
const signup = async () => {
@@ -105,9 +89,8 @@ export const Signup = ({
setLoading(true);
try {
await db.user.signup(values.email.toLowerCase(), values.password);
await sleep(1000);
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(values.email.toLowerCase(), values.password);
const user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
@@ -150,9 +133,8 @@ export const Signup = ({
width: "100%"
}}
contentContainerStyle={{
flex: 1
minHeight: "90%"
}}
bounces={false}
nestedScrollEnabled
keyboardShouldPersistTaps="handled"
>
@@ -163,24 +145,68 @@ export const Signup = ({
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_6
height: "100%"
}}
>
<Heading
<View
style={{
paddingBottom: Spacing.LEVEL_4
justifyContent: "flex-end",
paddingHorizontal: 16,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : undefined,
borderColor: isTablet ? colors.primary.border : undefined,
borderRadius: isTablet ? 20 : undefined,
marginTop: isTablet ? 50 : undefined,
width: !isTablet ? undefined : "50%",
minHeight: height * 0.25
}}
fontSize="XL"
>
{strings.createAccount()}
</Heading>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={AppFontSize.xxl}
>
{strings.createAccount()}
</Heading>
</View>
<View
style={{
gap: Spacing.LEVEL_2,
paddingBottom: Spacing.LEVEL_4
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: DDS.isTab ? 0 : 16,
backgroundColor: colors.primary.background,
flexGrow: 1,
alignSelf: "center"
}}
>
<FormInput
@@ -188,7 +214,6 @@ export const Signup = ({
formRef={formRef}
fwdRef={emailInputRef}
loading={loading}
label={strings.email()}
testID="input.email"
returnKeyLabel="Next"
returnKeyType="next"
@@ -196,7 +221,7 @@ export const Signup = ({
keyboardType="email-address"
autoCorrect={false}
autoCapitalize="none"
placeholder="you@example.com"
placeholder={strings.email()}
blurOnSubmit={false}
validators={[
validators.required(strings.emailRequired()),
@@ -220,11 +245,9 @@ export const Signup = ({
autoCapitalize="none"
blurOnSubmit={false}
autoCorrect={false}
label={strings.password()}
placeholder={PASSWORD_PLACEHOLDER}
placeholder={strings.password()}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
if (formRef.current.validateField("password")) return;
confirmPasswordInputRef.current?.focus();
}}
/>
@@ -242,8 +265,8 @@ export const Signup = ({
autoCapitalize="none"
autoCorrect={false}
blurOnSubmit={false}
label={strings.confirmPassword()}
placeholder={PASSWORD_PLACEHOLDER}
placeholder={strings.confirmPassword()}
marginBottom={12}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField(
@@ -255,17 +278,7 @@ export const Signup = ({
signup();
}}
/>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
backgroundColor: colors.primary.background,
flexGrow: 1,
alignSelf: "center",
gap: Spacing.LEVEL_2
}}
>
<Button
title={!loading ? strings.continue() : null}
type="accent"
@@ -283,21 +296,18 @@ export const Signup = ({
}}
activeOpacity={0.8}
style={{
alignSelf: "center"
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph
style={{
marginBottom: 25,
textAlign: "center"
}}
fontSize="SM"
color={colors.primary.paragraph}
size={AppFontSize.xs + 1}
color={colors.secondary.paragraph}
>
{strings.alreadyHaveAccount()}{" "}
<Paragraph
fontSize="SM"
fontFamily="SEMI_BOLD"
size={AppFontSize.xs + 1}
style={{ color: colors.primary.accent }}
>
{strings.login()}
@@ -337,16 +347,18 @@ export const Signup = ({
marginBottom: 25,
textAlign: "center"
}}
fontSize="XS"
size={AppFontSize.xxs}
color={colors.secondary.paragraph}
>
{strings.signupAgreement[0]()}
<Paragraph
fontSize="XS"
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
}}
fontFamily="SEMI_BOLD"
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
{" "}
@@ -354,38 +366,31 @@ export const Signup = ({
</Paragraph>{" "}
{strings.signupAgreement[2]()}
<Paragraph
fontSize="XS"
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
}}
fontFamily="SEMI_BOLD"
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
{" "}
{strings.signupAgreement[3]()}
</Paragraph>
</Paragraph>{" "}
{strings.signupAgreement[4]()}
</Paragraph>
</View>
</View>
</KeyboardAwareScrollView>
</>
) : (
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
{welcome ? <ProgressPills activePillIndex={2} /> : null}
<>
<Loading
title={strings.settingUpYourAccount()}
svgSrc={getSettingAccountSvg(isDark)}
description={strings.accountAlmostReady()}
style={{
height: undefined,
marginTop: Spacing.LEVEL_6
}}
title={"Setting up your account..."}
description="Your account is almost ready, please wait..."
/>
</View>
</>
)}
</SignupContext.Provider>
);

View File

@@ -20,18 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TextInput, TouchableOpacity, View } from "react-native";
import { TextInput, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../common/database/index";
import { Radius, Spacing } from "../../common/design/spacing";
import useTimer from "../../hooks/use-timer";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
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 PinInput from "../ui/pin-input/index";
import Input from "../ui/input";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -72,19 +73,11 @@ const TwoFactorVerification = ({
const inputRef = useRef<TextInput>(null);
const [sending, setSending] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [codeInput, setCodeInput] = useState("");
const isRecoveryCode = currentMethod.method === "recoveryCode";
const codeLength = isRecoveryCode ? 10 : 6;
const onNext = async () => {
if (!code.current || code.current.length < 6) {
setError(
new Error(
currentMethod.method === "recoveryCode"
? strings.provideValidMultiFactorAuthRecoveryCode()
: strings.provideValidMultiFactorAuthCode()
)
new Error("Please provide a valid multi-factor authentication code.")
);
return;
}
@@ -101,7 +94,10 @@ const TwoFactorVerification = ({
method: currentMethod.method,
code: code.current
},
() => {
(result) => {
if (result) {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
}
setLoading(false);
},
(e) => {
@@ -122,14 +118,12 @@ const TwoFactorVerification = ({
{
id: "sms",
title: strings.sendCodeSms(),
icon: "chat",
iconFamily: "notesnook"
icon: "message-plus-outline"
},
{
id: "email",
title: strings.sendCodeEmail(),
icon: "envelope-simple",
iconFamily: "notesnook"
icon: "email-outline"
},
{
id: "app",
@@ -139,8 +133,7 @@ const TwoFactorVerification = ({
{
id: "recoveryCode",
title: strings.recoveryCode(),
icon: "lock-simple",
iconFamily: "notesnook"
icon: "key"
}
];
@@ -162,7 +155,6 @@ const TwoFactorVerification = ({
setSending(false);
} catch (e) {
setSending(false);
console.log("error e");
setError(
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
);
@@ -176,56 +168,48 @@ const TwoFactorVerification = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentMethod.method]);
useEffect(() => {
setCodeInput("");
code.current = "";
}, [currentMethod.method]);
return (
<View
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
style={{
width: "100%",
height: "100%",
backgroundColor: colors.primary.background,
paddingTop: 60
}}
onLayout={() => {
setTimeout(() => {
inputRef.current?.focus();
}, 500);
}}
style={{
alignItems: "center",
gap: Spacing.LEVEL_3
}}
>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
width: "100%"
alignItems: "center",
paddingHorizontal: currentMethod.method ? 12 : 0,
gap: 12
}}
>
<IconButton
style={{
width: 50,
height: 50,
backgroundColor: colors.primary.shade,
borderRadius: Radius.XS,
marginBottom: Spacing.LEVEL_7
width: 70,
height: 70
}}
size={25}
iconFamily="notesnook"
name="shield-check"
size={50}
name="key"
color={colors.primary.accent}
/>
<Heading
style={{
textAlign: "center",
marginBottom: Spacing.LEVEL_1
textAlign: "center"
}}
>
{currentMethod.method === "recoveryCode"
? strings.recoveryCodeTitle()
: currentMethod.method
? strings["2fa"]()
: strings.select2faMethod()}
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
</Heading>
<Paragraph
style={{
width: "80%",
textAlign: "center"
}}
>
@@ -235,35 +219,61 @@ const TwoFactorVerification = ({
]?.() || strings.select2faCodeHelpText()
: strings.select2faCodeHelpText()}
</Paragraph>
</View>
{currentMethod.method ? (
<>
<View
style={{
borderRadius: Radius.S,
width: "100%",
gap: Spacing.LEVEL_4
}}
>
<PinInput
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
type={seconds ? "plain" : "transparent"}
title={
sending
? ""
: `${
seconds
? strings.resend2faCode(`${seconds}`)
: strings.sendCode()
}`
}
loading={sending}
height={30}
/>
) : null}
{currentMethod.method ? (
<>
<Input
placeholder={
currentMethod.method === "recoveryCode"
? "xxxxx-xxxxx"
: "xxxxxx"
}
testID={"input.totp"}
inputRef={inputRef}
length={codeLength}
value={codeInput}
onChangeText={(value: string) => {
setCodeInput(value);
maxLength={
currentMethod.method === "recoveryCode" ? undefined : 6
}
fwdRef={inputRef}
textAlign="center"
onChangeText={(value) => {
setError(undefined);
code.current = value;
}}
cursorColor={colors.selected.accent}
selectionHandleColor={colors.selected.accent}
selectionColor={colors.selected.accent}
onSubmitEditing={onNext}
keyboardType={isRecoveryCode ? "web-search" : "number-pad"}
showDashAfterDigit={isRecoveryCode ? 5 : undefined}
sanitize={(value: string) => {
if (isRecoveryCode) {
return value.replace(/[^0-9a-zA-Z]/g, "").toUpperCase();
}
return value.replace(/[^0-9]/g, "");
height={60}
marginBottom={0}
inputStyle={{
fontSize: AppFontSize.lg,
textAlign: "center",
letterSpacing: 7,
width: 250
}}
keyboardType={
currentMethod.method === "recoveryCode" ? "default" : "numeric"
}
enablesReturnKeyAutomatically
containerStyle={{
minWidth: "50%"
}}
/>
{error ? (
@@ -274,9 +284,7 @@ const TwoFactorVerification = ({
style={{
textAlign: "center",
marginVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
maxWidth: 250,
alignSelf: "center",
marginTop: -10
maxWidth: 250
}}
>
<AppIcon
@@ -288,144 +296,76 @@ const TwoFactorVerification = ({
</Paragraph>
) : null}
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
<Button
title={strings.cancel()}
type="secondary"
onPress={() => {
reset();
onCancel();
}}
style={{
width: "48%"
}}
/>
<Button
title={loading ? null : strings.continue()}
type="accent"
loading={loading}
onPress={onNext}
style={{
width: "48%"
}}
/>
</View>
<View
style={{
gap: Spacing.LEVEL_1,
marginTop: -Spacing.LEVEL_0
}}
>
{currentMethod.method === "sms" ||
currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
type={"plain"}
disabled={parseInt(`${seconds}`) !== 0}
title={
sending
? ""
: `${
seconds
? strings.resend2faCode(`${seconds}`)
: strings.resendCode()
}`
}
loading={sending}
fontSize={AppFontSize.sm}
fontFamily="REGULAR"
style={{
paddingVertical: 0,
alignSelf: "flex-start",
paddingHorizontal: 0
}}
/>
) : null}
<TouchableOpacity
onPress={onRequestSecondaryMethod}
activeOpacity={0.8}
style={{
alignSelf: "flex-start"
}}
>
<Paragraph
fontFamily={isRecoveryCode ? "REGULAR" : "SEMI_BOLD"}
fontSize="SM"
color={
isRecoveryCode
? colors.primary.paragraph
: colors.primary.accent
}
>
{strings["2faCodeSecondaryMethodText"][
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
]()}
</Paragraph>
</TouchableOpacity>
</View>
</View>
</>
) : (
<View
style={{
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
{getMethods().map((item) => (
<Pressable
key={item.title}
<Button
title={loading ? null : strings.next()}
type="accent"
width={250}
loading={loading}
onPress={onNext}
/>
<Button
title={strings.cancel()}
type="secondaryAccented"
onPress={() => {
setCurrentMethod({
method: item.id,
isPrimary: false
});
reset();
onCancel();
}}
style={{
padding: Spacing.LEVEL_2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
borderRadius: Radius.S,
alignItems: "center",
width: "100%",
justifyContent: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<IconButton
style={{
borderRadius: Radius.XS,
padding: Spacing.LEVEL_1,
width: undefined,
height: undefined,
backgroundColor: RGB_Linear_Shade(
0.04,
hexToRGBA(colors.secondary.background)
)
width={250}
/>
<Button
title={strings["2faCodeSecondaryMethodText"][
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
]()}
type="plain"
onPress={onRequestSecondaryMethod}
height={30}
/>
</>
) : (
<>
{getMethods().map((item) => (
<Pressable
key={item.title}
onPress={() => {
setCurrentMethod({
method: item.id,
isPrimary: false
});
}}
size={17}
color={colors.primary.icon}
name={item.icon}
iconFamily={item.iconFamily as "notesnook"}
/>
<View
style={{
flexShrink: 1
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
marginTop: 0,
flexDirection: "row",
borderRadius: 0,
alignItems: "center",
width: "100%",
justifyContent: "flex-start"
}}
>
<Paragraph size={AppFontSize.sm}>{item.title}</Paragraph>
</View>
</Pressable>
))}
</View>
)}
</View>
<IconButton
type="secondaryAccented"
style={{
marginRight: 10
}}
size={15}
color={colors.primary.accent}
name={item.icon}
/>
<View
style={{
flexShrink: 1
}}
>
<Paragraph size={AppFontSize.md}>{item.title}</Paragraph>
</View>
</Pressable>
))}
</>
)}
</View>
</ScrollView>
);
};

View File

@@ -21,11 +21,13 @@ import { strings } from "@notesnook/intl";
import { useRef, useState } from "react";
import { TextInput } from "react-native";
import { db } from "../../common/database";
import { ToastManager } from "../../services/event-manager";
import { ToastManager, eSendEvent } from "../../services/event-manager";
import { clearMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSimpleDialog } from "../../utils/events";
import TwoFactorVerification from "./two-factor";
import { createFormRef } from "../ui/input/form-input";
export const LoginSteps = {
@@ -44,7 +46,6 @@ export const useLogin = (
const [step, setStep] = useState(LoginSteps.emailAuth);
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const mfaData = useRef<any>(undefined);
const formRef = useRef(
createFormRef({
email: "",
@@ -59,43 +60,69 @@ export const useLogin = (
setLoading(true);
switch (step) {
case LoginSteps.emailAuth: {
if (!formRef.current.validate()) {
if (formRef.current.validateField("email")) {
setLoading(false);
return;
}
const mfaInfo = await db.user.authenticateEmail(
formRef.current.getValue("email")
);
if (mfaInfo) {
mfaData.current = mfaInfo;
setStep(LoginSteps.mfaAuth);
setLoading(false);
TwoFactorVerification.present(
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
if (success) {
setStep(LoginSteps.passwordAuth);
setLoading(false);
setTimeout(() => {
passwordInputRef.current?.focus();
}, 500);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if ((e as Error).message === "invalid_grant") {
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);
}
}
},
mfaInfo,
() => {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
);
} else {
finishWithError(new Error(strings.unableToSend2faCode()));
}
break;
}
case LoginSteps.passwordAuth:
case LoginSteps.mfaAuth: {
case LoginSteps.passwordAuth: {
if (!formRef.current.validate()) {
setLoading(false);
return;
}
const values = formRef.current.getValues();
try {
await db.user.authenticatePassword(
await db.user.authenticatePassword(
values.email,
values.password,
undefined,
sessionExpired
);
finishLogin();
} catch(e) {
formRef.current.setError("password", (e as Error).message);
setLoading(false);
setStep(LoginSteps.passwordAuth);
}
finishLogin();
break;
}
}
@@ -145,7 +172,6 @@ export const useLogin = (
setLoading,
error,
setError,
formRef,
mfaData
formRef
};
};

View File

@@ -36,7 +36,6 @@ import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import AppIcon from "../ui/AppIcon";
interface FloatingButtonProps {
onPress: () => void;
@@ -141,7 +140,7 @@ const FloatingButton = ({
borderBottomWidth: 0,
borderLeftWidth: 0,
borderRightWidth: 0,
backgroundColor: colors.primary.accent
backgroundColor: colors.primary.background
}}
onPress={onPress}
>
@@ -149,19 +148,26 @@ const FloatingButton = ({
style={{
alignItems: "center",
justifyContent: "center",
height: 55,
width: 55,
height: normalize(size === "small" ? 40 : 60),
width: normalize(size === "small" ? 40 : 60),
backgroundColor: color
? RGB_Linear_Shade(0.87, hexToRGBA(color))
: colors.primary.accent,
: colors.primary.shade,
borderRadius: size === "small" ? 15 : 20
}}
>
<AppIcon
name={icon ? icon : route.name === "Trash" ? "trash-alt" : "plus"}
iconFamily="notesnook"
color={color || colors.primary.accentForeground}
size={24}
<Icon
name={
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}
/>
</View>
</TouchableOpacity>

View File

@@ -78,7 +78,7 @@ export default function DatePickerComponent(props: {
<Button
title={strings.cancel()}
type="plain-outline"
type="secondary"
style={{
width: "100%"
}}

View File

@@ -31,7 +31,8 @@ import ImagePreview from "../image-preview";
import MergeConflicts from "../merge-conflicts";
import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import Progress from "../dialogs/progress/progress";
import RecoveryKeySheet from "../sheets/recovery-key";
import Progress from "../dialogs/progress";
import { useSettingStore } from "../../stores/use-setting-store";
const DialogProvider = () => {
@@ -50,6 +51,7 @@ const DialogProvider = () => {
{isAppLoading ? null : (
<>
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />

View File

@@ -20,11 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { Spacing } from "../../common/design/spacing";
import { getColorLinearShade } from "../../utils/colors";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Button, ButtonProps } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
const DialogButtons = ({
onPressPositive,
@@ -48,41 +51,82 @@ const DialogButtons = ({
return (
<View
style={[
styles.container,
{
flexDirection: "row",
gap: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
backgroundColor: colors.secondary.background,
height: 60,
paddingHorizontal: DefaultAppStyles.GAP,
borderTopWidth: 0.7,
borderTopColor: getColorLinearShade(
colors.secondary.background,
0.05,
isDark
)
}
]}
>
<Button
onPress={onPressNegative}
fontSize={AppFontSize.md}
testID={notesnook.ids.default.dialog.no}
type="plain-outline"
style={{
flexGrow: 1,
flexShrink: 1
}}
title={negativeTitle}
/>
{onPressPositive ? (
<Button
onPress={onPressPositive}
fontSize={AppFontSize.md}
testID={notesnook.ids.default.dialog.yes}
{doneText ? (
<View
style={{
flexGrow: 1,
flexShrink: 1
flexDirection: "row",
alignItems: "center"
}}
loading={loading}
bold
type={positiveType || "accent"}
title={positiveTitle}
>
<Icon
color={colors.primary.accent}
name="check-circle-outline"
size={AppFontSize.md}
/>
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
</View>
) : loading ? (
<ActivityIndicator
size={AppFontSize.lg}
color={colors.primary.accent}
/>
) : null}
) : (
<View />
)}
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<Button
onPress={onPressNegative}
fontSize={AppFontSize.sm}
testID={notesnook.ids.default.dialog.no}
type="plain"
bold
title={negativeTitle}
/>
{onPressPositive ? (
<Button
onPress={onPressPositive}
fontSize={AppFontSize.sm}
testID={notesnook.ids.default.dialog.yes}
style={{
marginLeft: 10
}}
bold
type={positiveType || "transparent"}
title={positiveTitle}
/>
) : null}
</View>
</View>
);
};
export default DialogButtons;
const styles = StyleSheet.create({
container: {
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
marginTop: DefaultAppStyles.GAP_VERTICAL
}
});

View File

@@ -23,7 +23,6 @@ import { DDS } from "../../services/device-detection";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../utils/elevation";
import { getContainerBorder } from "../../utils/colors";
import { Radius } from "../../common/design/spacing";
const DialogContainer = ({
width,
@@ -45,10 +44,9 @@ const DialogContainer = ({
{
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: Radius.LG,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12,
marginTop: -300
paddingTop: 12
},
restProps?.noBorder
? {}

View File

@@ -17,20 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Text, View, ViewStyle } from "react-native";
import { Radius, Spacing } from "../../common/design/spacing";
import { DefaultAppStyles } from "../../utils/styles";
import AppIcon, { IconProps } from "../ui/AppIcon";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { Button, ButtonProps } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
type DialogHeaderProps = {
icon?: string;
iconFamily?: IconProps["iconFamily"];
iconType?: "error" | "normal";
title?: string;
paragraph?: string;
button?: ButtonProps;
@@ -49,10 +46,7 @@ const DialogHeader = ({
padding,
centered,
titlePart,
style,
icon,
iconFamily,
iconType
style
}: DialogHeaderProps) => {
const { colors } = useThemeColors();
@@ -63,89 +57,61 @@ const DialogHeader = ({
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
minHeight: 50,
paddingHorizontal: padding,
...style
}}
>
<View
style={{
width: "100%",
gap: Spacing.LEVEL_4
width: "100%"
}}
>
{icon ? (
<View
style={{
alignSelf: centered ? "center" : "flex-start",
width: 40,
height: 40,
backgroundColor: colors.error.shade,
justifyContent: "center",
alignItems: "center",
borderRadius: Radius.XS
}}
>
<AppIcon
name={icon}
iconFamily={iconFamily}
color={
iconType == "error" ? colors.static.red : colors.primary.icon
}
/>
</View>
) : null}
<View
style={{
gap: Spacing.LEVEL_1
flexDirection: "row",
justifyContent: centered ? "center" : "space-between",
alignItems: "center"
}}
>
<View
style={{
flexDirection: "row",
justifyContent: centered ? "center" : "space-between",
alignItems: "center"
}}
<Heading
style={{ textAlign: centered ? "center" : "left" }}
size={AppFontSize.lg}
>
<Heading
style={{ textAlign: centered ? "center" : "left" }}
fontSize="XL"
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
) : null}
</Heading>
{button ? (
<Button
style={{
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP
}}
fontSize={13}
type={button.type || "secondary"}
height={30}
{...button}
/>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
) : null}
</View>
</Heading>
{paragraph ? (
<Paragraph
{button ? (
<Button
style={{
textAlign: centered ? "center" : "left",
maxWidth: centered ? "90%" : "100%",
alignSelf: centered ? "center" : "flex-start"
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP
}}
color={paragraphColor || colors.secondary.paragraph}
>
{paragraph}
</Paragraph>
fontSize={13}
type={button.type || "secondary"}
height={30}
{...button}
/>
) : null}
</View>
{paragraph ? (
<Paragraph
style={{
textAlign: centered ? "center" : "left",
maxWidth: centered ? "90%" : "100%",
alignSelf: centered ? "center" : "flex-start"
}}
color={paragraphColor || colors.secondary.paragraph}
>
{paragraph}
</Paragraph>
) : null}
</View>
</View>
</>

View File

@@ -17,18 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { KeyboardTypeOptions, TextInput, TextInputProps } 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";
import { IconProps } from "../ui/AppIcon";
export type DialogInfo = {
title?: string;
paragraph?: string;
centered?: boolean;
positiveText: string;
negativeText: string;
background?: string;
@@ -46,8 +44,6 @@ export type DialogInfo = {
| "error"
| "errorShade";
icon?: string;
iconFamily?: IconProps["iconFamily"];
iconType?: "error" | "normal";
paragraphColor: string;
form?: {
formRef: FormRef;
@@ -58,12 +54,10 @@ export type DialogInfo = {
validators: FieldValidator[];
defaultValue?: string;
ref: RefObject<TextInput | null>;
inputProps?: TextInputProps;
}[];
onFormSubmit?: (form: FormRef) => Promise<boolean>;
};
input: boolean;
inputLabel?: string;
inputPlaceholder: string;
defaultValue: string;
// eslint-disable-next-line @typescript-eslint/ban-types

View File

@@ -19,14 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
useState,
RefObject
} from "react";
import { TextInput, View, ViewStyle } from "react-native";
import { Spacing } from "../../common/design/spacing";
import { DDS } from "../../services/device-detection";
import {
eSubscribeEvent,
@@ -39,10 +38,11 @@ import { defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Toast } from "../toast";
import { Checkbox } from "../ui/checkbox";
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";
import DialogButtons from "./dialog-buttons";
import DialogHeader from "./dialog-header";
@@ -143,6 +143,9 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}, [hide, show]);
const onNegativePress = async () => {
if (dialogInfo?.onClose) {
await dialogInfo.onClose();
}
hide();
};
@@ -152,7 +155,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
maxHeight: 450,
borderRadius: defaultBorderRadius,
backgroundColor: colors.primary.background,
gap: Spacing.LEVEL_4,
paddingTop: 12,
...getContainerBorder(colors.primary.border, 0.5),
overflow: "hidden"
};
@@ -167,7 +170,9 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
bounce={!dialogInfo.input && !dialogInfo.form}
closeOnTouch={!dialogInfo.disableBackdropClosing}
background={dialogInfo.background}
transparent={dialogInfo.transparent}
transparent={
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
}
onShow={async () => {
if (dialogInfo.input && !dialogInfo.form) {
inputRef.current?.setNativeProps({
@@ -196,66 +201,54 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
: dialogInfo.component}
{dialogInfo.component ? null : (
<View
style={[
{
paddingVertical: Spacing.LEVEL_4
},
style
]}
>
<View
style={[
{
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_4
}
]}
>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
iconFamily={dialogInfo.iconFamily}
iconType={dialogInfo.iconType}
centered={dialogInfo.centered}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
style={{
minHeight: 0
}}
/>
<View style={style}>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
style={{
minHeight: 0
}}
/>
<Seperator half />
{dialogInfo.form ? (
<View
style={{
gap: Spacing.LEVEL_2
}}
>
{dialogInfo.form.items.map((item, index) => (
<FormInput
{...item.inputProps}
key={item.name}
fwdRef={item.ref}
name={item.name}
label={item.label}
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 ? (
{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
}}
>
<Input
fwdRef={inputRef}
autoCapitalize="none"
@@ -265,7 +258,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
defaultValue={dialogInfo.defaultValue}
label={dialogInfo.inputLabel}
onSubmit={() => {
onPressPositive();
}}
@@ -274,34 +266,48 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
keyboardType={dialogInfo.keyboardType || "default"}
placeholder={dialogInfo.inputPlaceholder}
/>
) : null}
</View>
) : null}
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
{dialogInfo.check ? (
<Checkbox
checked={checked}
{dialogInfo.check ? (
<>
<Button
onPress={() => {
setChecked(!checked);
}}
title={dialogInfo.check.info}
icon={
checked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
iconColor={
checked ? colors.secondary.icon : colors.primary.icon
}
style={{
marginTop: -Spacing.LEVEL_1
justifyContent: "flex-start"
}}
height={35}
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type || "plain" : "plain"}
/>
) : null}
</View>
</>
) : null}
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={

View File

@@ -16,11 +16,9 @@ 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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import {
clearAppLockVerificationCipher,
@@ -35,7 +33,7 @@ import {
eSubscribeEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
import { useSettingStore } from "../../../stores/use-setting-store";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseAppLocKPasswordDailog,
@@ -44,122 +42,61 @@ import {
import { AppFontSize } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import Heading from "../../ui/typography/heading";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
export const AppLockPassword = () => {
const { colors } = useThemeColors();
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
const [keyboardType, setKeyboardType] = useState<"pin" | "password">(
useSettingStore.getState().settings.applockKeyboardType === "default"
? "password"
: "pin"
);
const [visible, setVisible] = useState(false);
const currentPasswordInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const formRef = useRef(
createFormRef({
currentPassword: "",
password: "",
confirmPassword: ""
})
);
const values = useRef<{
currentPassword?: string;
password?: string;
confirmPassword?: string;
}>({});
const [secureTextEntry, setSecureTextEntry] = useState(true);
const [accountPass, setAccountPass] = useState(false);
const enableApplock = useRef(false);
useEffect(() => {
const subs = [
eSubscribeEvent(
eOpenAppLockPasswordDialog,
(mode: "create" | "change" | "remove", _enableApplock = false) => {
(mode: "create" | "change" | "remove") => {
setMode(mode);
setAccountPass(false);
setVisible(true);
enableApplock.current = _enableApplock;
}
),
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
close();
values.current = {};
setVisible(false);
})
];
return () => {
subs.forEach((sub) => sub?.unsubscribe());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const close = () => {
formRef.current.setValue("currentPassword", "");
formRef.current.setValue("password", "");
formRef.current.setValue("confirmPassword", "");
formRef.current.clearErrors();
values.current = {};
setVisible(false);
};
const onSubmit = async () => {
if (!formRef.current.validate()) return;
const currentPassword = formRef.current.getValue("currentPassword");
const password = formRef.current.getValue("password");
if (mode === "create") {
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
if (enableApplock.current) {
SettingsService.setProperty("appLockEnabled", true);
}
} else if (mode === "change") {
const isCurrentPasswordCorrect =
await validateAppLockPassword(currentPassword);
if (!isCurrentPasswordCorrect) {
formRef.current.setError(
"currentPassword",
strings.incorrect("password")
);
return;
}
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(password)
: await validateAppLockPassword(password);
if (!isCurrentPasswordCorrect) {
formRef.current.setError(
"password",
accountPass
? strings.passwordIncorrect()
: strings.incorrect("password")
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
});
}
}
close();
};
return !visible ? null : (
<BaseDialog
onShow={async () => {
@@ -178,132 +115,286 @@ export const AppLockPassword = () => {
style={{
...getElevationStyle(10),
width: DDS.isTab ? 350 : "85%",
borderRadius: Radius.MD,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_4
paddingTop: 12
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.changeAppLockCredentials(mode, "password")}
</Heading>
<DialogHeader
title={strings.changeAppLockCredentials(mode, keyboardType)}
icon="shield"
padding={12}
/>
<Seperator half />
<View style={{ gap: Spacing.LEVEL_2 }}>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{mode === "change" ? (
<FormInput
name="currentPassword"
formRef={formRef}
label={strings.currentPassword()}
<Input
fwdRef={currentPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.currentPassword = value;
}}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
defaultValue={values.current.currentPassword}
autoComplete="password"
onSubmitEditing={() => passwordInputRef.current?.focus()}
returnKeyLabel={strings.next()}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
returnKeyType="next"
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[validators.required(strings.passwordRequired())]}
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.currentPin()
: strings.currentPassword()
}
/>
) : null}
<FormInput
name="password"
formRef={formRef}
label={
mode === "change"
? strings.newPassword()
: strings.enterPassword()
}
<Input
fwdRef={passwordInputRef}
autoCapitalize="none"
autoComplete="password"
onSubmitEditing={() => {
if (mode !== "remove") {
confirmPasswordInputRef.current?.focus();
} else {
onSubmit();
}
onChangeText={(value) => {
values.current.password = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.password}
keyboardType={
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
}
autoComplete="password"
returnKeyLabel={
mode !== "remove" ? strings.next() : strings.remove()
}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[validators.required(strings.passwordRequired())]}
secureTextEntry={secureTextEntry}
buttonLeft={
accountPass ? null : (
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={AppFontSize.lg}
/>
)
}
placeholder={
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
}
/>
{mode !== "remove" ? (
<FormInput
name="confirmPassword"
formRef={formRef}
label={
mode === "change"
? strings.confirmNewPassword()
: strings.confirmPassword()
}
<Input
fwdRef={confirmPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.confirmPassword = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.confirmPassword}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
customValidator={() => values.current.password || ""}
validationType="confirmPassword"
autoComplete="password"
onSubmitEditing={() => onSubmit()}
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField("password", strings.passwordNotMatched())
]}
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.confirmPin()
: strings.confirmPassword()
}
/>
) : null}
{mode === "remove" ? (
<Button
icon={accountPass ? "checkbox" : "box-empty"}
iconFamily="notesnook"
onPress={() => setAccountPass(!accountPass)}
iconSize={AppFontSize.sm}
fontFamily="MEDIUM"
fontSize={AppFontSize.xs}
type="plain"
iconColor={
accountPass
? [colors.primary.accent, colors.primary.accentForeground]
: colors.primary.icon
}
textStyle={{
color: colors.primary.paragraph
}}
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
paddingVertical: 0
}}
/>
<>
<Button
icon={
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
}
onPress={() => {
setSecureTextEntry(false);
setAccountPass(!accountPass);
setTimeout(() => {
setSecureTextEntry(true);
});
}}
iconSize={AppFontSize.lg}
type="plain"
iconColor={
accountPass ? colors.primary.accent : colors.primary.icon
}
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
</>
) : null}
</View>
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
<Button
title={strings.cancel()}
type="plain-outline"
onPress={close}
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
/>
<Button
title={mode === "remove" ? strings.remove() : strings.save()}
type="accent"
onPress={onSubmit}
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
/>
</View>
<DialogButtons
onPressNegative={close}
onPressPositive={async () => {
if (mode === "create") {
if (!values.current.password || !values.current.confirmPassword) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
const password = values.current.password;
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
} else if (mode === "change") {
if (
!values.current.currentPassword ||
!values.current.password ||
!values.current.confirmPassword
) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.currentPassword
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(strings.incorrect(keyboardType)),
undefined,
"local"
);
return;
}
const password = values.current.password;
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(values.current.password)
: await validateAppLockPassword(values.current.password);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
accountPass
? strings.passwordIncorrect()
: strings.incorrect(keyboardType)
),
undefined,
"local"
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
});
}
}
SettingsService.setProperty(
"applockKeyboardType",
keyboardType === "password" ? "default" : "numeric"
);
close();
}}
positiveTitle={
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"
loading={false}
doneText=""
/>
</View>
<Toast context="local" />
@@ -311,9 +402,6 @@ export const AppLockPassword = () => {
);
};
AppLockPassword.present = (
mode: "create" | "change" | "remove",
enableAppLock?: boolean
) => {
eSendEvent(eOpenAppLockPasswordDialog, mode, enableAppLock);
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
eSendEvent(eOpenAppLockPasswordDialog, mode);
};

View File

@@ -45,7 +45,6 @@ import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import ReactNativeBlobUtil from "react-native-blob-util";
import { useSettingStore } from "../../../stores/use-setting-store";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -287,9 +286,6 @@ const PDFPreview = () => {
color={colors.static.white}
name="open-in-new"
onPress={() => {
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
FileViewer.open(pdfSource, {
showOpenWithDialog: true,
showAppsSuggestions: true

View File

@@ -17,8 +17,28 @@ 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 { eSendEvent } from "../../../services/event-manager";
import { ProgressOptions } from "./progress";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import { Button } from "../../ui/button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
export type ProgressOptions = {
progress?: string;
cancelCallback?: () => void;
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
};
export const PROGRESS_EVENTS = {
start: "startProgress",
@@ -26,6 +46,146 @@ export const PROGRESS_EVENTS = {
update: "updateProgress"
};
export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>(undefined);
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
}>();
useEffect(() => {
const events = [
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
setData({
title: options.title,
paragraph: options.paragraph,
fillBackground: options.fillBackground,
canHideProgress: options.canHideProgress
});
setVisible(true);
}),
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
setProgress(undefined);
setVisible(false);
setData(undefined);
cancelCallback.current?.();
cancelCallback.current = undefined;
}),
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
setProgress(options.progress);
if (options.cancelCallback) {
cancelCallback.current = options.cancelCallback;
}
const data: ProgressOptions = {};
if (options.title) data.title = options.title;
if (options.paragraph) data.paragraph = options.paragraph;
if (options.fillBackground)
data.fillBackground = options.fillBackground;
setData((current) => {
return {
...current,
...data
};
});
})
];
return () => {
events.forEach((event) => event?.unsubscribe());
cancelCallback.current = undefined;
};
}, []);
return !visible ? null : (
<BaseDialog
background={data?.fillBackground ? colors.primary.background : undefined}
visible
>
<DialogContainer
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingBottom: 10
}}
noBorder={data?.fillBackground ? true : false}
>
<Dialog context="local" />
<View
style={{
justifyContent: "center",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 10,
paddingBottom: 20
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.paragraph}
size={AppFontSize.lg}
>
{data?.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{progress ? progress : data?.paragraph}
</Paragraph>
<View
style={{
flexDirection: "row",
width: 100,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<ProgressBarComponent
height={5}
width={100}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="secondaryAccented"
onPress={() => {
if (cancelCallback.current) {
cancelCallback.current?.();
}
setVisible(false);
setProgress(undefined);
setData(undefined);
}}
width="100%"
/>
)}
</View>
</DialogContainer>
</BaseDialog>
);
}
export function startProgress(options: ProgressOptions) {
eSendEvent(PROGRESS_EVENTS.start, options);
}

View File

@@ -1,210 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { PROGRESS_EVENTS } from ".";
import { Radius, Spacing } from "../../../common/design/spacing";
import { eSubscribeEvent } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
export type ProgressOptions = {
progress?: string;
cancelCallback?: () => void;
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
icon?: string;
};
export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>(undefined);
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
icon?: string;
}>();
useEffect(() => {
const events = [
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
setData({
...options
});
setVisible(true);
}),
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
setProgress(undefined);
setVisible(false);
setData(undefined);
cancelCallback.current?.();
cancelCallback.current = undefined;
}),
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
setProgress(options.progress);
if (options.cancelCallback) {
cancelCallback.current = options.cancelCallback;
}
const data: ProgressOptions = {};
if (options.title) data.title = options.title;
if (options.paragraph) data.paragraph = options.paragraph;
if (options.fillBackground)
data.fillBackground = options.fillBackground;
if (options.icon) {
data.icon = options.icon;
}
setData((current) => {
return {
...current,
...data
};
});
})
];
return () => {
events.forEach((event) => event?.unsubscribe());
cancelCallback.current = undefined;
};
}, []);
return !visible ? null : (
<BaseDialog
background={data?.fillBackground ? colors.primary.background : undefined}
visible
>
<DialogContainer
style={{
padding: Spacing.LEVEL_4
}}
noBorder={data?.fillBackground ? true : false}
>
<Dialog context="local" />
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: Spacing.LEVEL_4,
paddingTop: Spacing.LEVEL_1
}}
>
{data?.icon ? (
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name={data?.icon}
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
</View>
) : null}
<View
style={{
gap: Spacing.LEVEL_1
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.heading}
size={AppFontSize.lg}
>
{data?.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{progress ? progress : data?.paragraph}
</Paragraph>
<View
style={{
flexDirection: "row",
width: "100%"
}}
>
<ProgressBarComponent
height={8}
width={null}
style={{
flex: 1
}}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
</View>
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="accent"
onPress={() => {
if (cancelCallback.current) {
cancelCallback.current?.();
}
setVisible(false);
setProgress(undefined);
setData(undefined);
}}
width={160}
/>
)}
</View>
</DialogContainer>
</BaseDialog>
);
}

View File

@@ -55,7 +55,10 @@ import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import {
isEncryptedContent,
Note,
@@ -63,17 +66,17 @@ import {
VAULT_ERRORS
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { Radius, Spacing } from "../../../common/design/spacing";
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
import { useUserStore } from "../../../stores/use-user-store";
import { AppFontSize } from "../../../utils/size";
import { useSettingStore } from "../../../stores/use-setting-store";
import { Checkbox } from "../../ui/checkbox";
import { Pressable } from "../../ui/pressable";
import AppIcon from "../../ui/AppIcon";
export const VaultDialog: React.FC = () => {
const { colors } = useThemeColors();
// UI State
const [visible, setVisible] = useState(false);
const isUserLoggedIn = useUserStore((state) => !!state.user);
const [loading, setLoading] = useState(false);
const [deleteAll, setDeleteAll] = useState(false);
const [biometricUnlock, setBiometricUnlock] = useState(false);
@@ -88,7 +91,7 @@ export const VaultDialog: React.FC = () => {
const paragraphRef = useRef<string | null>(null);
const buttonTitleRef = useRef<string | null>(null);
const positiveButtonTypeRef = useRef<"errorShade" | "transparent" | "accent">(
"accent"
"transparent"
);
const customActionTitleRef = useRef<string | null>(null);
const customActionParagraphRef = useRef<string | null>(null);
@@ -116,7 +119,6 @@ export const VaultDialog: React.FC = () => {
const passInputRef = useRef<TextInput>(null);
const confirmPassRef = useRef<TextInput>(null);
const newPassInputRef = useRef<TextInput>(null);
const [icon, setIcon] = useState<string>();
const close = useCallback(() => {
if (loading) {
@@ -144,7 +146,7 @@ export const VaultDialog: React.FC = () => {
descriptionRef.current = null;
paragraphRef.current = null;
buttonTitleRef.current = null;
positiveButtonTypeRef.current = "accent";
positiveButtonTypeRef.current = "transparent";
customActionTitleRef.current = null;
customActionParagraphRef.current = null;
noteLockedRef.current = false;
@@ -199,9 +201,12 @@ export const VaultDialog: React.FC = () => {
}
eSendEvent("vaultUpdated");
setLoading(false);
setTimeout(() => {
close();
}, 100);
close();
ToastManager.show({
message: strings.vaultDeleted(),
type: "success",
context: "global"
});
} else {
setLoading(false);
formRef.current.setError("password", strings.passwordIncorrect());
@@ -350,7 +355,6 @@ export const VaultDialog: React.FC = () => {
async (note: Note & { content?: NoteContent<false> }) => {
close();
try {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
await Share.open({
title: note.title,
failOnCancel: false,
@@ -493,6 +497,13 @@ export const VaultDialog: React.FC = () => {
const { password, newPassword } = formRef.current.getValues();
if (requestType === VaultRequestType.DeleteVault && !isUserLoggedIn) {
setLoading(true);
await deleteVault();
setLoading(false);
return;
}
if (requestType === VaultRequestType.CreateVault) {
createVault();
} else if (requestType === VaultRequestType.ChangePassword) {
@@ -617,7 +628,7 @@ export const VaultDialog: React.FC = () => {
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "accent";
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
@@ -626,24 +637,29 @@ export const VaultDialog: React.FC = () => {
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
// setBiometricUnlock(fingerprint);
setIcon(data.icon);
setBiometricUnlock(fingerprint);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
available &&
data.requestType !== VaultRequestType.EnableFingerprint &&
data.requestType !== VaultRequestType.RevokeFingerprint &&
data.requestType !== VaultRequestType.ChangePassword &&
data.requestType !== VaultRequestType.ClearVault &&
data.requestType !== VaultRequestType.DeleteVault &&
data.requestType !== VaultRequestType.CustomAction &&
data.requestType !== VaultRequestType.PermanentUnlock;
data.requestType !== VaultRequestType.PermanentUnlock &&
data.requestType !== VaultRequestType.CreateVault;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
if (canAutoUnlock && available) {
try {
await onPressFingerprintAuth(data.title, data.description);
} catch (e) {
setVisible(true);
}
} else {
setVisible(true);
}
@@ -691,58 +707,56 @@ export const VaultDialog: React.FC = () => {
style={{
...getElevationStyle(5),
width: DDS.isTab ? 350 : "85%",
borderRadius: Radius.MD,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingVertical: Spacing.LEVEL_4,
paddingTop: 12,
overflow: "hidden"
}}
>
<DialogHeader
title={
requestType === VaultRequestType.PermanentUnlock
? noteRef.current?.title
: titleRef.current
}
title={titleRef.current}
paragraph={
paragraphRef.current || customActionParagraphRef.current || ""
}
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
centered={!!icon}
icon={icon}
iconType="error"
iconFamily="notesnook"
icon="shield"
padding={12}
/>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
gap: isDeleteVault ? 0 : Spacing.LEVEL_4,
marginTop: Spacing.LEVEL_4,
marginBottom: Spacing.LEVEL_4
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{(isChangePassword ||
isClearVault ||
!isCreateVault ||
isDeleteVault ||
requestType === VaultRequestType.LockNote ||
requestType === VaultRequestType.UnlockNote ||
requestType === VaultRequestType.PermanentUnlock ||
isGoToEditor ||
isShareNote ||
isDeleteNote ||
isEnableFingerprint ||
(isDeleteVault && isUserLoggedIn) ||
isCustomAction) &&
!isRevokeFingerprint ? (
<>
<FormInput
name="password"
formRef={formRef}
label={
isChangePassword
? strings.currentPassword()
: strings.password()
}
fwdRef={passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
autoComplete="password"
marginBottom={
!biometricUnlock ||
!isBiometryEnrolled ||
isCreateVault ||
isChangePassword ||
isCustomAction
? 0
: 10
}
onSubmitEditing={() => {
if (isChangePassword) {
newPassInputRef.current?.focus();
@@ -755,7 +769,11 @@ export const VaultDialog: React.FC = () => {
}
returnKeyType={isChangePassword ? "next" : "done"}
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
placeholder={
isChangePassword
? strings.currentPassword()
: strings.password()
}
validators={[validators.required(strings.passwordRequired())]}
/>
@@ -780,32 +798,39 @@ export const VaultDialog: React.FC = () => {
) : null}
{isDeleteVault && (
<Button
<Pressable
onPress={() => setDeleteAll(!deleteAll)}
icon={deleteAll ? "checkbox" : "box-empty"}
iconFamily="notesnook"
fontFamily="MEDIUM"
iconSize={14}
fontSize={AppFontSize.sm}
width="100%"
style={{
justifyContent: "flex-start",
paddingHorizontal: 0
paddingVertical: 0,
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL,
marginTop: isUserLoggedIn
? DefaultAppStyles.GAP_VERTICAL_SMALL
: 0,
justifyContent: "flex-start"
}}
title={strings.deleteAllNotes()}
type="transparent"
iconColor={colors.error.accent}
textStyle={{
color: colors.error.accent
}}
/>
>
<AppIcon
name={
deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
color={colors.error.accent}
size={AppFontSize.md}
/>
<Paragraph color={colors.error.accent} size={AppFontSize.sm}>
{strings.deleteAllNotes()}
</Paragraph>
</Pressable>
)}
{isChangePassword ? (
<>
<Seperator half />
<FormInput
name="newPassword"
label={strings.newPassword()}
formRef={formRef}
fwdRef={newPassInputRef}
editable={!loading}
@@ -818,21 +843,16 @@ export const VaultDialog: React.FC = () => {
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
placeholder={strings.newPassword()}
validators={[validators.required(strings.passwordRequired())]}
/>
</>
) : null}
{isCreateVault ? (
<View
style={{
gap: Spacing.LEVEL_2
}}
>
<View>
<FormInput
name="password"
label={strings.password()}
formRef={formRef}
fwdRef={passInputRef}
autoCapitalize="none"
@@ -844,13 +864,12 @@ export const VaultDialog: React.FC = () => {
onSubmitEditing={() => {
confirmPassRef.current?.focus();
}}
placeholder={PASSWORD_PLACEHOLDER}
placeholder={strings.password()}
validators={[validators.required(strings.passwordRequired())]}
/>
<FormInput
name="confirmPassword"
label={strings.confirmPassword()}
formRef={formRef}
fwdRef={confirmPassRef}
autoCapitalize="none"
@@ -863,7 +882,7 @@ export const VaultDialog: React.FC = () => {
onSubmitEditing={() => {
onPress();
}}
placeholder={PASSWORD_PLACEHOLDER}
placeholder={strings.confirmPassword()}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField(
@@ -875,7 +894,12 @@ export const VaultDialog: React.FC = () => {
</View>
) : null}
{!isBiometryEnrolled &&
{biometricUnlock && !isBiometryEnrolled && !isCreateVault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{!biometricUnlock &&
!isBiometryEnrolled &&
isBiometryAvailable &&
(requestType === VaultRequestType.CopyNote ||
requestType === VaultRequestType.DeleteNote ||
@@ -884,16 +908,20 @@ export const VaultDialog: React.FC = () => {
requestType === VaultRequestType.GoToEditor ||
requestType === VaultRequestType.PermanentUnlock ||
requestType === VaultRequestType.LockNote) ? (
<Checkbox
title={strings.vaultEnableBiometrics()}
checked={biometricUnlock}
iconSize={12}
style={{
marginTop: -Spacing.LEVEL_1
}}
<Button
onPress={() => {
setBiometricUnlock(!biometricUnlock);
}}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
iconColor={
biometricUnlock ? colors.selected.accent : colors.primary.icon
}
type={biometricUnlock ? "transparent" : "plain"}
/>
) : null}
</View>

View File

@@ -17,23 +17,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform, View, ViewStyle } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import { Radius, Spacing } from "../../common/design/spacing";
import { View } from "react-native";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { RouteName } from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { eScrollEvent } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import { IconButton, IconButtonProps } from "../ui/icon-button";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { IconButtonProps } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
export const Header = ({
renderedInRoute,
@@ -43,8 +45,7 @@ export const Header = ({
canGoBack,
hasSearch,
onSearch,
rightButton,
style
rightButton
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute?: RouteName;
@@ -55,7 +56,6 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
rightButton?: IconButtonProps;
style?: ViewStyle;
}) => {
const { colors } = useThemeColors();
const [borderHidden, setBorderHidden] = useState(true);
@@ -64,9 +64,6 @@ export const Header = ({
state.selectionMode
]);
const deviceMode = useSettingStore((state) => state.deviceMode);
const isTablet = deviceMode === "tablet";
const onScroll = useCallback(
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
@@ -88,60 +85,34 @@ export const Header = ({
};
}, [borderHidden, onScroll]);
const _onLeftButtonPress = () => {
if (onLeftMenuButtonPress) return onLeftMenuButtonPress();
if (!canGoBack) {
if (fluidTabsRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
} else {
Navigation.openDrawer();
}
return;
}
Navigation.goBack();
};
const HeaderWrapper = hasSearch ? Pressable : View;
return (
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
marginTop: Platform.OS === "android" ? 5 : 0
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
style={[
{
flexDirection: "row",
justifyContent: "space-between",
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_3,
backgroundColor: colors.secondary.background,
alignItems: "center"
},
style
]}
<HeaderWrapper
style={{
flexDirection: "row",
justifyContent: "space-between",
borderRadius: 10,
paddingVertical: 3,
borderWidth: hasSearch ? 1 : 0,
borderColor: colors.primary.border,
paddingHorizontal: !hasSearch ? 0 : DefaultAppStyles.GAP_SMALL,
alignItems: "center"
}}
testID="search-header"
onPress={() => {
onSearch?.();
}}
>
{isTablet && !canGoBack ? null : (
<IconButton
testID={notesnook.ids.default.header.buttons.left}
left={40}
top={40}
onPress={_onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}
style={{
width: 20,
height: 20
}}
size={20}
name={canGoBack ? "arrow-back" : "menu"}
iconFamily="notesnook"
color={colors.primary.icon}
/>
)}
<LeftMenus
canGoBack={canGoBack}
onLeftButtonPress={onLeftMenuButtonPress}
/>
{!title ? (
<View
@@ -152,46 +123,18 @@ export const Header = ({
borderRadius: 100
}}
/>
) : hasSearch ? (
<Paragraph>
{selectionMode
? `${selectedItemsList.length} selected`
: strings.searchInRoute(title)}
</Paragraph>
) : (
<Heading fontSize="XL">
{selectionMode ? `${selectedItemsList.length} selected` : title}
</Heading>
<Heading size={AppFontSize.lg}>{title}</Heading>
)}
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{rightButton ? (
<IconButton {...rightButton} color={colors.primary.icon} />
) : !hasSearch ? (
<View
style={{
width: 20,
height: 20
}}
/>
) : null}
{hasSearch ? (
<IconButton
color={colors.primary.icon}
size={20}
onPress={() => {
onSearch?.();
}}
style={{
width: 20,
height: 20
}}
iconFamily="notesnook"
name="search"
/>
) : null}
</View>
</View>
<RightMenus rightButton={rightButton} />
</HeaderWrapper>
</View>
);
};

View File

@@ -0,0 +1,66 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation";
import { useSettingStore } from "../../stores/use-setting-store";
import { fluidTabsRef } from "../../utils/global-refs";
import { IconButton } from "../ui/icon-button";
export const LeftMenus = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const isTablet = deviceMode === "tablet";
const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
if (!canGoBack) {
if (fluidTabsRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
} else {
Navigation.openDrawer();
}
return;
}
Navigation.goBack();
};
return isTablet && !canGoBack ? null : (
<IconButton
testID={notesnook.ids.default.header.buttons.left}
left={40}
top={40}
onPress={_onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}
name={canGoBack ? "arrow-left" : "menu"}
color={colors.primary.icon}
/>
);
};

View File

@@ -17,41 +17,41 @@ 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 { DimensionValue, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { StyleSheet, View } from "react-native";
import { IconButton, IconButtonProps } from "../ui/icon-button";
const LineSeparator = ({
style,
paddingHorizontal,
paddingVertical,
stroke = 1
export const RightMenus = ({
rightButton
}: {
style?: ViewStyle;
paddingHorizontal?: DimensionValue;
paddingVertical?: DimensionValue;
stroke?: number;
rightButton?: IconButtonProps;
}) => {
const { colors } = useThemeColors();
return (
<View
style={[
style,
{
paddingHorizontal,
paddingVertical
}
]}
>
<View
style={{
width: "100%",
height: stroke,
backgroundColor: colors.primary.separator
}}
/>
<View style={styles.rightBtnContainer}>
{rightButton ? (
<IconButton {...rightButton} color={colors.primary.icon} />
) : (
<View
style={{
width: 40,
height: 40
}}
/>
)}
</View>
);
};
export default LineSeparator;
const styles = StyleSheet.create({
rightBtnContainer: {
flexDirection: "row",
alignItems: "center"
},
rightBtn: {
justifyContent: "center",
alignItems: "center"
}
});

View File

@@ -39,6 +39,7 @@ import Share from "react-native-share";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useSettingStore } from "../../stores/use-setting-store";
import { DefaultAppStyles } from "../../utils/styles";
import FileViewer from "react-native-file-viewer";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
@@ -87,9 +88,8 @@ const ImagePreview = () => {
return;
}
const attachment = await db.attachments.attachment(hash);
const path = `${cacheDir}/${
"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
}`;
const path = `${cacheDir}/${"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, path).catch(() => {
/* empty */
});
@@ -136,17 +136,23 @@ const ImagePreview = () => {
borderWidth: 0
}}
onPress={async () => {
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
await Share.open({
url: image
}).catch(() => {
/* empty */
});
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(false);
if (!image) return;
try {
if (Platform.OS === "android") {
await FileViewer.open(image.replace("file://", ""), {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any);
} else {
await Share.open({
url: image
});
}
} catch (error) {
console.error(error);
}
}}
/>
<IconButton

View File

@@ -1,314 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withSpring,
withTiming
} from "react-native-reanimated";
import Svg, {
Circle,
ClipPath,
Defs,
G,
Mask,
Path,
Rect
} from "react-native-svg";
import { useThemeColors } from "@notesnook/theme";
const SPRING_CONFIG = {
damping: 14,
stiffness: 120,
mass: 0.8
};
// Each icon floats independently with a different phase/speed
function useFloatAnim(delay: number, range = 6) {
const y = useSharedValue(0);
React.useEffect(() => {
const start = () => {
y.value = withSequence(
withTiming(0, { duration: delay, easing: Easing.linear }),
withRepeat(
withSequence(
withTiming(-range, {
duration: 1600,
easing: Easing.inOut(Easing.sin)
}),
withTiming(range, {
duration: 1600,
easing: Easing.inOut(Easing.sin)
})
),
-1, // infinite
true // reverse
)
);
};
start();
}, []);
return y;
}
export const IntroIllustration = () => {
const { isDark } = useThemeColors();
// Gesture-driven tilt
const tiltX = useSharedValue(0); // rotateX: finger up/down
const tiltY = useSharedValue(0); // rotateY: finger left/right
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
// Independent float offsets for each corner icon
const floatTR = useFloatAnim(0, 2); // top-right lock
const floatBR = useFloatAnim(500, 4); // bottom-right eye-off
const floatTL = useFloatAnim(260, 3); // top-left share
const floatBL = useFloatAnim(780, 2); // bottom-left document
const pan = Gesture.Pan()
.onUpdate((e) => {
// Clamp tilt to ±15 degrees and translate ±12 pts
tiltY.value = (e.translationX / 12) * 1;
tiltX.value = -(e.translationY / 12) * 1;
translateX.value = e.translationX * 0.08;
translateY.value = e.translationY * 0.08;
})
.onEnd(() => {
tiltX.value = withSpring(0, SPRING_CONFIG);
tiltY.value = withSpring(0, SPRING_CONFIG);
translateX.value = withSpring(0, SPRING_CONFIG);
translateY.value = withSpring(0, SPRING_CONFIG);
});
const containerStyle = useAnimatedStyle(() => ({
transform: [
{ perspective: 600 },
{ rotateX: `${tiltX.value}deg` },
{ rotateY: `${tiltY.value}deg` },
{ translateX: translateX.value },
{ translateY: translateY.value }
]
}));
// Derived animated styles for each floating icon
const styleTR = useAnimatedStyle(() => ({
transform: [{ translateY: floatTR.value }]
}));
const styleBR = useAnimatedStyle(() => ({
transform: [{ translateY: floatBR.value }]
}));
const styleTL = useAnimatedStyle(() => ({
transform: [{ translateY: floatTL.value }]
}));
const styleBL = useAnimatedStyle(() => ({
transform: [{ translateY: floatBL.value }]
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={containerStyle}>
<Svg width={243} height={210} viewBox="0 0 243 210" fill="none">
{/* ── Concentric background circles (static) ── */}
<Circle
cx={116}
cy={105}
r={104.75}
stroke={isDark ? "#1F2722" : "#E8F4ED"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={105.5}
r={95.25}
fill="#008836"
fillOpacity={0.05}
stroke={isDark ? "#233C2D" : "#E8F0EC"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={105.5}
r={82.25}
fill="#008836"
fillOpacity={0.04}
stroke={isDark ? "#233C2D" : "#E3F1E8"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={105.5}
r={62.25}
fill="#008836"
fillOpacity={0.06}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Circle cx={116.5} cy={105.5} r={44.5} fill="#008836" />
{/* ── Defs ── */}
<Defs>
<ClipPath id="clip0">
<Rect width={45} height={45} x={94.5} y={83} />
</ClipPath>
<ClipPath id="clip1">
<Rect width={21.5} height={21.5} x={202.75} y={176.75} />
</ClipPath>
<Mask
id="mask0"
maskUnits="userSpaceOnUse"
x={91}
y={80}
width={52}
height={51}
>
<Path
d="M132.504 80.055H101.496C96.007 80.055 91.5574 84.5046 91.5574 89.9935V121.002C91.5574 126.491 96.007 130.94 101.496 130.94H132.504C137.993 130.94 142.443 126.491 142.443 121.002V89.9935C142.443 84.5046 137.993 80.055 132.504 80.055Z"
fill="white"
/>
</Mask>
</Defs>
{/* ── Centre logo (static) ── */}
<G clipPath="url(#clip0)">
<G mask="url(#mask0)">
<Path
d="M127.584 113.991C126.726 116.496 125.008 118.616 122.734 119.973C120.461 121.33 117.78 121.836 115.168 121.401C112.556 120.967 110.183 119.62 108.471 117.6C106.759 115.58 105.82 113.018 105.819 110.371V104.892L109.751 106.535V110.368C109.751 111.401 109.971 112.422 110.398 113.362C110.825 114.302 111.448 115.14 112.226 115.819C112.373 115.948 112.527 116.073 112.685 116.19C113.845 117.054 115.239 117.549 116.685 117.611C116.734 117.611 116.781 117.615 116.83 117.616C116.879 117.617 116.942 117.616 116.998 117.616H117.166C117.221 117.616 117.262 117.616 117.311 117.611C118.756 117.549 120.149 117.054 121.31 116.191C121.467 116.074 121.621 115.949 121.769 115.821C122.79 114.927 123.541 113.765 123.934 112.467L127.584 113.991ZM128.181 100.628V110.371C128.181 110.497 128.181 110.623 128.173 110.749L124.249 109.108V100.628C124.249 98.7551 123.523 96.9554 122.225 95.6055C120.928 94.2557 119.158 93.4604 117.287 93.3862C115.415 93.3121 113.588 93.9649 112.188 95.2078C110.787 96.4508 109.922 98.1875 109.773 100.054C109.759 100.243 109.751 100.435 109.751 100.628V103.048L105.819 101.405V89.4469H117C119.965 89.4469 122.809 90.6249 124.906 92.7217C127.003 94.8185 128.181 97.6624 128.181 100.628Z"
fill="white"
/>
</G>
</G>
</Svg>
{/* ── Floating corner icons rendered as absolute Animated.Views over the SVG ── */}
{/* Top-left — share */}
<Animated.View
style={[{ position: "absolute", top: 0, left: -10 }, styleTL]}
>
<Svg width={47} height={47} viewBox="0 0 47 47" fill="none">
<Rect
x={0.25}
y={0.25}
width={46.5}
height={46.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Rect
width={22}
height={22}
x={13}
y={12}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
/>
<Path
d="M28.125 25.75C27.6663 25.7499 27.2124 25.8417 26.7899 26.0202C26.3674 26.1987 25.9851 26.4601 25.6654 26.789L21.7037 24.2427C22.0153 23.4435 22.0153 22.5565 21.7037 21.7573L25.6654 19.211C26.2605 19.8206 27.0607 20.1875 27.911 20.2406C28.7612 20.2937 29.6008 20.0292 30.2671 19.4984C30.9334 18.9675 31.3788 18.2082 31.5171 17.3677C31.6554 16.5271 31.4766 15.6651 31.0154 14.9488C30.5542 14.2326 29.8435 13.7131 29.0211 13.4911C28.1987 13.269 27.3231 13.3603 26.5641 13.7472C25.8051 14.134 25.2169 14.7889 24.9133 15.5849C24.6098 16.3808 24.6126 17.2612 24.9212 18.0552L20.9595 20.6015C20.4824 20.1117 19.8701 19.7753 19.2009 19.6353C18.5317 19.4953 17.8359 19.5581 17.2025 19.8156C16.5691 20.0731 16.0269 20.5136 15.6452 21.0808C15.2635 21.6481 15.0596 22.3163 15.0596 23C15.0596 23.6837 15.2635 24.3519 15.6452 24.9192C16.0269 25.4864 16.5691 25.9269 17.2025 26.1844C17.8359 26.4419 18.5317 26.5047 19.2009 26.3647C19.8701 26.2247 20.4824 25.8883 20.9595 25.3985L24.9212 27.9448C24.6558 28.6294 24.6165 29.3809 24.8091 30.0893C25.0018 30.7978 25.4161 31.4259 25.9916 31.8819C26.567 32.3378 27.2733 32.5975 28.007 32.6229C28.7408 32.6484 29.4634 32.4383 30.069 32.0233C30.6747 31.6084 31.1316 31.0105 31.3729 30.3171C31.6141 29.6237 31.627 28.8712 31.4097 28.17C31.1924 27.4687 30.7562 26.8554 30.1652 26.42C29.5741 25.9845 28.8591 25.7497 28.125 25.75ZM28.125 14.75C28.5329 14.75 28.9317 14.871 29.2708 15.0976C29.61 15.3242 29.8744 15.6463 30.0305 16.0232C30.1866 16.4001 30.2274 16.8148 30.1478 17.2149C30.0683 17.615 29.8718 17.9825 29.5834 18.2709C29.2949 18.5594 28.9274 18.7558 28.5273 18.8354C28.1273 18.915 27.7126 18.8741 27.3357 18.718C26.9588 18.5619 26.6367 18.2975 26.4101 17.9584C26.1834 17.6192 26.0625 17.2204 26.0625 16.8125C26.0625 16.2655 26.2798 15.7409 26.6666 15.3541C27.0533 14.9673 27.578 14.75 28.125 14.75ZM18.5 25.0625C18.092 25.0625 17.6933 24.9415 17.3541 24.7149C17.0149 24.4883 16.7506 24.1662 16.5945 23.7893C16.4384 23.4124 16.3975 22.9977 16.4771 22.5976C16.5567 22.1975 16.7531 21.83 17.0416 21.5416C17.33 21.2532 17.6975 21.0567 18.0976 20.9771C18.4977 20.8976 18.9124 20.9384 19.2892 21.0945C19.6661 21.2506 19.9882 21.515 20.2149 21.8541C20.4415 22.1933 20.5625 22.5921 20.5625 23C20.5625 23.547 20.3452 24.0716 19.9584 24.4584C19.5716 24.8452 19.047 25.0625 18.5 25.0625ZM28.125 31.25C27.717 31.25 27.3183 31.129 26.9791 30.9024C26.6399 30.6758 26.3756 30.3537 26.2195 29.9768C26.0634 29.5999 26.0225 29.1852 26.1021 28.7851C26.1817 28.385 26.3781 28.0175 26.6666 27.7291C26.955 27.4407 27.3225 27.2442 27.7226 27.1646C28.1227 27.0851 28.5374 27.1259 28.9142 27.282C29.2911 27.4381 29.6132 27.7025 29.8399 28.0416C30.0665 28.3808 30.1875 28.7796 30.1875 29.1875C30.1875 29.7345 29.9702 30.2591 29.5834 30.6459C29.1966 31.0327 28.672 31.25 28.125 31.25Z"
fill="#008836"
/>
</Svg>
</Animated.View>
{/* Top-right — lock */}
<Animated.View
style={[{ position: "absolute", top: 28, right: -5 }, styleTR]}
>
<Svg width={32} height={32} viewBox="0 0 32 32" fill="none">
<Rect
x={0.25}
y={0.25}
width={31.5}
height={31.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Path
d="M16 14.125C15.475 14.1252 14.967 14.3144 14.57 14.658C14.172 15.0017 13.912 15.4769 13.836 15.9967C13.76 16.5165 13.874 17.0462 14.157 17.4891C14.439 17.9319 14.872 18.2583 15.375 18.4086V19.75C15.375 19.9158 15.441 20.0747 15.558 20.1919C15.675 20.3092 15.834 20.375 16 20.375C16.166 20.375 16.325 20.3092 16.442 20.1919C16.559 20.0747 16.625 19.9158 16.625 19.75V18.4086C17.128 18.2583 17.561 17.9319 17.843 17.4891C18.126 17.0462 18.24 16.5165 18.164 15.9967C18.088 15.4769 17.828 15.0017 17.43 14.658C17.033 14.3144 16.525 14.1252 16 14.125ZM16 17.25C15.815 17.25 15.633 17.195 15.479 17.092C15.325 16.989 15.205 16.8426 15.134 16.6713C15.063 16.5 15.044 16.3115 15.081 16.1296C15.117 15.9477 15.206 15.7807 15.337 15.6496C15.468 15.5185 15.635 15.4292 15.817 15.393C15.999 15.3568 16.187 15.3754 16.359 15.4464C16.53 15.5173 16.676 15.6375 16.78 15.7917C16.883 15.9458 16.937 16.1271 16.937 16.3125C16.937 16.5611 16.839 16.7996 16.663 16.9754C16.487 17.1512 16.249 17.25 16 17.25ZM22.25 11.625H19.75V9.75C19.75 8.7554 19.355 7.8016 18.652 7.0983C17.948 6.3951 16.995 6 16 6C15.005 6 14.052 6.3951 13.348 7.0983C12.645 7.8016 12.25 8.7554 12.25 9.75V11.625H9.75C9.418 11.625 9.101 11.7567 8.866 11.9911C8.632 12.2255 8.5 12.5435 8.5 12.875V21.625C8.5 21.9565 8.632 22.2745 8.866 22.5089C9.101 22.7433 9.418 22.875 9.75 22.875H22.25C22.582 22.875 22.899 22.7433 23.134 22.5089C23.368 22.2745 23.5 21.9565 23.5 21.625V12.875C23.5 12.5435 23.368 12.2255 23.134 11.9911C22.899 11.7567 22.582 11.625 22.25 11.625ZM13.5 9.75C13.5 9.087 13.763 8.4511 14.232 7.9822C14.701 7.5134 15.337 7.25 16 7.25C16.663 7.25 17.299 7.5134 17.768 7.9822C18.237 8.4511 18.5 9.087 18.5 9.75V11.625H13.5V9.75ZM22.25 21.625H9.75V12.875H22.25V21.625Z"
fill="#008836"
/>
</Svg>
</Animated.View>
{/* Bottom-left — document */}
<Animated.View
style={[{ position: "absolute", bottom: 0, left: 25 }, styleBL]}
>
<Svg width={35} height={35} viewBox="0 0 35 35" fill="none">
<Rect
x={0.25}
y={0.25}
width={34.5}
height={34.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Rect
width={18.3333}
height={18.3333}
x={8.3334}
y={8.333}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
/>
<Path
d="M23.6346 14.178L19.6242 10.168C19.5709 10.114 19.5077 10.072 19.4382 10.044C19.3686 10.015 19.2941 10 19.2188 10H12.3438C12.0399 10 11.7485 10.121 11.5336 10.336C11.3187 10.551 11.198 10.842 11.198 11.146V23.75C11.198 24.054 11.3187 24.345 11.5336 24.56C11.7485 24.775 12.0399 24.896 12.3438 24.896H22.6563C22.9602 24.896 23.2517 24.775 23.4666 24.56C23.6814 24.345 23.8022 24.054 23.8022 23.75V14.583C23.8022 14.508 23.7875 14.434 23.7587 14.364C23.7299 14.295 23.6878 14.231 23.6346 14.178ZM19.7917 11.956L21.8464 14.01H19.7917V11.956ZM22.6563 23.75H12.3438V11.146H18.6459V14.583C18.6459 14.735 18.7063 14.881 18.8137 14.989C18.9212 15.096 19.0669 15.156 19.2188 15.156H22.6563V23.75ZM20.3647 18.021C20.3647 18.173 20.3043 18.319 20.1969 18.426C20.0894 18.533 19.9437 18.594 19.7917 18.594H15.2084C15.0565 18.594 14.9107 18.533 14.8033 18.426C14.6959 18.319 14.6355 18.173 14.6355 18.021C14.6355 17.869 14.6959 17.723 14.8033 17.616C14.9107 17.508 15.0565 17.448 15.2084 17.448H19.7917C19.9437 17.448 20.0894 17.508 20.1969 17.616C20.3043 17.723 20.3647 17.869 20.3647 18.021ZM20.3647 20.313C20.3647 20.465 20.3043 20.61 20.1969 20.718C20.0894 20.825 19.9437 20.885 19.7917 20.885H15.2084C15.0565 20.885 14.9107 20.825 14.8033 20.718C14.6959 20.61 14.6355 20.465 14.6355 20.313C14.6355 20.161 14.6959 20.015 14.8033 19.907C14.9107 19.8 15.0565 19.74 15.2084 19.74H19.7917C19.9437 19.74 20.0894 19.8 20.1969 19.907C20.3043 20.015 20.3647 20.161 20.3647 20.313Z"
fill="#008836"
/>
</Svg>
</Animated.View>
{/* Bottom-right — eye-off */}
<Animated.View
style={[{ position: "absolute", bottom: 0, right: 5 }, styleBR]}
>
<Svg width={43} height={43} viewBox="0 0 43 43" fill="none">
<Rect
x={0.25}
y={0.25}
width={42.5}
height={42.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<G>
<Path
d="M15.028 3.408C14.969 3.341 14.898 3.287 14.818 3.248C14.738 3.21 14.651 3.187 14.562 3.182C14.473 3.178 14.384 3.191 14.301 3.22C14.217 3.25 14.14 3.296 14.074 3.356C14.008 3.416 13.955 3.488 13.917 3.569C13.88 3.649 13.858 3.737 13.855 3.825C13.851 3.914 13.865 4.003 13.896 4.086C13.926 4.17 13.973 4.246 14.034 4.311L15.65 6.089C12.6 7.961 11.288 10.847 11.23 10.978C11.192 11.064 11.172 11.157 11.172 11.251C11.172 11.345 11.192 11.438 11.23 11.524C11.259 11.59 11.971 13.167 13.552 14.749C15.659 16.855 18.321 17.969 21.25 17.969C22.756 17.977 24.246 17.667 25.623 17.059L27.471 19.092C27.53 19.159 27.601 19.213 27.681 19.252C27.761 19.29 27.848 19.313 27.937 19.317C28.026 19.322 28.115 19.309 28.199 19.28C28.282 19.25 28.359 19.204 28.425 19.144C28.491 19.084 28.544 19.012 28.582 18.931C28.62 18.851 28.641 18.763 28.645 18.675C28.648 18.586 28.634 18.497 28.604 18.414C28.573 18.33 28.526 18.254 28.465 18.189L15.028 3.408ZM19.003 9.777L22.503 13.628C21.976 13.905 21.372 13.999 20.785 13.896C20.199 13.793 19.663 13.497 19.263 13.057C18.862 12.616 18.62 12.055 18.573 11.461C18.526 10.868 18.677 10.275 19.003 9.777ZM21.25 16.625C18.665 16.625 16.407 15.685 14.537 13.833C13.77 13.07 13.117 12.2 12.6 11.25C12.993 10.512 14.251 8.446 16.576 7.103L18.088 8.762C17.503 9.511 17.201 10.443 17.237 11.394C17.273 12.344 17.643 13.251 18.283 13.955C18.923 14.658 19.791 15.113 20.733 15.238C21.676 15.364 22.633 15.152 23.434 14.64L24.672 16.001C23.58 16.42 22.419 16.631 21.25 16.625ZM21.754 8.61C21.579 8.577 21.424 8.475 21.324 8.328C21.224 8.181 21.186 7.999 21.22 7.824C21.253 7.649 21.355 7.495 21.502 7.394C21.65 7.294 21.831 7.257 22.006 7.29C22.862 7.456 23.642 7.895 24.229 8.54C24.815 9.185 25.178 10.003 25.262 10.871C25.279 11.049 25.224 11.225 25.11 11.363C24.997 11.5 24.833 11.586 24.656 11.603C24.635 11.604 24.614 11.604 24.593 11.603C24.425 11.603 24.263 11.541 24.138 11.428C24.014 11.316 23.936 11.16 23.921 10.993C23.864 10.416 23.623 9.872 23.232 9.442C22.842 9.013 22.323 8.721 21.754 8.61ZM31.268 11.524C31.232 11.603 30.382 13.486 28.466 15.202C28.4 15.263 28.324 15.31 28.24 15.34C28.156 15.371 28.067 15.384 27.978 15.38C27.889 15.376 27.802 15.354 27.721 15.316C27.641 15.277 27.569 15.223 27.509 15.157C27.45 15.091 27.404 15.013 27.375 14.929C27.346 14.845 27.334 14.755 27.339 14.666C27.345 14.577 27.368 14.491 27.408 14.411C27.447 14.331 27.502 14.26 27.57 14.201C28.51 13.357 29.299 12.359 29.905 11.25C29.386 10.299 28.732 9.429 27.963 8.666C26.093 6.815 23.835 5.875 21.25 5.875C20.705 5.874 20.162 5.918 19.624 6.007C19.537 6.022 19.447 6.02 19.36 6.001C19.274 5.982 19.192 5.945 19.119 5.894C19.047 5.843 18.985 5.778 18.938 5.703C18.891 5.627 18.859 5.544 18.844 5.456C18.83 5.369 18.833 5.279 18.853 5.193C18.873 5.106 18.91 5.025 18.962 4.953C19.014 4.88 19.079 4.819 19.155 4.773C19.231 4.726 19.315 4.695 19.402 4.682C20.013 4.581 20.631 4.53 21.25 4.531C24.179 4.531 26.841 5.645 28.948 7.752C30.529 9.333 31.241 10.912 31.27 10.978C31.308 11.064 31.328 11.157 31.328 11.251C31.328 11.345 31.308 11.438 31.27 11.524H31.268Z"
fill="#008836"
transform="translate(0, 10)"
/>
</G>
</Svg>
</Animated.View>
</Animated.View>
</GestureDetector>
);
};

View File

@@ -20,93 +20,99 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, useWindowDimensions, View } from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";
import { Spacing } from "../../common/design/spacing";
import { Linking, useWindowDimensions, View } from "react-native";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useRotator from "../../hooks/use-rotator";
import Navigation from "../../services/navigation";
import SettingsService from "../../services/settings";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { AuthMode } from "../auth/common";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { IntroIllustration } from "./illustration";
import { ProgressPills } from "./progress-pills";
import SettingsService from "../../services/settings";
import { SafeAreaView } from "react-native-safe-area-context";
const Intro = () => {
const { colors } = useThemeColors();
const { width } = useWindowDimensions();
const isTablet = width > 600;
const rotator = useRotator([0], 10000, true);
const insets = useGlobalSafeAreaInsets();
const isTablet = width > 600;
const renderItem = React.useCallback(
({ item }: { item: (typeof strings.introData)[0] }) => (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
<View
style={{
justifyContent: "flex-start"
justifyContent: "center",
width: isTablet ? width / 2 : width,
paddingHorizontal: isTablet ? (width / 2) * 0.05 : width * 0.05
}}
>
<View
style={{
gap: Spacing.LEVEL_2
flexDirection: "row"
}}
>
{item.headings?.map((heading, index) =>
heading.bold ? (
<Heading
key={heading.value()}
fontFamily="SEMI_BOLD"
fontSize="XXL"
lineHeight="120%"
>
{heading.value()}
</Heading>
) : (
<Paragraph
key={heading.value()}
style={{
marginTop: index !== 0 ? -5 : undefined
}}
fontFamily="MEDIUM"
fontSize="XL"
>
{heading.value()}
</Paragraph>
)
)}
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
{item.user ? (
<Paragraph fontFamily="MEDIUM" fontSize="XL">
{item.user}
</Paragraph>
) : null}
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<View
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
maxWidth: "90%",
width: "100%"
}}
>
{item.headings?.map((heading) => (
<Heading
key={heading()}
style={{
marginBottom: 5
}}
extraBold
size={AppFontSize.xxl}
>
{heading()}
</Heading>
))}
{item.body ? (
<Paragraph color={colors.primary.paragraph} fontSize="SM">
{item.body()}
</Paragraph>
<Paragraph size={AppFontSize.sm}>{item.body()}</Paragraph>
) : null}
{item.tesimonial ? (
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{
fontStyle: "italic",
fontSize: AppFontSize.lg
}}
onPress={() => {
// Linking.openURL(item.link);
Linking.openURL(item.link);
}}
>
{item.tesimonial()}
{item.tesimonial()} {item.user}
</Paragraph>
) : null}
</View>
</Animated.View>
</View>
),
[colors.primary.paragraph, colors.secondary.paragraph]
[colors.primary.accent, colors.secondary.background, isTablet, width]
);
return (
@@ -120,25 +126,17 @@ const Intro = () => {
<View
testID="notesnook.splashscreen"
style={{
flex: 1,
paddingTop: Platform.OS === "android" ? 60 - insets.top : undefined
flex: 1
}}
>
<View
style={{
flexDirection: "row",
paddingHorizontal: Spacing.LEVEL_3
}}
>
<ProgressPills activePillIndex={0} />
</View>
<View
style={[
{
width: "100%",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
flexGrow: 1
},
isTablet && {
@@ -151,34 +149,33 @@ const Intro = () => {
}
]}
>
<View
style={{
alignItems: "center",
alignSelf: "center",
paddingVertical: Spacing.LEVEL_7
<SwiperFlatList
autoplay
autoplayDelay={10}
autoplayLoop={true}
index={0}
useReactNativeGestureHandler={true}
showPagination
data={strings.introData}
paginationActiveColor={colors.primary.accent}
paginationStyleItem={{
width: 10,
height: 5,
marginRight: 4,
marginLeft: 4
}}
>
<IntroIllustration />
</View>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
{strings.introData.map((item, index) =>
index !== rotator ? null : renderItem({ item })
)}
</View>
paginationDefaultColor={colors.primary.border}
renderItem={renderItem}
/>
</View>
</View>
<View
style={{
width: isTablet ? "50%" : "100%",
justifyContent: "center",
gap: Spacing.LEVEL_2,
padding: Spacing.LEVEL_3,
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: isTablet ? 0 : DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
flexShrink: 1,
alignSelf: "center"
}}
@@ -194,7 +191,7 @@ const Intro = () => {
});
}}
type="accent"
title={strings.continue()}
title={strings.getStarted()}
/>
<Button

View File

@@ -1,53 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { Spacing } from "../../common/design/spacing";
export const ProgressPills = (props: {
activePillIndex: number;
count?: number;
}) => {
const { colors } = useThemeColors();
return (
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
{new Array(props.count || 3).fill(0).map((item, index) => (
<View
key={`${item}-progress-pill-${props.count}`}
style={{
width: props.activePillIndex === index ? 26 : 14,
height: 5,
backgroundColor:
props.activePillIndex === index
? colors.primary.accent
: colors.secondary.background,
borderRadius: 2
}}
/>
))}
</View>
);
};

View File

@@ -22,22 +22,22 @@ import {
GroupingByIdKey,
GroupingKey,
GroupOptions,
ItemType,
Item,
VirtualizedGrouping
ItemType
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject } from "react";
import { FlatList, View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import React from "react";
import { View } from "react-native";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { RouteName } from "../../../stores/use-navigation-store";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import Sort from "../../sheets/sort";
import { IconButton } from "../../ui/icon-button";
import Paragraph from "../../ui/typography/paragraph";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
type SectionHeaderProps = {
item: GroupHeader;
@@ -49,9 +49,8 @@ type SectionHeaderProps = {
group: GroupingKey;
groupId?: string;
type?: GroupingByIdKey;
onOpenJumpToDialog: () => void;
itemCount?: number;
data?: VirtualizedGrouping<Item>;
ref?: RefObject<FlatList>;
};
export const SectionHeader = React.memo<
@@ -65,13 +64,12 @@ export const SectionHeader = React.memo<
screen,
groupOptions,
group,
onOpenJumpToDialog,
itemCount,
groupId,
type,
data,
ref
type
}: SectionHeaderProps) {
const { colors } = useThemeColors("list");
const { colors } = useThemeColors();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook" | "searchResult"
);
@@ -80,9 +78,8 @@ export const SectionHeader = React.memo<
<View
style={{
width: "100%",
paddingHorizontal: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_3,
marginTop: index === 0 ? 0 : Spacing.LEVEL_3
paddingHorizontal: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
@@ -91,37 +88,58 @@ export const SectionHeader = React.memo<
alignItems: "center",
width: "100%",
alignSelf: "center",
justifyContent: "space-between"
justifyContent: "space-between",
borderBottomWidth: 1,
borderColor: colors.primary.border,
paddingBottom: 1,
paddingTop:
index === 0
? DefaultAppStyles.GAP_VERTICAL
: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
<Paragraph
fontSize="SM"
fontFamily="MEDIUM"
style={{
alignSelf: "center",
textAlignVertical: "center"
<Pressable
onPress={() => {
onOpenJumpToDialog();
}}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
style={{
justifyContent: "flex-start",
flexDirection: "row",
width: "auto"
}}
color={colors.secondary.paragraph}
>
{!item.title || item.title === ""
? screen === "Search"
? strings.results(itemCount || 0)
: strings.pinned()
: item.title}
</Paragraph>
<Heading
size={AppFontSize.xxs}
style={{
alignSelf: "center",
textAlignVertical: "center"
}}
color={color || colors.primary.accent}
>
{!item.title || item.title === ""
? screen === "Search"
? strings.results(itemCount || 0)
: strings.pinned().toUpperCase()
: item.title.toUpperCase()}
</Heading>
</Pressable>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
gap: DefaultAppStyles.GAP_SMALL
}}
>
{index === 0 ? (
<>
<IconButton
name={"sliders"}
iconFamily="notesnook"
name={
groupOptions.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
color={colors.secondary.icon}
testID="icon-sort"
onPress={() => {
@@ -137,20 +155,15 @@ export const SectionHeader = React.memo<
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}
data={data}
ref={ref}
/>
)
});
}}
style={{
width: 30,
height: 30,
borderWidth: 1,
borderRadius: Radius.XS,
borderColor: colors.secondary.border
width: 25,
height: 25
}}
size={16}
size={AppFontSize.lg - 2}
/>
<IconButton
hidden={
@@ -160,16 +173,14 @@ export const SectionHeader = React.memo<
screen !== "Search"
}
style={{
width: 30,
height: 30,
borderWidth: 1,
borderRadius: Radius.XXS,
borderColor: colors.secondary.border
width: 25,
height: 25
}}
testID="icon-compact-mode"
color={colors.secondary.icon}
name={"view-list"}
iconFamily="notesnook"
name={
isCompactModeEnabled ? "view-list" : "view-list-outline"
}
onPress={() => {
SettingsService.set({
[dataType === "notebook"
@@ -181,11 +192,20 @@ export const SectionHeader = React.memo<
: "normal"
});
}}
type={isCompactModeEnabled ? "shade" : undefined}
size={16}
size={AppFontSize.lg - 2}
/>
</>
) : null}
{/* <IconButton
style={{
width: 25,
height: 25
}}
color={colors.secondary.icon}
name={"chevron-down"}
size={SIZE.lg - 2}
/> */}
</View>
</View>
</View>

View File

@@ -25,9 +25,10 @@ import {
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { decode, EntityLevel } from "entities";
import { EntityLevel, decode } from "entities";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import useNavigationStore, {
RouteParams
@@ -41,23 +42,20 @@ import {
TagsWithDateEdited
} from "@notesnook/common";
import { strings } from "@notesnook/intl";
import dayjs from "dayjs";
import { create } from "zustand";
import { notesnook } from "../../../../e2e/test.ids";
import { FontFamily } from "../../../common/design/font";
import { Radius, Spacing } from "../../../common/design/spacing";
import useIsSelected from "../../../hooks/use-selected";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { DefaultAppStyles } from "../../../utils/styles";
import { Properties } from "../../properties";
import AppIcon from "../../ui/AppIcon";
import { ExpiryDate } from "../../ui/expiry-date";
import { IconButton } from "../../ui/icon-button";
import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import dayjs from "dayjs";
import { ExpiryDate } from "../../ui/expiry-date";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
@@ -74,24 +72,6 @@ type NoteItemProps = {
renderedInRoute?: keyof RouteParams;
};
const useShowMoreStore = create<{
showMoreStatus: Record<string, boolean>;
show: (id: string) => void;
hide: (id: string) => void;
}>((set) => ({
showMoreStatus: {},
show(id) {
set((state) => ({
showMoreStatus: { ...state.showMoreStatus, [id]: true }
}));
},
hide(id) {
set((state) => ({
showMoreStatus: { ...state.showMoreStatus, [id]: false }
}));
}
}));
const NoteItem = ({
item,
isTrash,
@@ -118,329 +98,53 @@ const NoteItem = ({
const primaryColors = isEditingNote ? colors.selected : colors.primary;
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);
const showMore = useShowMoreStore((state) => state.showMoreStatus[item.id]);
const statusIcons = [
{
condition: item.conflicted,
name: "alert-circle",
color: colors.error.accent
},
{
condition: item.localOnly,
testID: "sync-off",
name: "sync-off",
color: primaryColors.icon
},
{
condition: item.readonly,
testID: "pencil-lock",
name: "pencil-lock",
color: primaryColors.icon
},
{
condition: item.pinned,
testID: "icon-pinned",
name: "pin",
color: primaryColors.icon
},
{
condition: !!locked,
testID: "lock",
name: "lock",
color: primaryColors.icon
},
{
condition: item.favorite,
testID: "star-filled",
name: "star-filled",
color: "orange"
}
];
const activeStatusIcons = statusIcons.filter(
(statusIcon) => statusIcon.condition
);
const canShowTopStatusBar =
attachmentsCount > 0 ||
activeStatusIcons.length > 0 ||
notebooks?.items?.length ||
tags?.items.length;
return (
<>
<View
style={{
flexGrow: 1,
flexShrink: 1,
gap: compactMode ? undefined : Spacing.LEVEL_1
flexShrink: 1
}}
>
{compactMode || !canShowTopStatusBar || isTrash ? null : (
<View
{compactMode ? null : (
<Paragraph
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
columnGap: Spacing.LEVEL_0,
rowGap: Spacing.LEVEL_0,
flexWrap: "wrap"
fontSize: AppFontSize.xxxs,
color: colors.secondary.paragraph
}}
>
{activeStatusIcons.map((statusIcon) => (
<View
key={statusIcon.testID || statusIcon.name}
style={{
borderRadius: Radius.XXS,
paddingHorizontal: 3,
paddingVertical: 3,
// borderWidth: 1,
// borderColor: primaryColors.border,
flexDirection: "row",
alignItems: "center"
}}
>
<AppIcon
testID={statusIcon.testID}
name={statusIcon.name}
size={12}
iconFamily="notesnook"
color={statusIcon.color}
/>
</View>
))}
{attachmentsCount !== 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
borderRadius: Radius.XXS,
paddingHorizontal: 3,
paddingVertical: 3,
gap: Spacing.LEVEL_0
}}
>
<AppIcon
name="link"
iconFamily="notesnook"
size={12}
color={colors.secondary.icon}
/>
<Paragraph color={colors.secondary.paragraph} fontSize="XS">
{attachmentsCount}
</Paragraph>
</View>
) : null}
{notebooks?.items
?.filter(
(item) =>
renderedInRoute !== "Notebook" ||
item.id !== useNavigationStore.getState().focusedRouteId
)
.filter((_, index) => showMore || index < 1)
.map((item) => (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<AppIcon
name="bookmark"
iconFamily="notesnook"
size={AppFontSize.xs}
color={colors.secondary.icon}
/>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{item.title}
</Paragraph>
</View>
))}
{tags?.items
?.filter((_, index) => showMore || index < 1)
.map((item) =>
item.id ? (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{item.title}
</Paragraph>
</View>
) : null
)}
{(() => {
const filteredNotebooks = (notebooks?.items || []).filter(
(nb) =>
renderedInRoute !== "Notebook" ||
nb.id !== useNavigationStore.getState().focusedRouteId
);
const filteredTags = (tags?.items || []).filter((t) => t.id);
const totalNotebooks = filteredNotebooks.length;
const totalTags = filteredTags.length;
const hasMore = totalNotebooks > 1 || totalTags > 1;
if (!hasMore) return null;
const hiddenCount =
(totalNotebooks > 1 ? totalNotebooks - 1 : 0) +
(totalTags > 1 ? totalTags - 1 : 0);
return (
<Heading
size={AppFontSize.xs}
color={colors.primary.accent}
onPress={() => {
if (showMore) {
useShowMoreStore.getState().hide(item.id);
} else {
useShowMoreStore.getState().show(item.id);
}
}}
>
{showMore ? "Show less" : `+${hiddenCount} ${strings.more()}`}
</Heading>
);
})()}
</View>
{getFormattedDate(
date,
dayjs(date).isBefore(dayjs().subtract(1, "day").hour(23))
? "date"
: "time"
)}
</Paragraph>
)}
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
{color && !compactMode ? (
<View
style={{
width: 8,
height: 8,
borderRadius: 100,
backgroundColor: color?.colorCode
}}
/>
) : null}
<View
{compactMode ? (
<Paragraph
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={AppFontSize.sm}
style={{
flexDirection: "row",
justifyContent: "space-between",
flexGrow: 1,
alignItems: "center"
paddingRight: 10
}}
>
<Heading
numberOfLines={1}
color={primaryColors.heading}
fontSize="MD"
style={{
flexShrink: 1
}}
>
{item.title}
</Heading>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
{color && compactMode ? (
<View
style={{
width: 8,
height: 8,
borderRadius: 100,
backgroundColor: color?.colorCode
}}
/>
) : null}
{compactMode ? (
<>
{item.conflicted ? (
<AppIcon
name="warning-circle"
size={12}
iconFamily="notesnook"
color={colors.error.accent}
/>
) : null}
{locked ? (
<AppIcon
name="lock"
testID="lock"
iconFamily="notesnook"
size={12}
color={primaryColors.icon}
/>
) : null}
{item.favorite ? (
<AppIcon
testID="icon-star"
name="star-filled"
iconFamily="notesnook"
size={12}
color={colors.static.orange}
/>
) : null}
<TimeSince
style={{
fontSize: AppFontSize.xs,
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
</>
) : null}
<IconButton
testID={notesnook.listitem.menu}
color={colors.secondary.icon}
name="dots-three"
iconFamily="notesnook"
size={20}
onPress={() => !noOpen && Properties.present(item)}
style={{
justifyContent: "center",
height: undefined,
width: undefined,
borderRadius: 100,
alignItems: "center"
}}
/>
</View>
</View>
</View>
{item.title}
</Paragraph>
) : (
<Heading
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={AppFontSize.sm}
style={{
paddingRight: 10
}}
>
{item.title}
</Heading>
)}
{item.headline && !compactMode ? (
<Paragraph
@@ -448,7 +152,6 @@ const NoteItem = ({
flexWrap: "wrap"
}}
color={primaryColors.paragraph}
fontSize="SM"
numberOfLines={2}
>
{decode(item.headline, {
@@ -457,96 +160,218 @@ const NoteItem = ({
</Paragraph>
) : null}
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
{isTrash ? (
<Paragraph
color={colors.secondary.paragraph}
fontSize="XS"
style={{
marginRight: 6
}}
>
{strings.note()}
</Paragraph>
) : null}
{compactMode ? null : (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
columnGap: 8,
rowGap: 4,
flexWrap: "wrap"
}}
>
{!isTrash ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
size={AppFontSize.sm}
color={colors.error.accent}
/>
) : null}
{compactMode ? null : (
<View
style={{
gap: Spacing.LEVEL_0,
flexDirection: "row"
}}
>
<AppIcon
color={colors.secondary.icon}
size={12}
style={{
marginTop: 1
}}
name="trash-alt"
iconFamily="notesnook"
/>
<Paragraph
style={{
color: colors.secondary.paragraph
}}
fontSize="XS"
>
{isTrash
? strings.deletedOn(
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)
{item.localOnly ? (
<Icon
testID="sync-off"
name="sync-off"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
) : null}
{item.readonly ? (
<Icon
testID="pencil-lock"
name="pencil-lock"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
) : null}
{attachmentsCount > 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 2
}}
>
<Icon
name="attachment"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
>
{attachmentsCount}
</Paragraph>
</View>
) : null}
{item.pinned ? (
<Icon
testID="icon-pinned"
name="pin-outline"
size={AppFontSize.sm}
color={color?.colorCode || primaryColors.accent}
/>
) : null}
{locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star-outline"
size={AppFontSize.sm}
color="orange"
/>
) : null}
{reminder ? (
<ReminderTime
reminder={reminder}
color={color?.colorCode}
textStyle={{
fontSize: AppFontSize.xxs
}}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
) : null}
{item.expiryDate?.value ? (
<ExpiryDate
note={item as Note}
color={color?.colorCode}
textStyle={{ fontSize: AppFontSize.xxs }}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
) : null}
{notebooks?.items
?.filter(
(item) =>
renderedInRoute !== "Notebook" ||
item.id !== useNavigationStore.getState().focusedRouteId
)
.map((item) => (
<View
key={item.id}
style={{
borderRadius: 4,
backgroundColor: colors.secondary.background,
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingVertical: 1,
flexDirection: "row",
alignItems: "center",
gap: DefaultAppStyles.GAP_SMALL / 2
}}
>
<AppIcon
name="book-outline"
size={AppFontSize.xxxs}
color={colors.secondary.icon}
/>
<Paragraph
size={AppFontSize.xxxs}
color={colors.secondary.paragraph}
>
{item.title}
</Paragraph>
</View>
))}
{!isTrash && !compactMode && tags
? tags.items?.map((item) =>
item.id ? (
<View
key={item.id}
style={{
borderRadius: 4,
backgroundColor: colors.secondary.background,
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
borderWidth: 0.5,
borderColor:
color?.colorCode || primaryColors.border,
paddingVertical: 1
}}
>
<Paragraph
size={AppFontSize.xxxs}
color={colors.secondary.paragraph}
>
#{item.title}
</Paragraph>
</View>
) : null
)
: getFormattedDate(
date,
dayjs(date).isBefore(dayjs().subtract(1, "day").hour(23))
? "date"
: "time"
)}
</Paragraph>
</View>
)}
: null}
</>
) : (
<>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
style={{
marginRight: 6
}}
>
{item && item.dateDeleted
? strings.deletedOn(
new Date(item.dateDeleted).toISOString().slice(0, 10)
)
: null}
</Paragraph>
{item.expiryDate?.value ? (
<ExpiryDate
note={item as Note}
color={color?.colorCode}
textStyle={{ fontSize: AppFontSize.xxs }}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
) : null}
{reminder ? (
<ReminderTime
reminder={reminder}
color={color?.colorCode}
textStyle={{
fontSize: AppFontSize.xs,
fontFamily: FontFamily.REGULAR
}}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start",
backgroundColor: colors.primary.shade
}}
/>
) : null}
</View>
<Paragraph
color={primaryColors.accent}
size={AppFontSize.xxs}
style={{
marginRight: 6
}}
>
{strings.note()}
</Paragraph>
</>
)}
</View>
)}
</View>
<View
style={{
@@ -554,6 +379,55 @@ const NoteItem = ({
alignItems: "center"
}}
>
{compactMode ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
marginRight: 6
}}
size={AppFontSize.sm}
color={colors.error.accent}
/>
) : null}
{locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={AppFontSize.sm}
style={{
marginRight: 6
}}
color={primaryColors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star-outline"
size={AppFontSize.sm}
style={{
marginRight: 6
}}
color={colors.static.orange}
/>
) : null}
<TimeSince
style={{
fontSize: AppFontSize.xxs,
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
</>
) : null}
{selectionMode === "note" || selectionMode === "trash" ? (
<>
<View
@@ -571,7 +445,22 @@ const NoteItem = ({
/>
</View>
</>
) : null}
) : (
<IconButton
testID={notesnook.listitem.menu}
color={colors.secondary.icon}
name="dots-horizontal"
size={AppFontSize.lg}
onPress={() => !noOpen && Properties.present(item)}
style={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
/>
)}
</View>
</>
);

View File

@@ -23,17 +23,20 @@ import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import { eSendEvent, hideSheet } from "../../../services/event-manager";
import {
eSendEvent,
hideSheet,
presentSheet
} from "../../../services/event-manager";
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 Navigation from "../../../services/navigation";
import { RouteParams } from "../../../stores/use-navigation-store";
import SelectionWrapper from "../selection-wrapper";
import { selectItem } from "../../../stores/use-selection-store";
import NotePreview from "../../note-history/preview";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
export const openNote = async (
item: Note,
@@ -56,7 +59,11 @@ export const openNote = async (
if (isTrash) {
if (!note.contentId) return;
Navigation.navigate("NotePreview", { note: item });
const content = await db.content.get(note.contentId as string);
presentSheet({
component: <NotePreview note={item} content={content} />
});
} else {
if (!useTabStore.getState().hasTabForNote(note.id!)) {
editorController.current.commands.setLoading(
@@ -87,7 +94,6 @@ type NoteWrapperProps = {
isRenderedInActionSheet: boolean;
locked?: boolean;
renderedInRoute?: keyof RouteParams;
hasGroupHeader?: boolean;
};
export const NoteWrapper = React.memo<
@@ -97,7 +103,6 @@ export const NoteWrapper = React.memo<
item,
index,
isRenderedInActionSheet,
hasGroupHeader,
...restProps
}: NoteWrapperProps) {
const isTrash = item.type === "trash";
@@ -108,7 +113,6 @@ export const NoteWrapper = React.memo<
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
isSheet={isRenderedInActionSheet}
item={item}
hasGroupHeader={hasGroupHeader}
index={index}
color={restProps.color?.colorCode}
>

View File

@@ -27,8 +27,7 @@ import Navigation from "../../../services/navigation";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper from "../selection-wrapper";
import { selectItem } from "../../../stores/use-selection-store";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {

View File

@@ -36,8 +36,7 @@ import { IconButton } from "../../ui/icon-button";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
import { selectItem } from "../../../stores/use-selection-store";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
const ReminderItem = React.memo(
({

View File

@@ -71,7 +71,6 @@ export const SearchResult = (props: SearchResultProps) => {
}}
onLongPress={async () => {
const note = await db.notes.note(props.item.id);
if (!note) return;
Properties.present(note);
}}
onPress={async () => openNote()}
@@ -152,7 +151,6 @@ export const SearchResult = (props: SearchResultProps) => {
}}
onLongPress={async () => {
const note = await db.notes.note(props.item.id);
if (!note) return;
Properties.present(note);
}}
onPress={() => {

View File

@@ -23,9 +23,26 @@ import React, { PropsWithChildren, useRef } from "react";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { DefaultAppStyles } from "../../../utils/styles";
import { Pressable } from "../../ui/pressable";
import { View } from "react-native";
import { Spacing } from "../../../common/design/spacing";
export function selectItem(item: Item) {
if (useSelectionStore.getState().selectionMode === item.type) {
const { selectionMode, clearSelection, setSelectedItem } =
useSelectionStore.getState();
if (selectionMode === item.type) {
setSelectedItem(item.id);
}
if (useSelectionStore.getState().selectedItemsList.length === 0) {
clearSelection();
}
return true;
}
return false;
}
type SelectionWrapperProps = PropsWithChildren<{
item: Item;
@@ -34,7 +51,6 @@ type SelectionWrapperProps = PropsWithChildren<{
isSheet?: boolean;
color?: string;
index?: number;
hasGroupHeader?: boolean;
}>;
const SelectionWrapper = ({
@@ -44,7 +60,6 @@ const SelectionWrapper = ({
isSheet,
children,
color,
hasGroupHeader,
index = 0
}: SelectionWrapperProps) => {
const itemId = useRef(item.id);
@@ -71,55 +86,47 @@ const SelectionWrapper = ({
};
return (
<>
{hasGroupHeader ? null : (
<Pressable
customColor={
isEditingNote
? colors.selected.background
: isSheet
? colors.primary.hover
: "transparent"
}
testID={testID}
onLongPress={onLongPress}
onPress={onPress}
customSelectedColor={colors.primary.hover}
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={1}
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
alignSelf: "center",
overflow: "hidden",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: compactMode ? 4 : DefaultAppStyles.GAP_VERTICAL,
borderRadius: isSheet ? 10 : 0,
marginBottom: isSheet ? DefaultAppStyles.GAP_VERTICAL : undefined
}}
>
{isEditingNote ? (
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
backgroundColor: color || colors.selected.accent,
position: "absolute",
bottom: 0,
top: 0,
left: 0,
width: 5
}}
>
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.separator,
width: "100%"
}}
/>
</View>
)}
<View
style={{
backgroundColor: isEditingNote
? colors.selected.background
: undefined
}}
>
<Pressable
customColor={isSheet ? colors.primary.hover : "transparent"}
testID={testID}
onLongPress={onLongPress}
onPress={onPress}
customSelectedColor={colors.primary.hover}
noborder
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={1}
style={{
paddingHorizontal: Spacing.LEVEL_3,
paddingBottom: Spacing.LEVEL_2,
paddingTop: hasGroupHeader ? 0 : Spacing.LEVEL_2
}}
>
<View
style={{
width: "100%",
flexDirection: "row"
}}
>
{children}
</View>
</Pressable>
</View>
</>
/>
) : null}
{children}
</Pressable>
);
};

View File

@@ -28,8 +28,7 @@ import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
import { selectItem } from "../../../stores/use-selection-store";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";

View File

@@ -21,11 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Dimensions, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Radius, Spacing } from "../../common/design/spacing";
import { Message, useMessageStore } from "../../stores/use-message-store";
import AppIcon from "../ui/AppIcon";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const Card = ({
@@ -48,18 +47,15 @@ export const Card = ({
<View
style={{
width: "100%",
paddingHorizontal: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_4
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Pressable
onPress={messageBoardState.onPress}
type="plain"
style={{
paddingVertical: Spacing.LEVEL_3,
paddingHorizontal: Spacing.LEVEL_2,
backgroundColor: colors.primary.shade,
borderRadius: Radius.S,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
@@ -70,24 +66,23 @@ export const Card = ({
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
flexShrink: 1,
gap: Spacing.LEVEL_1
width: "100%"
}}
>
<View
style={{
width: 34,
height: 34,
borderRadius: Radius.XS,
width: 40 * fontScale,
height: 40 * fontScale,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
justifyContent: "center"
}}
>
<Icon
size={16}
color={colors.primary.icon}
size={AppFontSize.xxxl}
color={
messageBoardState.type === "error" ? colors.error.icon : color
}
allowFontScaling
name={messageBoardState.icon}
/>
@@ -95,27 +90,25 @@ export const Card = ({
<View
style={{
gap: Spacing.LEVEL_0,
flexShrink: 1
marginLeft: 10,
marginRight: 10
}}
>
<Heading
<Paragraph
style={{
flexWrap: "nowrap",
flexShrink: 1
}}
fontSize="MD"
size={AppFontSize.sm}
color={colors.primary.heading}
>
{messageBoardState.actionText}
</Heading>
<Paragraph color={colors.secondary.paragraph} fontSize="SM">
</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
{messageBoardState.message}
</Paragraph>
</View>
</View>
<AppIcon size={24} name="chevron-right" color={colors.primary.icon} />
</Pressable>
</View>
);

View File

@@ -21,10 +21,13 @@ import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import { Spacing } from "../../common/design/spacing";
import { TTip, useTip } from "../../services/tip-manager";
import { RouteParams } from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -55,13 +58,22 @@ export const Empty = React.memo(function Empty({
screen
}: EmptyListProps) {
const { colors } = useThemeColors();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
return (
<View
style={[
{
flex: 1,
width: "100%",
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
@@ -69,53 +81,50 @@ export const Empty = React.memo(function Empty({
>
{!loading ? (
<>
<View
<Tip
color={color}
tip={
screen !== "Search"
? tip || ({ text: () => placeholder?.paragraph } as TTip)
: ({ text: () => placeholder?.paragraph } as TTip)
}
style={{
alignItems: "center",
gap: Spacing.LEVEL_1
backgroundColor: "transparent",
paddingHorizontal: 0
}}
>
<Heading size={AppFontSize.md} color={colors.primary.heading}>
{placeholder?.title}
</Heading>
<Paragraph
/>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="secondaryAccented"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
style={{
textAlign: "center",
marginBottom: Spacing.LEVEL_2,
maxWidth: "80%"
alignSelf: "flex-start"
}}
fontSize="SM"
color={colors.primary.paragraph}
>
{placeholder?.paragraph}
</Paragraph>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="accent"
title={placeholder?.button}
iconPosition="right"
onPress={placeholder?.action}
/>
)}
</View>
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "center",
width: "100%",
gap: Spacing.LEVEL_2
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={AppFontSize.sm} textBreakStrategy="balanced">
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={AppFontSize.lg}
color={color || colors.primary.accent}
/>
<Paragraph>{placeholder?.loading}</Paragraph>
</View>
</>
)}

View File

@@ -43,7 +43,9 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { getGroupOptions } from "../../hooks/use-group-options";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
@@ -188,8 +190,12 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
ref={props.scrollRef}
data={items}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
@@ -199,9 +205,6 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
hasGroupHeader={
groupHeader && previousIndex.current === index && !isSheet
}
attachmentsCount={attachmentsCount.current}
date={getDate(item as Note, group)}
isRenderedInActionSheet={isSheet}
@@ -226,8 +229,12 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
ref={props.scrollRef}
data={items}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NotebookWrapper
@@ -253,8 +260,12 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
groupId={props.groupId}
groupOptions={groupOptions}
ref={props.scrollRef}
data={items}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<ReminderItem
@@ -278,8 +289,12 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
ref={props.scrollRef}
data={items}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<TagItem
@@ -304,8 +319,12 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
color={props.customAccentColor}
groupOptions={groupOptions}
itemCount={items?.placeholders.length}
ref={props.scrollRef}
data={items}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<SearchResult item={item as HighlightedResult} />

View File

@@ -18,19 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View, ViewStyle } from "react-native";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { ProgressBarComponent } from "../ui/svg/lazy";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SvgView } from "../ui/svg";
import { Radius, Spacing } from "../../common/design/spacing";
export const Loading = (props: {
title?: string;
description?: string;
icon?: string;
svgSrc?: string;
style?: ViewStyle;
}) => {
const { colors } = useThemeColors();
return (
@@ -41,38 +37,18 @@ export const Loading = (props: {
backgroundColor: colors.primary.background,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: Spacing.LEVEL_3,
...props.style
paddingHorizontal: 16
}}
>
{props.icon ? (
<Icon name={props.icon} size={80} color={colors.primary.accent} />
) : null}
{props.svgSrc ? (
<View
style={{
marginBottom: Spacing.LEVEL_7
}}
>
<SvgView
src={props.svgSrc}
style={{
width: 180,
height: 180
}}
/>
</View>
) : null}
{props.title ? (
<Heading
style={{
textAlign: "center",
marginBottom: Spacing.LEVEL_1
textAlign: "center"
}}
fontSize="XL"
lineHeight={null}
>
{props.title}
</Heading>
@@ -81,10 +57,8 @@ export const Loading = (props: {
{props.description ? (
<Paragraph
style={{
textAlign: "center",
marginBottom: Spacing.LEVEL_4
textAlign: "center"
}}
fontSize="SM"
>
{props.description}
</Paragraph>
@@ -93,15 +67,15 @@ export const Loading = (props: {
<View
style={{
flexDirection: "row",
width: 300
width: 100,
marginTop: 15
}}
>
<ProgressBarComponent
height={7}
width={300}
height={5}
width={100}
animated={true}
useNativeDriver
borderRadius={Radius.XS}
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}

View File

@@ -72,6 +72,7 @@ const MergeConflicts = () => {
const insets = useGlobalSafeAreaInsets();
const content = useRef<UnencryptedContentItem>(null);
const { height } = useSettingStore((state) => state.dimensions);
const keepScreenOn = useSettingStore((state) => state.settings.keepScreenOn);
const applyChanges = async () => {
const contentToSave = selectedContent;
@@ -155,9 +156,9 @@ const MergeConflicts = () => {
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
@@ -182,9 +183,9 @@ const MergeConflicts = () => {
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
@@ -385,7 +386,7 @@ const MergeConflicts = () => {
paddingTop: insets.top
}}
>
<KeepAwake />
{keepScreenOn && <KeepAwake />}
{dialogVisible && (
<BaseDialog visible={true}>
<DialogContainer>

View File

@@ -0,0 +1,265 @@
/*
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, useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { LegendList } from "@legendapp/list";
import { getFormattedDate, getTimeAgo } from "@notesnook/common";
import { HistorySession, Note, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useDBItem } from "../../hooks/use-db-item";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
import { presentDialog } from "../dialog/functions";
import { Dialog } from "../dialog";
const HistoryItem = ({
index,
items,
note,
refresh
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
note: Note;
refresh: () => void;
}) => {
const [item] = useDBItem(index, "noteHistory", items);
const { colors } = useThemeColors();
const getDate = (start: number, end: number) => {
const _start_date = getFormattedDate(start, "date");
const _end_date = getFormattedDate(end + 60000, "date");
const _start_time = getFormattedDate(start, "time");
const _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_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}
update={refresh}
/>
),
context: "note_history"
});
},
[note, refresh]
);
return (
<Pressable
type="secondary"
onPress={() => {
if (!item) return;
preview(item);
}}
style={{
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
height: 45,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row"
}}
>
{!item ? null : (
<>
<Paragraph>{getDate(item.dateCreated, item.dateModified)}</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
{getTimeAgo(item.dateModified)}
</Paragraph>
</>
)}
</Pressable>
);
};
export default function NoteHistory({
note,
fwdRef
}: {
note: Note;
fwdRef: RefObject<ActionSheetRef>;
}) {
const [history, setHistory] = useState<VirtualizedGrouping<HistorySession>>();
const [_loading, setLoading] = useState(true);
const { colors } = useThemeColors();
const refresh = useCallback(() => {
db.noteHistory
.get(note.id)
.sorted({
sortBy: "dateModified",
sortDirection: "desc"
})
.then((result) => {
setHistory(result);
setLoading(false);
})
.catch((e) => {
setLoading(false);
});
}, [note.id]);
useEffect(() => {
refresh();
}, [refresh]);
const renderItem = useCallback(
({ index }: { index: number }) => (
<HistoryItem
index={index}
items={history}
note={note}
refresh={refresh}
/>
),
[history, note, refresh]
);
return (
<View>
<SheetProvider context="note_history" />
<Dialog context="local" />
<DialogHeader
title={strings.noteHistory()}
padding={12}
button={
history?.placeholders.length
? {
title: strings.clearHistory(),
type: "secondary",
onPress: () => {
presentDialog({
title: strings.clearHistory(),
paragraph: strings.clearHistoryConfirmation(),
positiveText: strings.clear(),
negativeText: strings.cancel(),
positiveType: "errorShade",
context: "local",
positivePress: async () => {
await db.noteHistory.clearSessions(note.id);
fwdRef.current?.hide();
ToastManager.show({
heading: strings.historyCleared(),
type: "success"
});
}
});
}
}
: undefined
}
/>
<Seperator />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
height: !history?.placeholders.length
? 300
: (history.placeholders.length + 1) * 55,
maxHeight: "100%"
}}
>
<LegendList
renderScrollComponent={(props) => <ScrollView {...props} />}
data={history?.placeholders || []}
estimatedItemSize={55}
extraData={history}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 300,
gap: 10
}}
>
{_loading ? (
<ActivityIndicator
size={AppFontSize.xl}
color={colors.primary.accent}
/>
) : (
<>
<Icon name="history" size={50} color={colors.primary.icon} />
<Paragraph color={colors.secondary.paragraph}>
{strings.noteHistoryPlaceholder()}
</Paragraph>
</>
)}
</View>
}
renderItem={renderItem}
/>
</View>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center"
}}
>
{strings.noteHistoryNotice[0]()}{" "}
<Text
onPress={() => {
openLinkInBrowser(
"https://help.notesnook.com/note-version-history"
);
}}
style={{
color: colors.primary.accent,
textDecorationLine: "underline"
}}
>
{strings.noteHistoryNotice[1]()}
</Text>
</Paragraph>
</View>
);
}

View File

@@ -0,0 +1,237 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import { ToastManager, eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-store";
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
import { Dialog } from "../dialog";
import DialogHeader from "../dialog/dialog-header";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import {
HistorySession,
isEncryptedContent,
Note,
NoteContent,
TrashOrItem
} from "@notesnook/core";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({
session,
content,
note,
update
}: {
session?: HistorySession & { session: string };
content:
| Partial<
NoteContent<boolean> & {
title: string;
}
>
| undefined;
note: TrashOrItem<Note>;
update?: () => void;
}) {
const { colors } = useThemeColors();
const [locked, setLocked] = useState(false);
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode();
ToastManager.show({
heading: strings.noteRestored(),
type: "success"
});
eSendEvent(eCloseSheet);
return;
}
if (session) {
await db.noteHistory.restore(session.id);
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
const note = editorController.current.note.current[session?.noteId];
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
}
}
eSendEvent(eCloseSheet, "note_history");
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteRestoredFromHistory(),
type: "success"
});
}
}
useEffect(() => {
db.vaults.itemExists(note).then((locked) => setLocked(locked));
}, [note]);
const deleteNote = async () => {
const isSession = !!session;
presentDialog({
title: isSession ? strings.deleteVersion() : strings.deleteNote(),
paragraph: isSession
? strings.deleteVersionConfirmation()
: strings.deleteNoteConfirmation(),
positiveText: strings.delete(),
negativeText: strings.cancel(),
context: "preview",
positivePress: async () => {
if (isSession) {
await db.noteHistory.remove(session.id);
update?.();
eSendEvent(eCloseSheet, "note_history");
Navigation.queueRoutesForUpdate();
} else if (note.type === "trash") {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode();
eSendEvent(eCloseSheet);
}
ToastManager.show({
heading: isSession ? strings.versionDeleted() : strings.noteDeleted(),
type: "success",
context: isSession ? "local" : "global"
});
return true;
},
positiveType: "error"
});
};
return (
<View
style={{
height: locked || session?.locked ? null : 600,
width: "100%"
}}
>
<Dialog context="preview" />
<DialogHeader
padding={12}
title={content?.title || note.title || session?.session}
/>
{!session?.locked && !locked && content?.data ? (
<View
style={{
flex: 1,
backgroundColor: colors.primary.background
}}
>
<ReadonlyEditor
editorId="historyPreview"
onLoad={async (loadContent) => {
try {
if (content?.data) {
const currentContent = note?.contentId
? await db.content.get(note.contentId)
: undefined;
if (
currentContent?.data &&
!isEncryptedContent(currentContent)
) {
loadContent({
data: diff(
currentContent?.data || "<p></p>",
content.data as string
),
id: session?.noteId || note.id
});
}
}
} catch (e) {
ToastManager.error(
e as Error,
"Failed to load history preview",
"local"
);
}
}}
/>
</View>
) : (
<View
style={{
width: "100%",
height: 100,
justifyContent: "center",
alignItems: "center"
}}
>
<Paragraph color={colors.secondary.paragraph}>
{!content?.data
? strings.noContent()
: strings.encryptedNoteHistoryNotice()}
</Paragraph>
</View>
)}
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Button
onPress={restore}
title={strings.restore()}
type="accent"
width="100%"
/>
<Button
onPress={deleteNote}
title={strings.deletePermanently()}
type="error"
width="100%"
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
/>
</View>
</View>
);
}

View File

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

View File

@@ -1,185 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFeaturesTable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ScrollView, useWindowDimensions, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import usePricingPlans from "../../hooks/use-pricing-plans";
import { AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Steps } from "./common";
import { Radius, Spacing } from "../../common/design/spacing";
export const ComparePlans = React.memo(
(props: {
pricingPlans?: ReturnType<typeof usePricingPlans>;
setStep: (step: number) => void;
}) => {
const { colors } = useThemeColors();
const { width } = useWindowDimensions();
const isTablet = width > 600;
return (
<ScrollView
horizontal
style={{
width: isTablet ? "100%" : undefined,
paddingLeft: Spacing.LEVEL_3
}}
contentContainerStyle={{
flexDirection: "column"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{["Features", "Free", "Essential", "Pro", "Believer"].map(
(plan, index) => (
<View
key={plan}
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: Spacing.LEVEL_2,
backgroundColor:
index === 0 ? colors.secondary.background : undefined,
borderBottomWidth: index === 0 ? 1 : undefined,
borderBottomColor: colors.primary.border
}}
>
<Heading size={AppFontSize.sm}>{plan}</Heading>
</View>
)
)}
</View>
{getFeaturesTable().map((item, keyIndex) => {
return (
<View
key={`${item[0] + item[1]}`}
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{item.map((featureItem, index) => (
<View
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: Spacing.LEVEL_2,
backgroundColor:
index === 0 ? colors.secondary.background : undefined,
borderBottomWidth: index === 0 ? 1 : undefined,
borderBottomColor: colors.primary.border
}}
key={item[0] + index}
>
{typeof featureItem === "string" ? (
<Heading size={AppFontSize.sm}>
{featureItem as string}
</Heading>
) : (
<>
{typeof featureItem.caption === "string" ||
typeof featureItem.caption === "number" ? (
<Paragraph>
{featureItem.caption === "infinity"
? "∞"
: featureItem.caption}
</Paragraph>
) : typeof featureItem.caption === "boolean" ? (
<>
{featureItem.caption === true ? (
<Icon
color={colors.primary.accent}
size={AppFontSize.sm}
name="check"
/>
) : (
<Paragraph color={colors.secondary.paragraph}>
-
</Paragraph>
)}
</>
) : null}
</>
)}
</View>
))}
</View>
);
})}
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{["features", "free", "essential", "pro", "believer"].map(
(plan, index) => (
<View
key={plan + "btn"}
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: Spacing.LEVEL_1
}}
>
{plan !== "free" && plan !== "features" ? (
<Button
title={strings.select()}
type="accent"
fontSize={AppFontSize.xs}
style={{
paddingVertical: Spacing.LEVEL_1,
borderRadius: Radius.XS,
alignSelf: "flex-start"
}}
onPress={() => {
props.pricingPlans?.selectPlan(plan);
props.setStep(Steps.buy);
}}
/>
) : null}
</View>
)
)}
</View>
</ScrollView>
);
},
() => true
);
ComparePlans.displayName = "ComparePlans";

View File

@@ -1,73 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import { AppFontSize } from "../../utils/size";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const FAQItem = (props: { question: string; answer: string }) => {
const [expanded, setExpanded] = useState(false);
const { colors } = useThemeColors();
return (
<TouchableOpacity
style={{
padding: 16,
backgroundColor: colors.secondary.background,
borderRadius: 10,
marginBottom: 10,
gap: 12
}}
activeOpacity={0.9}
onPress={() => {
setExpanded(!expanded);
}}
key={props.question}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
<Heading
style={{
flexShrink: 1
}}
size={AppFontSize.md}
>
{props.question}
</Heading>
<Icon
name={expanded ? "chevron-up" : "chevron-down"}
color={colors.secondary.icon}
size={AppFontSize.xxl}
/>
</View>
{expanded ? (
<Paragraph size={AppFontSize.md}>{props.answer}</Paragraph>
) : null}
</TouchableOpacity>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,315 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import {
ActivityIndicator,
Text,
useWindowDimensions,
View
} from "react-native";
import * as RNIap from "react-native-iap";
//@ts-ignore
import { Radius, Spacing } from "../../common/design/spacing";
import usePricingPlans, {
PlanOverView,
PricingPlan
} from "../../hooks/use-pricing-plans";
import PremiumService from "../../services/premium";
import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Steps } from "./common";
export const PricingPlanCard = ({
plan,
pricingPlans,
annualBilling,
setStep
}: {
plan: PricingPlan;
pricingPlans?: ReturnType<typeof usePricingPlans>;
annualBilling?: boolean;
setStep: (step: number) => void;
}) => {
const { colors } = useThemeColors();
const regionalDiscount =
plan.id === "pro" && annualBilling
? pricingPlans?.regionalDiscount
: undefined;
const { width } = useWindowDimensions();
const isTablet = width > 600;
const product =
plan.subscriptions?.[
regionalDiscount?.sku ||
`notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
];
const WebPlan = pricingPlans?.getWebPlan(
plan.id,
annualBilling ? "yearly" : "monthly"
);
const price = pricingPlans?.getPrice(
pricingPlans.isGithubRelease && WebPlan
? WebPlan
: (product as RNIap.Subscription),
pricingPlans.hasTrialOffer(plan.id, product?.productId) ? 1 : 0,
annualBilling
);
const isSubscribed =
product?.productId &&
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
pricingPlans.isSubscribed();
const isNotReady =
pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross);
return (
<View
style={{
...getElevationStyle(3),
backgroundColor: colors.secondary.background,
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: Radius.LG,
padding: 16,
flexShrink: isTablet ? 1 : undefined,
flexDirection: "column",
justifyContent: "space-between",
gap: 6
}}
>
<View>
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
justifyContent: "space-between",
marginBottom: Spacing.LEVEL_1
}}
>
<Heading size={AppFontSize.xl}>{plan.name} </Heading>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
{isSubscribed ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
padding: Spacing.LEVEL_0,
alignItems: "center",
justifyContent: "center",
alignSelf: "flex-start",
marginBottom: Spacing.LEVEL_1
}}
>
<Heading color={colors.static.white} size={AppFontSize.xxs}>
{strings.currentPlan()}
</Heading>
</View>
) : (
<>
{regionalDiscount?.discount || WebPlan?.discount ? (
<View
style={{
backgroundColor: colors.static.red,
borderRadius: 100,
padding: Spacing.LEVEL_0,
paddingHorizontal: Spacing.LEVEL_1,
justifyContent: "center",
alignSelf: "flex-start"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xxs}>
{strings.percentOff(
`${regionalDiscount?.discount || WebPlan?.discount?.amount}`
)}
</Heading>
</View>
) : null}
{plan.recommended ? (
<View
style={{
backgroundColor: colors.static.red,
borderRadius: 100,
padding: Spacing.LEVEL_0,
paddingHorizontal: Spacing.LEVEL_1
}}
>
<Text
style={{
color: colors.static.white,
fontSize: AppFontSize.xxs
}}
>
{strings.recommended()}
</Text>
</View>
) : null}
</>
)}
</View>
</View>
<Paragraph>{plan.description}</Paragraph>
<View
style={{
marginTop: Spacing.LEVEL_3
}}
>
{pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross) ? (
<ActivityIndicator size="small" color={colors.primary.accent} />
) : (
<View>
<Heading size={24}>
{price ||
`${WebPlan?.price?.currency} ${WebPlan?.price?.gross}`}{" "}
<Paragraph color={colors.secondary.heading} fontSize="SM">
/{strings.month()}
</Paragraph>
</Heading>
{!product && !WebPlan ? null : (
<Paragraph
color={colors.primary.paragraph}
size={AppFontSize.xs}
>
{annualBilling
? strings.billedAnnually(
pricingPlans?.getStandardPrice(
(product || WebPlan) as any
) as string
)
: strings.billedMonthly(
pricingPlans?.getStandardPrice(
(product || WebPlan) as any
) as string
)}
</Paragraph>
)}
</View>
)}
</View>
<View
style={{
marginVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_2
}}
>
{Object.keys(PlanOverView[plan.id as keyof typeof PlanOverView]).map(
(item) => (
<View
key={item + plan.id}
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
borderBottomColor: colors.primary.border,
borderBottomWidth: 1,
paddingBottom: Spacing.LEVEL_2
}}
>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
<AppIcon
name={
item === "storage"
? "cloud"
: item === "fileSize"
? "file"
: "image-outline"
}
iconFamily="notesnook"
size={AppFontSize.lg}
/>
<Paragraph size={AppFontSize.sm}>
{strings[item as "storage" | "hdImages" | "fileSize"]()}
</Paragraph>
</View>
<Heading size={AppFontSize.sm}>
{
PlanOverView[plan.id as keyof typeof PlanOverView][
item as "storage" | "hdImages" | "fileSize"
]
}
</Heading>
</View>
)
)}
</View>
<Button
title={strings.selectPlan()}
type={plan.id === "pro" ? "accent" : "tertiary"}
style={{
width: "100%"
}}
onPress={() => {
if (isNotReady) return;
const currentPlanSubscribed =
PremiumService.get() &&
(pricingPlans?.user?.subscription?.productId ===
(product as RNIap.Subscription)?.productId ||
pricingPlans?.user?.subscription?.productId?.startsWith(
(product as RNIap.Subscription)?.productId
));
pricingPlans?.selectPlan(
plan.id,
currentPlanSubscribed
? `notesnook.${plan.id}.${
!(product as RNIap.Subscription)?.productId.includes(
"yearly"
)
? "yearly"
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}
/>
</View>
</View>
);
};

View File

@@ -1,84 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Image, TouchableOpacity, View } from "react-native";
//@ts-ignore
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { Radius, Spacing } from "../../common/design/spacing";
import Heading from "../ui/typography/heading";
export const ReviewItem = (props: {
review: string;
user: string;
userSource: string;
link: string;
userImage?: string;
}) => {
const { colors } = useThemeColors();
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => {
openLinkInBrowser(props.link);
}}
style={{
width: "100%",
padding: Spacing.LEVEL_2,
borderWidth: 1,
backgroundColor: colors.secondary.background,
borderRadius: Radius.S,
borderColor: colors.primary.border
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
paddingBottom: Spacing.LEVEL_1
}}
>
{props.userImage ? (
<Image
source={{
uri: props.userImage
}}
style={{
width: 34,
height: 34,
borderRadius: 100
}}
/>
) : null}
<View>
<Heading size={AppFontSize.sm}>{props.user}</Heading>
<Paragraph size={AppFontSize.xs} color={colors.primary.paragraph}>
{props.userSource}
</Paragraph>
</View>
</View>
<Paragraph>{props.review}</Paragraph>
</TouchableOpacity>
);
};

View File

@@ -27,7 +27,6 @@ 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 { Spacing } from "../../common/design/spacing";
import {
eSendEvent,
sendItemUpdateEvent,
@@ -39,9 +38,10 @@ import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { refreshNotesPage } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import ColorPicker from "../dialogs/color-picker";
import PaywallSheet from "../sheets/paywall";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
@@ -80,21 +80,20 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
key={item.id}
onPress={toggleColor}
style={{
width: 40,
height: 40,
width: 35,
height: 35,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
borderWidth: 0
marginRight: 5
}}
>
{isLinked ? (
<AppIcon
<Icon
testID="icon-check"
name="check-small"
iconFamily="notesnook"
color={colors.static.black}
size={15}
name="check"
color="white"
size={AppFontSize.lg}
/>
) : null}
</Pressable>
@@ -152,10 +151,31 @@ export const ColorTags = ({ item }: { item: Note }) => {
/>
<View
style={{
flexDirection: "row"
flexGrow: isTablet ? undefined : 1,
flexDirection: "row",
marginLeft: 5,
flexShrink: 2
}}
>
{colorNotes?.length ? (
{!colorNotes || !colorNotes.length ? (
<Button
onPress={onPress}
buttonType={{
text: colors.primary.accent
}}
title={strings.addColor()}
type="secondary"
icon="plus"
iconPosition="right"
height={30}
fontSize={AppFontSize.xs}
style={{
marginRight: 5,
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
) : (
<LegendList
data={colorNotes}
estimatedItemSize={30}
@@ -164,14 +184,11 @@ export const ColorTags = ({ item }: { item: Note }) => {
bounces={false}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
gap: Spacing.LEVEL_1
}}
ListFooterComponent={
<Pressable
style={{
width: 40,
height: 40,
width: 35,
height: 35,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
@@ -180,17 +197,16 @@ export const ColorTags = ({ item }: { item: Note }) => {
type="secondary"
onPress={onPress}
>
<AppIcon
<Icon
testID="icon-plus"
iconFamily="notesnook"
name="plus"
color={colors.primary.accent}
color={colors.primary.icon}
size={AppFontSize.lg}
/>
</Pressable>
}
/>
) : null}
)}
</View>
</>
);

View File

@@ -24,11 +24,11 @@ import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import { db } from "../../common/database";
import { Item, Note } from "@notesnook/core";
import AppIcon from "../ui/AppIcon";
import { Radius, Spacing } from "../../common/design/spacing";
export const DateMeta = ({ item }: { item: Item }) => {
const { colors, isDark } = useThemeColors();
const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
@@ -50,56 +50,44 @@ export const DateMeta = ({ item }: { item: Item }) => {
key={key}
style={{
flexDirection: "row",
width: "48.5%",
justifyContent: "space-between",
backgroundColor: colors.secondary.background,
borderRadius: Radius.XS,
padding: Spacing.LEVEL_2,
gap: Spacing.LEVEL_1,
alignItems: "center"
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
}}
>
<View
style={{
gap: Spacing.LEVEL_0
}}
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.dateDescFromKey(
key as
| "dateDeleted"
| "dateEdited"
| "dateModified"
| "dateCreated"
| "dateUploaded"
)}
</Paragraph>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
onPress={
item.type !== "note"
? undefined
: () => {
setIsDatePickerVisible(true);
}
}
>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.dateDescFromKey(
key as
| "dateDeleted"
| "dateEdited"
| "dateModified"
| "dateCreated"
| "dateUploaded"
)}
</Paragraph>
<Paragraph
size={AppFontSize.xs}
color={colors.primary.heading}
fontFamily="MEDIUM"
onPress={
item.type !== "note"
? undefined
: () => {
setIsDatePickerVisible(true);
}
}
>
{getFormattedDate(
key === "dateCreated"
? dateCreated
: (item[key as keyof Item] as string),
"date-time"
)}
</Paragraph>
</View>
{key === "dateCreated" && item.type === "note" ? (
<>
<AppIcon name="edit-pencil" size={16} iconFamily="notesnook" />
</>
) : null}
{getFormattedDate(
key === "dateCreated"
? dateCreated
: (item[key as keyof Item] as string),
"date-time"
)}
{key === "dateCreated" && item.type === "note" ? (
<>
{" "}
<AppIcon name="pencil" size={AppFontSize.md} />
</>
) : null}
</Paragraph>
</View>
);
@@ -130,8 +118,10 @@ export const DateMeta = ({ item }: { item: Item }) => {
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
borderTopWidth: 1,
borderTopColor: colors.primary.border,
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
{getDateMeta().map(renderItem)}

View File

@@ -0,0 +1,251 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eOnLoadNote } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import { ReminderTime } from "../ui/reminder-time";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { TagStrip, Tags } from "./tags";
import { Dialog } from "../dialog";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
return (
<View
style={{
height: 1,
backgroundColor: colors.primary.border,
width: "100%",
marginTop: top,
marginBottom: bottom
}}
/>
);
};
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
const { colors } = useThemeColors();
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
{strings.noNotePropertiesNotice()}
</Paragraph>
);
}
return (
<FlatList
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
style={{
backgroundColor: colors.primary.background,
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%"
}}
nestedScrollEnabled
bounces={false}
data={[0]}
keyExtractor={() => "properties-scroll-item"}
renderItem={() => (
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
gap: 5
}}
>
{item.type === "color" ? (
<Pressable
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
style={{
width: 30,
height: 30,
borderRadius: 100,
marginRight: 10
}}
/>
) : item.type === "tag" ? (
<Icon
name="pound"
size={AppFontSize.lg}
color={colors.primary.icon}
/>
) : null}
<Heading size={AppFontSize.lg}>{item.title}</Heading>
</View>
{item.type === "note" ? (
<IconButton
name="open-in-new"
type="plain"
color={colors.primary.icon}
size={AppFontSize.lg}
style={{
alignSelf: "flex-start"
}}
onPress={() => {
close();
eSendEvent(eOnLoadNote, {
item: item,
newTab: true
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
}
}}
/>
) : null}
</View>
{(item.type === "notebook" || item.type === "reminder") &&
item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "note" ? (
<TagStrip close={close} item={item} />
) : null}
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
style={{
justifyContent: "flex-start",
borderWidth: 0,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
fontSize={AppFontSize.xs}
/>
) : null}
</View>
<DateMeta item={item} />
<Line bottom={0} top={0} />
{item.type === "note" ? (
<>
<Tags close={close} item={item} />
<Line bottom={0} top={0} />
</>
) : null}
{item.type === "note" ? (
<Notebooks note={item} close={close} />
) : null}
<Items
item={item}
buttons={buttons}
close={() => {
close();
}}
/>
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
<Dialog context="properties" />
</View>
)}
/>
);
};
Properties.present = async (item, isSheet, buttons = []) => {
if (!item) return;
let type = item?.type;
let dbItem;
switch (type) {
case "trash":
dbItem = item;
break;
case "note":
dbItem = await db.notes.note(item.id);
break;
case "notebook":
dbItem = await db.notebooks.notebook(item.id);
break;
case "tag":
dbItem = await db.tags.tag(item.id);
break;
case "color":
dbItem = await db.colors.color(item.id);
break;
case "reminder": {
dbItem = await db.reminders.reminder(item.id);
break;
}
}
presentSheet({
context: isSheet ? "local" : undefined,
component: (ref, close) => (
<Properties
close={close}
actionSheetRef={ref}
item={dbItem}
buttons={buttons}
/>
)
});
};

View File

@@ -1,460 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
presentSheet,
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import { eOnLoadNote, refreshNotesPage } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import { ReminderTime } from "../ui/reminder-time";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DateMeta } from "./date-meta";
import { Items } from "./items";
import { Tags } from "./tags";
import { Dialog } from "../dialog";
import AppIcon from "../ui/AppIcon";
import { Spacing } from "../../common/design/spacing";
import { Button } from "../ui/button";
import ManageTags from "../../screens/manage-tags";
import ColorPicker from "../dialogs/color-picker";
import { useRelationStore } from "../../stores/use-relation-store";
import { useMenuStore } from "../../stores/use-menu-store";
import Navigation from "../../services/navigation";
import { useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import { useSettingStore } from "../../stores/use-setting-store";
import { Action, useActions } from "../../hooks/use-actions";
import {
Color,
Note,
Notebook,
Reminder,
Tag,
TrashItem
} from "@notesnook/core";
import LineSeparator from "../ui/seperator/line-separator";
export type PropertiesItem =
| Note
| Notebook
| Tag
| Color
| Reminder
| TrashItem;
export const Properties = ({
close,
item,
buttons = []
}: {
close?: (ctx?: string | undefined) => void;
item: PropertiesItem;
buttons: Action[];
}) => {
const { colors } = useThemeColors();
const colorFeature = useIsFeatureAvailable("colors");
const [noteNotebooks, setNoteNotebooks] = useState<Notebook[]>([]);
const [tags, setTags] = useState<Tag[]>([]);
const [visible, setVisible] = useState(false);
const colorNotes = useMenuStore((state) => state.colorNotes);
const actions = useActions({
item,
close: () => {
close?.();
}
});
const editAction = actions.find(
(action) => action.id === "edit-notebook" || action.id === "rename-tag"
);
useEffect(() => {
async function getNotebooks() {
const filteredNotebooks = await db.relations
.to(item, "notebook")
.resolve();
return filteredNotebooks || [];
}
if (item.type === "note") {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
db.relations
.to(item, "tag")
.resolve()
.then((tags) => {
setTags(tags);
});
}
}, [item]);
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
{strings.noNotePropertiesNotice()}
</Paragraph>
);
}
return (
<ScrollView
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
style={{
backgroundColor: colors.primary.background,
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%",
paddingTop: Spacing.LEVEL_3
}}
nestedScrollEnabled
bounces={false}
>
<View>
{item.type === "note" ? (
<ColorPicker
visible={visible}
setVisible={setVisible}
onColorAdded={async (color) => {
await db.relations.to(item, "color").unlink();
await db.relations.add(color, item);
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(color.id, "color");
eSendEvent(refreshNotesPage);
}}
/>
) : null}
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
gap: Spacing.LEVEL_1
}}
>
{item.type === "note" && (noteNotebooks.length || tags.length) ? (
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
{noteNotebooks?.map((item) => (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<AppIcon
name="bookmark"
iconFamily="notesnook"
size={AppFontSize.xs}
color={colors.secondary.icon}
/>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{item.title}
</Paragraph>
</View>
))}
{tags?.map((item) =>
item.id ? (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{item.title}
</Paragraph>
</View>
) : null
)}
</View>
) : null}
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
gap: Spacing.LEVEL_1
}}
>
{item.type === "color" ? (
<Pressable
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
style={{
width: 8,
height: 8,
borderRadius: 100
}}
/>
) : null}
<Heading fontSize="XL">{item.title}</Heading>
{editAction ? (
<IconButton
name="edit-pencil"
iconFamily="notesnook"
type="plain"
color={colors.primary.icon}
size={AppFontSize.md}
onPress={editAction.onPress}
/>
) : null}
</View>
{item.type === "note" ? (
<IconButton
name="square-out"
iconFamily="notesnook"
type="plain"
color={colors.primary.icon}
size={AppFontSize.lg}
style={{
alignSelf: "flex-start"
}}
onPress={() => {
close?.();
eSendEvent(eOnLoadNote, {
item: item,
newTab: true
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
}
}}
/>
) : null}
</View>
{(item.type === "notebook" || item.type === "reminder") &&
item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
style={{
justifyContent: "flex-start",
borderWidth: 0,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
fontSize={AppFontSize.xs}
/>
) : null}
</View>
{item.type === "note" ? (
<LineSeparator paddingVertical={Spacing.LEVEL_3} />
) : null}
<DateMeta item={item} />.
{item.type === "note" && colorNotes.length > 0 ? (
<View>
<LineSeparator
paddingVertical={Spacing.LEVEL_3}
style={{
paddingTop: Spacing.LEVEL_2
}}
/>
<Tags close={close} item={item} />
<LineSeparator
paddingVertical={Spacing.LEVEL_3}
style={{
paddingBottom: 0
}}
/>
</View>
) : null}
{item.type === "note" ? (
<>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2,
marginTop: Spacing.LEVEL_2
}}
>
<Button
onPress={async () => {
ManageTags.present([item.id]);
close?.();
}}
title={
colorNotes?.length
? strings.addTag()
: strings.dataTypesCamelCase.tag()
}
type={colorNotes?.length ? "accent-outline" : "shade-plain"}
icon="plus"
iconFamily="notesnook"
style={{
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2,
width: colorNotes?.length > 0 ? "100%" : "48.5%"
}}
/>
{colorNotes.length > 0 ? null : (
<Button
onPress={() => {
if (colorFeature && !colorFeature.isAllowed) {
ToastManager.show({
message: colorFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(colorFeature);
ToastManager.hide();
}
});
return;
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
title={strings.dataTypesCamelCase.color()}
type="shade-plain"
icon="plus"
iconFamily="notesnook"
style={{
width: "48.5%",
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2
}}
/>
)}
</View>
</>
) : null}
</View>
<LineSeparator
paddingVertical={Spacing.LEVEL_2}
paddingHorizontal={Spacing.LEVEL_3}
/>
<Items
item={item}
buttons={buttons}
actions={actions}
close={() => {
close?.();
}}
/>
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
<Dialog context="properties" />
</View>
</ScrollView>
);
};
Properties.present = async (
item: PropertiesItem,
isSheet: boolean = false,
buttons: Action[] = []
) => {
if (!item) return;
const type = item?.type;
let dbItem;
switch (type) {
case "trash":
dbItem = item;
break;
case "note":
dbItem = await db.notes.note(item.id);
break;
case "notebook":
dbItem = await db.notebooks.notebook(item.id);
break;
case "tag":
dbItem = await db.tags.tag(item.id);
break;
case "color":
dbItem = await db.colors.color(item.id);
break;
case "reminder": {
dbItem = await db.reminders.reminder(item.id);
break;
}
}
if (!dbItem) return;
presentSheet({
context: isSheet ? "local" : undefined,
component: (ref, close) => (
<Properties close={close} item={dbItem} buttons={buttons} />
)
});
};

View File

@@ -18,21 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Dimensions, View } from "react-native";
import SwiperFlatList from "react-native-swiper-flatlist";
import { Action, ActionId } from "../../hooks/use-actions";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Action, ActionId, useActions } from "../../hooks/use-actions";
import { useStoredRef } from "../../hooks/use-stored-ref";
import { DDS } from "../../services/device-detection";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize } from "../../utils/size";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { Radius, Spacing } from "../../common/design/spacing";
const TOP_BAR_ITEMS: ActionId[] = [
"pin",
@@ -63,18 +63,10 @@ const BOTTOM_BAR_ITEMS: ActionId[] = [
"trash"
];
const PREFERENCE_ITEMS: ActionId[] = [
"add-shortcut",
"pin",
"default-notebook",
"default-tag",
"default-homepage"
];
const COLUMN_BAR_ITEMS: ActionId[] = [
"select",
"add-notebook",
// "edit-notebook",
"edit-notebook",
"move-notes",
"move-notebook",
"edit-reminder",
@@ -86,7 +78,7 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
"add-shortcut",
"reorder",
"rename-color",
// "rename-tag",
"rename-tag",
"launcher-shortcut",
"copy-id",
"copy-link",
@@ -98,13 +90,11 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
export const Items = ({
item,
close,
buttons,
actions
buttons
}: {
item: Item;
close: () => void;
buttons: Action[];
actions: Action[];
}) => {
const { colors } = useThemeColors();
const topBarSorting = useStoredRef<{ [name: string]: number }>(
@@ -112,12 +102,14 @@ export const Items = ({
{}
);
const dimensions = useSettingStore((state) => state.dimensions);
const actions = useActions({ item, close });
const selectedActions = actions.filter((i) => !i.hidden);
const deviceMode = useSettingStore((state) => state.deviceMode);
const width = Math.min(dimensions.width, 600);
const shouldShrink =
Dimensions.get("window").fontScale > 1 && dimensions.width < 450;
Dimensions.get("window").fontScale > 1 &&
Dimensions.get("window").width < 450;
const columnItemsCount = deviceMode === "tablet" ? 7 : shouldShrink ? 4 : 5;
@@ -143,23 +135,16 @@ export const Items = ({
BOTTOM_BAR_ITEMS.indexOf(a.id) > BOTTOM_BAR_ITEMS.indexOf(b.id) ? 1 : -1
);
const preferenceItems = selectedActions
.filter((item) => PREFERENCE_ITEMS.indexOf(item.id) > -1)
.sort((a, b) =>
PREFERENCE_ITEMS.indexOf(a.id) > PREFERENCE_ITEMS.indexOf(b.id) ? 1 : -1
);
const columnItems = selectedActions
.filter(
(item) =>
COLUMN_BAR_ITEMS.indexOf(item.id) > -1 &&
PREFERENCE_ITEMS.indexOf(item.id) === -1
)
.filter((item) => COLUMN_BAR_ITEMS.indexOf(item.id) > -1)
.sort((a, b) =>
COLUMN_BAR_ITEMS.indexOf(a.id) > COLUMN_BAR_ITEMS.indexOf(b.id) ? 1 : -1
);
const actionItems = [...buttons, ...columnItems];
const topBarItemHeight = Math.min(
(width - (topBarItems.length * 10 + 14)) / topBarItems.length,
60
);
const renderRowItem = React.useCallback(
({ item }: { item: Action }) => (
@@ -167,7 +152,7 @@ export const Items = ({
key={item.id}
style={{
alignItems: "center",
width: columnItemWidth - 10,
width: columnItemWidth - 8,
opacity: item.locked ? 0.5 : 1
}}
>
@@ -176,18 +161,17 @@ export const Items = ({
type={item.checked ? "shade" : "secondary"}
testID={"icon-" + item.id}
style={{
width: columnItemWidth - 10,
paddingVertical: Spacing.LEVEL_2,
height: columnItemWidth / 1.5,
width: columnItemWidth - 8,
borderRadius: 10,
justifyContent: "center",
alignItems: "center",
marginBottom: 6
marginBottom: DDS.isTab ? 7 : 3.5
}}
>
<AppIcon
<Icon
allowFontScaling
name={item.icon}
iconFamily="notesnook"
size={
DDS.isTab
? AppFontSize.xxl
@@ -223,79 +207,33 @@ export const Items = ({
]
);
const renderPreferenceItem = React.useCallback(
const renderColumnItem = React.useCallback(
(item: Action) => (
<Pressable
<Button
key={item.id}
onPress={item.onPress}
type={item.checked ? "shade" : "transparent"}
testID={"icon-" + item.id}
style={{
width: "48.5%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: Spacing.LEVEL_1,
paddingVertical: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: item.checked ? 0 : 1,
borderColor: colors.primary.border,
opacity: item.locked ? 0.7 : 1
buttonType={{
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
flexShrink: 1
}}
>
<AppIcon
name={item.icon}
iconFamily="notesnook"
allowFontScaling
size={AppFontSize.md}
color={item.checked ? colors.primary.icon : colors.secondary.icon}
/>
<Paragraph
numberOfLines={1}
fontSize="XS"
fontFamily="MEDIUM"
color={
item.checked
? colors.primary.paragraph
: colors.secondary.paragraph
}
style={{ flexShrink: 1 }}
>
{item.title}
</Paragraph>
</View>
<AppIcon
name={item.checked ? "toggle-on" : "toggle-off"}
iconFamily="notesnook"
size={16}
color={
item.checked
? [colors.primary.accent, colors.primary.background]
: [colors.disabled.icon, colors.primary.background]
}
/>
</Pressable>
testID={"icon-" + item.id}
onPress={item.onPress}
title={item.title}
icon={item.icon}
type={item.checked ? "inverted" : "plain"}
fontSize={AppFontSize.sm}
style={{
borderRadius: 0,
justifyContent: "flex-start",
alignSelf: "flex-start",
width: "100%",
opacity: item.locked ? 0.5 : 1
}}
/>
),
[
colors.disabled.icon,
colors.primary.accent,
colors.primary.background,
colors.primary.border,
colors.primary.icon,
colors.primary.paragraph,
colors.secondary.icon,
colors.secondary.paragraph
]
[colors.error.paragraph, colors.primary.accent, colors.primary.paragraph]
);
const renderTopBarItem = React.useCallback(
@@ -315,7 +253,7 @@ export const Items = ({
key={item.id}
testID={"icon-" + item.id}
style={{
width: columnItemWidth - 10,
width: columnItemWidth - 8,
alignSelf: "flex-start",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
@@ -323,26 +261,24 @@ export const Items = ({
<View
style={{
height: columnItemWidth / 2,
width: 65,
width: columnItemWidth - DefaultAppStyles.GAP_SMALL,
justifyContent: "center",
alignItems: "center",
backgroundColor: item.checked
? colors.primary.shade
: colors.secondary.background,
borderRadius: Radius.XS,
overflow: "hidden",
paddingVertical: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_2
borderWidth: 1,
borderRadius: defaultBorderRadius,
borderColor: item.checked
? item.activeColor || colors.primary.accent
: colors.primary.border,
overflow: "hidden"
}}
>
<AppIcon
<Icon
name={item.icon}
iconFamily="notesnook"
allowFontScaling
size={16}
size={DDS.isTab ? AppFontSize.xxl : AppFontSize.md + 4}
color={
item.checked
? colors.primary.icon
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
@@ -374,13 +310,7 @@ export const Items = ({
<Paragraph
textBreakStrategy="simple"
fontSize="XXS"
fontFamily="MEDIUM"
color={
item.checked
? colors.primary.paragraph
: colors.secondary.paragraph
}
size={AppFontSize.xxs}
style={{ textAlign: "center" }}
>
{item.title}
@@ -391,12 +321,8 @@ export const Items = ({
[
colors.error.icon,
colors.primary.accent,
colors.primary.icon,
colors.primary.paragraph,
colors.primary.shade,
colors.secondary.background,
colors.primary.border,
colors.secondary.icon,
colors.secondary.paragraph,
colors.static.orange,
columnItemWidth,
topBarSorting
@@ -413,7 +339,11 @@ export const Items = ({
};
return (
<View>
<View
style={{
gap: DefaultAppStyles.GAP
}}
>
{item.type === "note" ? (
<>
<View>
@@ -422,24 +352,22 @@ export const Items = ({
autoplay={false}
showPagination
paginationStyleItemActive={{
borderRadius: 6,
backgroundColor: colors.selected.accent,
height: 5,
width: 20,
marginHorizontal: 3
borderRadius: 2,
backgroundColor: colors.selected.background,
height: 6,
marginHorizontal: 2
}}
paginationStyleItemInactive={{
borderRadius: 6,
borderRadius: 2,
backgroundColor: colors.secondary.background,
height: 5,
width: 14,
marginHorizontal: 3
height: 6,
marginHorizontal: 2
}}
paginationStyle={{
position: "relative",
marginHorizontal: 2,
marginBottom: Spacing.LEVEL_0,
marginTop: Spacing.LEVEL_2
marginBottom: -10,
marginTop: 10
}}
contentContainerStyle={{
justifyContent: "flex-start",
@@ -452,7 +380,7 @@ export const Items = ({
style={{
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
gap: Spacing.LEVEL_2,
gap: 5,
width: width
}}
>
@@ -466,7 +394,7 @@ export const Items = ({
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: Spacing.LEVEL_1,
gap: 5,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
@@ -474,61 +402,9 @@ export const Items = ({
</View>
</>
) : (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{preferenceItems.length > 0 ? (
<View style={{ gap: Spacing.LEVEL_2 }}>
<Paragraph
fontFamily="MEDIUM"
fontSize="SM"
color={colors.secondary.paragraph}
>
{strings.preferences()}
</Paragraph>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: Spacing.LEVEL_1
}}
>
{preferenceItems.map(renderPreferenceItem)}
</View>
</View>
) : null}
<View
style={{
marginVertical: Spacing.LEVEL_3,
width: "100%",
borderBottomWidth: 1,
borderColor: colors.primary.separator
}}
/>
{actionItems.length > 0 ? (
<View style={{ gap: Spacing.LEVEL_2 }}>
<Paragraph
fontFamily="MEDIUM"
fontSize="SM"
color={colors.secondary.paragraph}
>
{strings.actionsHeading()}
</Paragraph>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: Spacing.LEVEL_1
}}
>
{actionItems.map((item) => renderRowItem({ item }))}
</View>
</View>
) : null}
<View>
{buttons.map(renderColumnItem)}
{columnItems.map(renderColumnItem)}
</View>
)}
</View>

View File

@@ -17,17 +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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import ManageTags from "../../screens/manage-tags";
import { TaggedNotes } from "../../screens/notes/tagged";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
import { Spacing } from "../../common/design/spacing";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -36,9 +37,32 @@ export const Tags = ({ item, close }) => {
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
alignSelf: "center",
justifyContent: "space-between",
width: "100%"
}}
>
<Button
onPress={async () => {
ManageTags.present([item.id]);
close();
}}
buttonType={{
text: colors.primary.accent
}}
title={strings.addTag()}
type="secondary"
icon="plus"
iconPosition="right"
fontSize={AppFontSize.xs}
style={{
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
<ColorTags item={item} />
</View>
) : null;

View File

@@ -170,147 +170,147 @@ export const SelectionHeader = React.memo(
{!selectedItemsList.length
? null
: (focusedRouteId === "Monographs"
? [
{
title: strings.unpublish(),
onPress: () => {
presentDialog({
title: strings.doActions.unpublish.note(
selectedItemsList.length
),
positiveText: strings.unpublish(),
negativeText: strings.cancel(),
positivePress: async () => {
for (const id of selectedItemsList) {
await db.monographs.unpublish(id);
}
Navigation.queueRoutesForUpdate();
clearSelection();
},
positiveType: "errorShade"
});
},
visible: true,
icon: "delete"
}
]
: [
{
title: strings.manageTags(),
onPress: async () => {
ManageTags.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: strings.export(),
onPress: async () => {
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: strings.addToNotebook(),
onPress: async () => {
Navigation.navigate("LinkNotebooks", {
noteIds: selectedItemsList
});
},
visible: type === "note",
icon: "plus"
},
{
title: strings.unlinkNotebook(),
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(
id,
...selectedItemsList
);
updateNotebook(id);
? [
{
title: strings.unpublish(),
onPress: () => {
presentDialog({
title: strings.doActions.unpublish.note(
selectedItemsList.length
),
positiveText: strings.unpublish(),
negativeText: strings.cancel(),
positivePress: async () => {
for (const id of selectedItemsList) {
await db.monographs.unpublish(id);
}
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: strings.unfavorite(),
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" &&
type !== "note" &&
type !== "notebook",
icon: "delete"
},
{
title: strings.restore(),
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: strings.delete(),
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
]
).map((item) =>
!item.visible ? null : (
<IconButton
size={AppFontSize.lg}
type="plain"
tooltipText={item.title}
tooltipPosition={NativeTooltip.POSITIONS.TOP}
testID={`select-${item.icon}`}
name={item.icon}
key={item.title}
color={colors.primary.icon}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
positiveType: "errorShade"
});
},
visible: true,
icon: "delete"
}
]
: [
{
title: strings.manageTags(),
onPress: async () => {
ManageTags.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: strings.export(),
onPress: async () => {
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: strings.addToNotebook(),
onPress: async () => {
Navigation.navigate("LinkNotebooks", {
noteIds: selectedItemsList
});
},
visible: type === "note",
icon: "plus"
},
{
title: strings.unlinkNotebook(),
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(
id,
...selectedItemsList
);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: strings.unfavorite(),
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" &&
type !== "note" &&
type !== "notebook",
icon: "delete"
},
{
title: strings.restore(),
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: strings.delete(),
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
]
).map((item) =>
!item.visible ? null : (
<IconButton
size={AppFontSize.lg}
type="plain"
tooltipText={item.title}
tooltipPosition={NativeTooltip.POSITIONS.TOP}
testID={`select-${item.icon}`}
name={item.icon}
key={item.title}
color={colors.primary.icon}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
</View>
<IconButton

View File

@@ -34,8 +34,6 @@ import SheetWrapper from "../ui/sheet";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { Radius, Spacing } from "../../common/design/spacing";
import AppIcon from "../ui/AppIcon";
const SheetProvider = ({ context = "global" }) => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
@@ -115,78 +113,43 @@ const SheetProvider = ({ context = "global" }) => {
: data.enableGesturesInScrollView
}
>
{data?.progress || data?.icon || data?.paragraph ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
marginBottom:
!data.progress && !data.icon && !data.title && !data.paragraph
? 0
: 10,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
<View
style={{
justifyContent: "center",
alignItems: "center",
marginBottom:
!data.progress && !data.icon && !data.title && !data.paragraph
? 0
: 10,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{data?.progress ? (
<ActivityIndicator
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingTop: Spacing.LEVEL_2
marginTop: DefaultAppStyles.GAP
}}
>
{data?.icon || data?.progress ? (
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
{data.icon ? (
<AppIcon
name="clock"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
) : (
<ActivityIndicator
style={{
marginTop: DefaultAppStyles.GAP
}}
size={50}
color={colors.primary.accent}
/>
)}
</View>
) : null}
size={50}
color={colors.primary.accent}
/>
) : null}
{data?.title || data?.paragraph ? (
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
{data.title ? (
<Heading fontSize="XL" lineHeight="100%">
{data.title}
</Heading>
) : null}
{data.paragraph ? (
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{data.paragraph}
</Paragraph>
) : null}
</View>
) : null}
</View>
</View>
) : null}
{data?.icon ? (
<Icon
color={colors[data.iconColor] || colors.primary.accent}
name={data.icon}
size={50}
/>
) : null}
{data?.title ? <Heading> {data?.title}</Heading> : null}
{data?.paragraph ? (
<Paragraph style={{ textAlign: "center" }}>
{data?.paragraph}
</Paragraph>
) : null}
</View>
{typeof data.component === "function"
? data.component(
@@ -249,27 +212,20 @@ const SheetProvider = ({ context = "global" }) => {
/>
) : null}
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
{data?.actionsArray &&
data?.actionsArray.map((item) => (
<Button
onPress={item.action}
key={item.accentText}
title={item.actionText}
icon={item.icon && item.icon}
type={item.type || "accent"}
style={{
flex: 1,
flexShrink: 1
}}
/>
))}
</View>
{data?.actionsArray &&
data?.actionsArray.map((item) => (
<Button
onPress={item.action}
key={item.accentText}
title={item.actionText}
icon={item.icon && item.icon}
type={item.type || "accent"}
style={{
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
width="100%"
/>
))}
{data?.learnMore ? (
<Paragraph

View File

@@ -16,24 +16,23 @@ 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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import { strings } from "@notesnook/intl";
import { db } from "../../../common/database";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import { DefaultAppStyles } from "../../../utils/styles";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import Heading from "../../ui/typography/heading";
import { DefaultAppStyles } from "../../../utils/styles";
import { AppFontSize } from "../../../utils/size";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { Pressable } from "../../ui/pressable";
import { ScrollView } from "react-native-actions-sheet";
const getExpiryOptions = () => [
{ label: strings.expiryOneDay(), value: 24 * 60 * 60 * 1000 },
@@ -95,7 +94,7 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
<ScrollView
contentContainerStyle={{
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL,
paddingTop: DefaultAppStyles.GAP_VERTICAL,
paddingBottom: DefaultAppStyles.GAP_VERTICAL * 2
}}
@@ -103,92 +102,60 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: DefaultAppStyles.GAP_SMALL
justifyContent: "space-between",
alignItems: "center"
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="key"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View style={{ flex: 1, gap: DefaultAppStyles.GAP_VERTICAL_SMALL }}>
<Heading fontSize="XL" lineHeight="100%">
{strings.createApiKey()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.createApiKeyDesc()}
</Paragraph>
</View>
<Heading size={AppFontSize.xl}>{strings.createApiKey()}</Heading>
</View>
<FormInput
name="keyName"
formRef={formRef}
label={strings.keyName()}
placeholder={strings.exampleKeyName()}
validators={[validators.required(strings.enterKeyName())]}
containerStyle={{ borderRadius: Radius.XS }}
onChangeText={() => {
formRef.current.setError("keyName", undefined);
}}
onSubmitEditing={handleCreate}
/>
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
<Paragraph size={AppFontSize.sm}>{strings.keyName()}</Paragraph>
<FormInput
name="keyName"
formRef={formRef}
placeholder={strings.exampleKeyName()}
validators={[validators.required(strings.enterKeyName())]}
onChangeText={() => {
formRef.current.setError("keyName", undefined);
}}
onSubmitEditing={handleCreate}
/>
</View>
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
<Heading fontSize="MD" lineHeight="100%">
{strings.expiresIn()}
</Heading>
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
{getExpiryOptions().map((option) => {
const selected = selectedExpiry === option.value;
return (
<Pressable
key={option.label}
onPress={() => setSelectedExpiry(option.value)}
type={selected ? "selected" : "transparent"}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.secondary.border
}}
<Paragraph size={AppFontSize.sm}>{strings.expiresIn()}</Paragraph>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: DefaultAppStyles.GAP_SMALL
}}
>
{getExpiryOptions().map((option) => (
<Pressable
key={option.label}
onPress={() => setSelectedExpiry(option.value)}
type={
selectedExpiry === option.value ? "selected" : "transparent"
}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph
size={AppFontSize.sm}
color={
selectedExpiry === option.value
? colors.selected.paragraph
: colors.primary.paragraph
}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={
selected ? colors.selected.heading : colors.secondary.heading
}
>
{option.label}
</Heading>
<AppIcon
name={selected ? "radiobox-marked" : "radiobox-blank"}
size={16}
color={selected ? colors.selected.accent : colors.secondary.icon}
/>
</Pressable>
);
})}
{option.label}
</Paragraph>
</Pressable>
))}
</View>
</View>
@@ -199,6 +166,9 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
loading={isCreating}
disabled={isCreating}
onPress={handleCreate}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
/>
</ScrollView>
);

View File

@@ -31,18 +31,15 @@ import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
import Input from "../../ui/input";
import { useSettingStore } from "../../../stores/use-setting-store";
import DialogHeader from "../../dialog/dialog-header";
import DialogButtons from "../../dialog/dialog-buttons";
import { presentDialog } from "../../dialog/functions";
import { getElevationStyle } from "../../../utils/elevation";
import { defaultBorderRadius } from "../../../utils/size";
import { useThemeColors } from "@notesnook/theme";
import { getContainerBorder } from "../../../utils/colors";
import { Radius, Spacing } from "../../../common/design/spacing";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
export const AddNotebookSheet = ({
notebook,
@@ -58,28 +55,29 @@ export const AddNotebookSheet = ({
defaultTitle?: string;
}) => {
const { colors } = useThemeColors();
const title = useRef(notebook?.title || defaultTitle);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const formRef = useRef(
createFormRef({
title: notebook?.title || defaultTitle || "",
description: notebook?.description || ""
})
);
const onSaveChanges = async () => {
if (loading) return;
if (!formRef.current?.validate()) return;
if (!title.current || title?.current.trim().length === 0) {
ToastManager.show({
heading: strings.enterNotebookTitle(),
type: "error",
context: "local"
});
return;
}
setLoading(true);
const values = formRef.current.getValues();
const id = await db.notebooks.add({
title: values.title,
description: values.description,
title: title.current,
description: description.current,
id: notebook?.id
});
@@ -129,67 +127,57 @@ export const AddNotebookSheet = ({
...getElevationStyle(5),
width: DDS.isTab ? 400 : "85%",
maxHeight: 450,
borderRadius: Radius.LG,
borderRadius: defaultBorderRadius,
backgroundColor: colors.primary.background,
gap: Spacing.LEVEL_4,
paddingVertical: Spacing.LEVEL_4,
paddingTop: 12,
...getContainerBorder(colors.primary.border, 0.5),
overflow: "hidden"
}}
>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_4
paddingHorizontal: 12
}}
>
<DialogHeader
title={notebook ? strings.editNotebook() : strings.newNotebook()}
/>
<View
style={{
gap: Spacing.LEVEL_2
<Input
fwdRef={titleInput}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
title.current = value;
}}
>
<FormInput
fwdRef={titleInput}
name="title"
formRef={formRef}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onLayout={() => {
setTimeout(() => {
titleInput?.current?.focus();
}, 300);
}}
validators={[validators.required(strings.titleIsRequired())]}
placeholder={"eg. My Notebook"}
onSubmitEditing={() => {
descriptionInput.current?.focus();
}}
label={strings.enterNotebookTitle()}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={
notebook ? notebook.title : formRef.current.getValue("title")
}
/>
onLayout={() => {
setTimeout(() => {
titleInput?.current?.focus();
}, 300);
}}
placeholder={strings.enterNotebookTitle()}
onSubmit={() => {
descriptionInput.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : title.current}
/>
<FormInput
fwdRef={descriptionInput}
name="description"
formRef={formRef}
testID={notesnook.ids.dialogs.notebook.inputs.description}
label={strings.enterNotebookDescription()}
placeholder={"eg. This is My Notebook"}
returnKeyLabel={strings.next()}
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
</View>
<Input
fwdRef={descriptionInput}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
description.current = value;
}}
placeholder={strings.enterNotebookDescription()}
returnKeyLabel={strings.next()}
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
</View>
<DialogButtons
onPressNegative={() => {
title.current = undefined;
close?.(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}}

View File

@@ -1,178 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const OPTIONS = [-1, 0, 1, 5, 15, 30];
const formatValue = (item: number) => {
return item === -1
? strings.never()
: item === 0
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
};
type AppLockTimeoutProps = {
close?: (ctx?: string) => void;
};
function AppLockTimeout({ close }: AppLockTimeoutProps) {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [currentValue, setCurrentValue] = useState(
useSettingStore.getState().settings.appLockTimer
);
const onChange = async (item: number) => {
SettingsService.set({ appLockTimer: item });
setCurrentValue(item);
close?.();
};
return (
<ScrollView
style={{
maxHeight: height * 0.8
}}
>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingTop: Spacing.LEVEL_2
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="clock"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.appLockTimeout()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.appLockTimeoutDesc()}
</Paragraph>
</View>
</View>
</View>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2
}}
>
{OPTIONS.map((item) => {
const selected = currentValue === item;
return (
<Pressable
key={item}
type={selected ? "selected" : "transparent"}
onPress={() => onChange(item)}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.primary.border
}}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={colors.primary.heading}
>
{formatValue(item)}
</Heading>
<AppIcon
name={selected ? "radio-button" : "ellipse"}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
</Pressable>
);
})}
</View>
</ScrollView>
);
}
AppLockTimeout.present = () => {
presentSheet({
component: (_ref, close) => <AppLockTimeout close={close} />
});
};
export default AppLockTimeout;

View File

@@ -23,6 +23,7 @@ import dayjs from "dayjs";
import React, { useState } from "react";
import {
Linking,
Platform,
ScrollView,
Text,
TouchableOpacity,
@@ -40,15 +41,12 @@ import { DefaultAppStyles } from "../../../utils/styles";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { Radius, Spacing } from "../../../common/design/spacing";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
const isGithubRelease = Config.GITHUB_RELEASE === "true";
export const BuyPlan = (props: {
planId: string;
canActivateTrial?: boolean;
pricingPlans: ReturnType<typeof usePricingPlans>;
}) => {
const insets = useGlobalSafeAreaInsets();
const { colors } = useThemeColors();
const [checkoutUrl, setCheckoutUrl] = useState<string>();
const pricingPlans = props.pricingPlans;
@@ -65,18 +63,6 @@ export const BuyPlan = (props: {
: (pricingPlans.selectedProduct as RNIap.Product)?.productId
)?.includes("5");
const isAnnual = isGithubRelease
? (pricingPlans.selectedProduct as Plan)?.period === "yearly"
: (pricingPlans.selectedProduct as RNIap.Product)?.productId?.includes(
"yearly"
);
const hasTrialOffer = pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans.selectedProduct as Plan)?.period
);
return checkoutUrl ? (
<View
style={{
@@ -102,278 +88,162 @@ export const BuyPlan = (props: {
/>
</View>
) : (
<View
style={{
flex: 1
<ScrollView
contentContainerStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
<ScrollView
contentContainerStyle={{
paddingVertical: Spacing.LEVEL_4
}}
style={{
flex: 1
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
gap: Spacing.LEVEL_4
}}
>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
{[
Config.GITHUB_RELEASE === "true"
? "yearly"
: `notesnook.${props.planId}.yearly`,
Config.GITHUB_RELEASE === "true"
? "monthly"
: `notesnook.${props.planId}.monthly`,
...(props.planId === "essential" || pricingPlans.isSubscribed()
? []
: [
Config.GITHUB_RELEASE === "true"
? "5-year"
: `notesnook.${props.planId}.5year`
])
].map((item) => (
<ProductItem
key={item}
pricingPlans={pricingPlans}
productId={item}
/>
))}
</View>
<Paragraph
fontFamily="MEDIUM"
fontSize="MD"
color={colors.secondary.heading}
>
{strings.paymentSummary()}
</Paragraph>
<View
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: Radius.S,
backgroundColor: colors.secondary.background,
padding: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<View
style={{
gap: Spacing.LEVEL_1
}}
>
<Heading color={colors.primary.paragraph} size={AppFontSize.md}>
{strings.dueToday()}{" "}
</Heading>
<Paragraph>
{hasTrialOffer ? (
<Text>
{strings.freeTrialIncludes(
billingDuration?.duration || 0
)}
</Text>
) : is5YearPlanSelected ? (
strings.billingType.annual()
) : null}
</Paragraph>
</View>
<Heading color={colors.primary.accent} fontSize="SM">
{hasTrialOffer
? "Free"
: pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Heading>
</View>
{hasTrialOffer ? (
<>
<View
style={{
width: "100%",
height: 1,
backgroundColor: colors.primary.border,
marginVertical: Spacing.LEVEL_2
}}
/>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<View
style={{
gap: Spacing.LEVEL_1
}}
>
<Heading
color={colors.primary.paragraph}
size={AppFontSize.md}
>
{strings.nextBillingDate()}
</Heading>
<Paragraph>
{dayjs()
.add(billingDuration?.duration || 0, "day")
.format("DD MMMM,YYYY")}{" "}
{" "}
{isAnnual
? strings.billingType.annual()
: is5YearPlanSelected
? strings.billingType.oneTime()
: strings.billingType.monthly()}
</Paragraph>
</View>
<Heading fontSize="SM" color={colors.primary.accent}>
{pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Heading>
</View>
</>
) : null}
</View>
<Paragraph
fontFamily="MEDIUM"
fontSize="MD"
color={colors.secondary.heading}
>
{strings.whatsIncluded()}
</Paragraph>
<View
style={{
gap: Spacing.LEVEL_2,
borderWidth: 1,
borderColor: colors.primary.border,
padding: Spacing.LEVEL_3,
borderRadius: Radius.S,
backgroundColor: colors.secondary.background,
marginBottom: 27,
flex: 1
}}
>
{[
strings.planWhatsIncluded.unlimitedNotes(),
strings.planWhatsIncluded.endToEnd(),
strings.planWhatsIncluded.allDevices(),
hasTrialOffer
? strings.planWhatsIncluded.freeTrial(
billingDuration?.duration || 0
)
: undefined,
hasTrialOffer ? strings.planWhatsIncluded.remind() : undefined,
...(is5YearPlanSelected
? strings["5yearPlanConditions"]()
: ([] as string[]))
].map((item) =>
!item ? null : (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
flex: 1
}}
key={item}
>
<Icon
color={colors.primary.accent}
size={AppFontSize.lg}
name="check"
/>
<Paragraph
style={{
flexShrink: 1
}}
>
{item}
</Paragraph>
</View>
)
)}
<Paragraph
style={{
marginTop: Spacing.LEVEL_0
}}
fontSize="XS"
>
<Heading fontSize="XS">{strings.note()}: </Heading>
{is5YearPlanSelected
? strings.oneTimePurchase()
: strings.cancelAnytimeAlt()}
</Paragraph>
</View>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{strings.subTerms[0]()}{" "}
<Text
style={{
color: colors.primary.accent
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
}}
>
{strings.subTerms[1]()}
</Text>{" "}
{strings.subTerms[2]()}{" "}
<Text
style={{
color: colors.primary.accent
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
}}
>
{strings.subTerms[3]()}
</Text>
</Paragraph>
</View>
</ScrollView>
<View
style={{
backgroundColor: colors.secondary.background,
width: "100%",
padding: Spacing.LEVEL_3,
marginBottom: -insets.bottom,
paddingBottom: insets.bottom,
borderTopWidth: 1,
borderTopColor: colors.primary.border
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
{[
Config.GITHUB_RELEASE === "true"
? "yearly"
: `notesnook.${props.planId}.yearly`,
Config.GITHUB_RELEASE === "true"
? "monthly"
: `notesnook.${props.planId}.monthly`,
...(props.planId === "essential" || pricingPlans.isSubscribed()
? []
: [
Config.GITHUB_RELEASE === "true"
? "5-year"
: `notesnook.${props.planId}.5year`
])
].map((item) => (
<ProductItem
key={item}
pricingPlans={pricingPlans}
productId={item}
/>
))}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
}}
>
<Heading color={colors.primary.paragraph} size={AppFontSize.sm}>
{strings.dueToday()}{" "}
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
) ? (
<Text
style={{
color: colors.primary.accent
}}
>
({strings.daysFree(`${billingDuration?.duration || 0}`)})
</Text>
) : null}
</Heading>
<Paragraph color={colors.primary.paragraph}>
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
)
? "FREE"
: pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
) ? (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
}}
>
<Paragraph color={colors.secondary.paragraph}>
{strings.due(
dayjs()
.add(billingDuration?.duration || 0, "day")
.format("DD MMMM")
)}
</Paragraph>
<Paragraph color={colors.secondary.paragraph}>
{pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
) : null}
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans.selectedProduct as Plan)?.period
) || is5YearPlanSelected ? (
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL,
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
}}
>
{(is5YearPlanSelected
? strings["5yearPlanConditions"]()
: [
strings.trialPlanConditions[0](
billingDuration?.duration as number as never
),
...(isGithubRelease
? []
: [strings.trialPlanConditions[1](Platform.OS as never)])
]
).map((item) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10,
flex: 1
}}
key={item}
>
<Icon
color={colors.primary.accent}
size={AppFontSize.lg}
name="check"
/>
<Paragraph
style={{
flexShrink: 1
}}
>
{item}
</Paragraph>
</View>
))}
</View>
) : null}
<Button
width="100%"
type="accent"
@@ -408,8 +278,50 @@ export const BuyPlan = (props: {
);
}}
/>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{is5YearPlanSelected
? strings.oneTimePurchase()
: strings.cancelAnytimeAlt()}
</Paragraph>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{strings.subTerms[0]()}{" "}
<Text
style={{
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
}}
>
{strings.subTerms[1]()}
</Text>{" "}
{strings.subTerms[2]()}{" "}
<Text
style={{
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
}}
>
{strings.subTerms[3]()}
</Text>
</Paragraph>
</View>
</View>
</ScrollView>
);
};
@@ -477,13 +389,7 @@ const ProductItem = (props: {
style={{
flexDirection: "row",
gap: 10,
opacity: isSubscribed ? 0.5 : 1,
backgroundColor: colors.secondary.background,
padding: Spacing.LEVEL_2,
borderRadius: Radius.S,
borderWidth: 1,
borderColor: colors.primary.border,
justifyContent: "space-between"
opacity: isSubscribed ? 0.5 : 1
}}
activeOpacity={0.9}
onPress={() => {
@@ -502,85 +408,60 @@ const ProductItem = (props: {
);
}}
>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
<Icon
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
size={AppFontSize.lg}
/>
<Icon
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
size={AppFontSize.lg}
/>
<View>
<View
style={{
gap: Spacing.LEVEL_1
flexDirection: "row",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
<View
style={{
flexDirection: "row",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
<Heading size={AppFontSize.md}>
{isAnnual
? strings.yearly()
: is5YearProduct
? strings.fiveYearPlan()
: strings.monthly()}
</Heading>
<Heading size={AppFontSize.md}>
{isAnnual
? strings.yearly()
: is5YearProduct
? strings.fiveYearPlan()
: strings.monthly()}
</Heading>
{discountValue ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
padding: Spacing.LEVEL_0,
alignItems: "center",
justifyContent: "center"
}}
>
<Paragraph color={colors.static.white} size={AppFontSize.xxs}>
{strings.percentOff(`${discountValue}`)}
</Paragraph>
</View>
) : null}
{discountValue ? (
<View
style={{
backgroundColor: colors.static.red,
borderRadius: defaultBorderRadius,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.bestValue()} - {strings.percentOff(`${discountValue}`)}
</Heading>
</View>
) : null}
{isSubscribed ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.currentPlan()}
</Heading>
</View>
) : null}
</View>
<Paragraph>
{is5YearProduct
? strings.billingType.oneTime()
: isAnnual
? strings.billingType.annual()
: strings.billingType.monthly()}
</Paragraph>
{isSubscribed ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.currentPlan()}
</Heading>
</View>
) : null}
</View>
</View>
<View
style={{
gap: Spacing.LEVEL_1
}}
>
<Heading size={AppFontSize.sm}>
<Paragraph size={AppFontSize.md}>
{isAnnual || is5YearProduct
? `${props.pricingPlans.getPrice(
product as RNIap.Subscription,
@@ -591,23 +472,14 @@ const ProductItem = (props: {
? 1
: 0,
isAnnual
)}`
)}/${strings.month()}`
: null}
{!isAnnual && !is5YearProduct
? `${props.pricingPlans.getStandardPrice(
product as RNIap.Subscription
)}`
)}/${strings.month()}`
: null}
</Heading>
<Paragraph
style={{
alignSelf: "flex-end"
}}
size={AppFontSize.xs}
>
/month
</Paragraph>
</View>
</TouchableOpacity>

View File

@@ -1,185 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { DATE_FORMATS } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { Radius, Spacing } from "../../../common/design/spacing";
import { presentSheet } from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type DateFormatProps = {
close?: (ctx?: string) => void;
};
function DateFormat({ close }: DateFormatProps) {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [currentValue, setCurrentValue] = useState(db.settings.getDateFormat());
const onChange = async (item: string) => {
db.settings.setDateFormat(item);
useSettingStore.setState({
dateFormat: item
});
setCurrentValue(item);
close?.();
};
return (
<ScrollView
style={{
maxHeight: height * 0.8
}}
>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="calendar-day"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.dateFormat()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.dateFormatDesc()}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
{DATE_FORMATS.map((item) => {
const selected = currentValue === item;
return (
<Pressable
key={item}
type={selected ? "selected" : "transparent"}
onPress={() => onChange(item)}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.primary.border
}}
>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={colors.primary.heading}
>
{dayjs().format(item)}
</Heading>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{item}
</Paragraph>
</View>
<AppIcon
name={selected ? "radio-button" : "ellipse"}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
</Pressable>
);
})}
</View>
</View>
</ScrollView>
);
}
DateFormat.present = () => {
presentSheet({
component: (_ref, close) => <DateFormat close={close} />
});
};
export default DateFormat;

View File

@@ -1,378 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { format, logManager } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Linking, Platform, View } from "react-native";
import FileViewer from "react-native-file-viewer";
import ReactNativeBlobUtil from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { zip } from "react-native-zip-archive";
import filesystem from "../../../common/filesystem";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
import { Radius, Spacing } from "../../../common/design/spacing";
import { presentSheet } from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { sleep } from "../../../utils/time";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { DatabaseLogger } from "../../../common/database";
type DownloadLogsProps = {
close?: (ctx?: string) => void;
};
type DownloadResult = {
/** Local zip file path used for sharing/opening the archive. */
localPath: string;
/** Directory the archive was saved into (for "open location"). */
directory: string;
};
type Status = "loading" | "success" | "error";
const IconTile = ({
name,
color,
backgroundColor
}: {
name: string;
color: string;
backgroundColor: string;
}) => (
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor
}}
>
<AppIcon name={name} iconFamily="notesnook" size={16} color={color} />
</View>
);
async function downloadLogs(
onStep: (message: string, progress: number) => void
): Promise<DownloadResult> {
onStep(strings.preparingLogsChooseLocation(), 0.1);
let directory =
Platform.OS === "ios"
? await filesystem.checkAndCreateDir(`/debug-logs/`)
: undefined;
if (Platform.OS === "android") {
useUserStore.setState({ disableAppLockRequests: true });
const file = await ScopedStorage.openDocumentTree(true);
setTimeout(() => {
useUserStore.setState({ disableAppLockRequests: false });
}, 1000);
if (!file) throw new Error("no-directory");
directory = file.uri;
}
if (!directory) throw new Error("no-directory");
onStep(strings.preparingLogsCollect(), 0.35);
const logs = await logManager?.get();
if (!logs) throw new Error("no-logs");
const logsDir = cacheDir + `/notesnook-debug-logs`;
if (await ReactNativeBlobUtil.fs.exists(logsDir)) {
await ReactNativeBlobUtil.fs.unlink(logsDir);
}
await ReactNativeBlobUtil.fs.mkdir(logsDir);
for (const logGroup of logs) {
let logString = ``;
for (const log of logGroup.logs) {
logString += `\n${format(log)}`;
}
await ReactNativeBlobUtil.fs.createFile(
`${logsDir}/${logGroup.key}.txt`,
logString,
"utf8"
);
}
onStep(strings.preparingLogsCompress(), 0.7);
const fileName = `notesnook-debug-logs-${Date.now()}.zip`;
const outputPath = await zip(
logsDir,
Platform.OS === "android" ? logsDir + ".zip" : `${directory}/${fileName}`
);
onStep(strings.preparingLogsSave(), 0.9);
if (Platform.OS === "android") {
const file = await ScopedStorage.createFile(
directory,
fileName,
"application/zip"
);
await copyFileAsync("file://" + outputPath, file.uri);
// keep `outputPath` (the cache copy) around so it can be shared/opened.
return { localPath: outputPath, directory };
}
await ReactNativeBlobUtil.fs.unlink(logsDir);
await sleep(1000);
return { localPath: outputPath, directory };
}
function DownloadLogs({ close }: DownloadLogsProps) {
const { colors } = useThemeColors();
const [status, setStatus] = useState<Status>("loading");
const [step, setStep] = useState({
message: strings.preparingLogsChooseLocation(),
progress: 0.1
});
const result = useRef<DownloadResult | undefined>(undefined);
const started = useRef(false);
const start = useCallback(async () => {
setStatus("loading");
setStep({
message: strings.preparingLogsChooseLocation(),
progress: 0.1
});
try {
result.current = await downloadLogs((message, progress) =>
setStep({ message, progress })
);
setStatus("success");
} catch (e) {
DatabaseLogger.error(e as Error);
result.current = undefined;
setStatus("error");
}
}, []);
useEffect(() => {
if (started.current) return;
started.current = true;
start();
}, [start]);
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
paddingBottom: Spacing.LEVEL_4,
gap: Spacing.LEVEL_3
}}
>
{status === "loading" ? (
<View
style={{
alignItems: "center",
justifyContent: "center",
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
<IconTile
name="clock"
color={colors.primary.icon}
backgroundColor={colors.secondary.background}
/>
<Heading fontSize="XL" lineHeight="100%">
{strings.preparingLogs()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{ textAlign: "center" }}
>
{step.message}
</Paragraph>
<View
style={{
width: "100%",
height: 8,
borderRadius: Radius.XXL,
backgroundColor: colors.secondary.background,
overflow: "hidden"
}}
>
<View
style={{
width: `${Math.round(step.progress * 100)}%`,
height: 8,
borderRadius: Radius.XXL,
backgroundColor: colors.primary.accent
}}
/>
</View>
</View>
) : status === "error" ? (
<View style={{ width: "100%", gap: Spacing.LEVEL_3 }}>
<View style={{ alignItems: "center", gap: Spacing.LEVEL_2 }}>
<IconTile
name="warning"
color={colors.error.accent}
backgroundColor={colors.error.background}
/>
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
<Heading
fontSize="XL"
lineHeight="120%"
style={{ textAlign: "center" }}
>
{strings.downloadLogsFailed()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{ textAlign: "center" }}
>
{strings.downloadLogsFailedDesc()}
</Paragraph>
</View>
</View>
<Button
title={strings.tryDownloadLogsAgain()}
type="accent"
width="100%"
style={{ borderRadius: Radius.S }}
onPress={start}
/>
</View>
) : (
<View style={{ width: "100%", gap: Spacing.LEVEL_3 }}>
<View style={{ alignItems: "center", gap: Spacing.LEVEL_2 }}>
<IconTile
name="checks"
color={colors.primary.accent}
backgroundColor={colors.secondary.background}
/>
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
<Heading
fontSize="XL"
lineHeight="120%"
style={{ textAlign: "center" }}
>
{strings.debugLogsDownloaded()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{ textAlign: "center" }}
>
{strings.debugLogsDownloadedDesc()}
</Paragraph>
</View>
</View>
<View style={{ width: "100%", gap: Spacing.LEVEL_2 }}>
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
<Button
title={strings.shareZip()}
type="plain-outline"
style={{ flex: 1, borderRadius: Radius.S }}
onPress={async () => {
const path = result.current?.localPath;
if (!path) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({ url: path }).catch(() => {
/* empty */
});
} else {
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
}
}}
/>
<Button
title={strings.openFileLocation()}
type="plain-outline"
style={{ flex: 1, borderRadius: Radius.S }}
onPress={async () => {
const path = result.current?.localPath;
const directory = result.current?.directory;
if (!path) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "android") {
if (directory) {
Linking.openURL(directory).catch(() => {
/* empty */
});
}
} else {
await sleep(500);
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch(() => {
/* empty */
});
}
}}
/>
</View>
<Button
title={strings.goBack()}
type="accent"
width="100%"
style={{ borderRadius: Radius.S }}
onPress={() => close?.()}
/>
</View>
</View>
)}
</View>
);
}
DownloadLogs.present = () => {
presentSheet({
component: (_ref, close) => <DownloadLogs close={close} />
});
};
export default DownloadLogs;

View File

@@ -19,12 +19,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { ActivityIndicator, Linking, Platform, View } from "react-native";
import React, { Fragment, useState } from "react";
import {
ActivityIndicator,
Linking,
Platform,
StyleSheet,
View
} from "react-native";
import FileViewer from "react-native-file-viewer";
import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";
import {
@@ -34,11 +40,16 @@ import {
} from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import { useSettingStore } from "../../../stores/use-setting-store";
import { getElevationStyle } from "../../../utils/elevation";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
import AppIcon from "../../ui/AppIcon";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
@@ -106,7 +117,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("pdf");
},
icon: "file-pdf",
icon: "file-pdf-box",
id: notesnook.ids.dialogs.export.pdf
},
{
@@ -114,7 +125,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("md");
},
icon: "markdown",
icon: "language-markdown",
id: notesnook.ids.dialogs.export.md
},
{
@@ -122,7 +133,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("md-frontmatter");
},
icon: "markdown",
icon: "language-markdown",
id: notesnook.ids.dialogs.export.md
},
{
@@ -130,7 +141,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("txt");
},
icon: "file-text",
icon: "card-text",
id: notesnook.ids.dialogs.export.text
},
{
@@ -138,222 +149,215 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("html");
},
icon: "file-html",
icon: "language-html5",
id: notesnook.ids.dialogs.export.html
}
];
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
<Dialog context="export-notes" />
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="export"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
justifyContent: "center",
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{complete
? strings.exportSuccessHeading(ids.length)
: strings.exportNotes(ids.length)}
</Heading>
<Paragraph>
{complete
? strings.exportSuccessDesc(result?.fileName as string)
: strings.exportAllNotesDesc()}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View>
{!complete && !exporting ? (
<>
<View
style={{
gap: Spacing.LEVEL_0
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{actions.map((item) => (
<DialogHeader
icon="export"
title={strings.exportNotes(ids.length)}
/>
</View>
<Seperator half />
</>
) : null}
<Dialog context="export-notes" />
<View style={styles.buttonContainer}>
{!exporting && !complete ? (
actions.map((item) => (
<Fragment key={item.title}>
<Seperator half />
<Pressable
key={item.title}
testID={item.id}
type="transparent"
onPress={item.func}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
flexDirection: "row",
paddingRight: 12,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
justifyContent: "flex-start",
gap: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_1,
borderRadius: Radius.S
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
backgroundColor: colors.primary.shade,
borderRadius: defaultBorderRadius,
height: 60,
width: 60,
justifyContent: "center",
backgroundColor: colors.secondary.background
alignItems: "center"
}}
>
<AppIcon
<Icon
name={item.icon}
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
size={AppFontSize.xxxl + 10}
/>
</View>
<Heading fontSize="MD" lineHeight="100%">
{item.title}
</Heading>
<View
style={{
flexShrink: 1
}}
>
<Heading style={{ marginLeft: 10 }} size={AppFontSize.md}>
{item.title}
</Heading>
{/* <Paragraph
style={{ marginLeft: 10 }}
size={SIZE.sm}
color={colors.secondary.paragraph}
>
{item.desc}
</Paragraph> */}
</View>
</Pressable>
))}
</View>
</>
) : (
<View
style={{
minHeight: 150,
justifyContent: "center",
alignItems: "center",
width: "100%",
paddingVertical: Spacing.LEVEL_3,
gap: Spacing.LEVEL_2
}}
>
{!complete ? (
<>
<ActivityIndicator color={colors.primary.accent} />
<Paragraph>
{strings.exportingNotes(status) +
"..." +
" " +
strings.pleaseWait()}
</Paragraph>
</>
) : (
<>
<Button
title={
Platform.OS === "android"
? strings.openFileLocation()
: strings.open()
}
type="accent"
width={"100%"}
onPress={async () => {
if (!result?.filePath) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "android") {
Linking.openURL(result.fileDir).catch((e) => {
ToastManager.error(e as Error);
});
} else {
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch(() => {
ToastManager.show({
heading: strings.noApplicationFound(result.name),
type: "success",
context: "local"
</Fragment>
))
) : (
<View
style={{
minHeight: 150,
justifyContent: "center",
alignItems: "center",
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{!complete ? (
<>
<ActivityIndicator />
<Paragraph>
{strings.exportingNotes(status) +
"..." +
" " +
strings.pleaseWait()}
</Paragraph>
</>
) : (
<>
<IconButton
name="export"
color={colors.primary.icon}
size={50}
style={{
width: 70,
height: 70
}}
/>
<Heading
style={{
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
color={colors.secondary.heading}
>
{strings.exportSuccessHeading(ids.length)}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
{strings.exportSuccessDesc(result?.fileName as string)}
</Paragraph>
<Button
title={
Platform.OS === "android"
? strings.openFileLocation()
: strings.open()
}
type="accent"
width={250}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
if (!result?.filePath) return;
close?.();
if (Platform.OS === "android") {
Linking.openURL(result.fileDir).catch((e) => {
ToastManager.error(e as Error);
});
});
}
}}
/>
<Button
title={strings.share()}
type="secondary-simple"
width={"100%"}
onPress={async () => {
if (!result) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {
/* empty */
});
} else {
FileViewer.open(result.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
}
}}
/>
<Button
title={strings.exportAgain()}
type="plain-outline"
width={"100%"}
onPress={async () => {
setComplete(false);
setResult(undefined);
setExporting(false);
}}
/>
</>
)}
</View>
)}
} else {
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
ToastManager.show({
heading: strings.noApplicationFound(result.name),
type: "success",
context: "local"
});
});
}
}}
/>
<Button
title={strings.share()}
type="secondaryAccented"
width={250}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
if (!result) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {
/* empty */
});
} else {
FileViewer.open(result.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
}
}}
/>
<Button
title={strings.exportAgain()}
type="secondaryAccented"
width={250}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
setComplete(false);
setResult(undefined);
setExporting(false);
}}
/>
</>
)}
</View>
)}
</View>
</View>
);
};
@@ -368,4 +372,38 @@ ExportNotesSheet.present = async (ids?: string[], allNotes?: boolean) => {
});
};
const styles = StyleSheet.create({
container: {
...getElevationStyle(5),
borderRadius: defaultBorderRadius,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
},
buttonContainer: {
justifyContent: "space-between",
alignItems: "center"
},
button: {
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP_VERTICAL,
borderRadius: defaultBorderRadius,
width: "100%",
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
flexDirection: "row"
},
buttonText: {
//fontFamily: "sans-serif",
color: "white",
fontSize: AppFontSize.sm,
marginLeft: 5
},
overlay: {
width: "100%",
height: "100%",
position: "absolute"
}
});
export default ExportNotesSheet;

View File

@@ -24,7 +24,6 @@ import { ToastManager } from "../../../services/event-manager";
import { Button } from "../../ui/button";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { useSettingStore } from "../../../stores/use-setting-store";
export const ShareComponent = ({ uri, name, padding }) => {
return (
<View
@@ -37,7 +36,6 @@ export const ShareComponent = ({ uri, name, padding }) => {
type="accent"
width="100%"
onPress={async () => {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
FileViewer.open(uri, {
showOpenWithDialog: true,
showAppsSuggestions: true
@@ -58,7 +56,6 @@ export const ShareComponent = ({ uri, name, padding }) => {
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
FileViewer.open(uri, {
showOpenWithDialog: true,
showAppsSuggestions: true,

View File

@@ -1,221 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { Button } from "../../ui/button";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { AppFontSize } from "../../../utils/size";
import { presentSheet, eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { sleep } from "../../../utils/time";
import { Progress } from "../progress";
import Sync from "../../../services/sync";
type ForceSyncMode = "fetch" | "send";
type ForceSyncProps = {
mode: ForceSyncMode;
close?: (ctx?: string) => void;
};
function ForceSync({ mode, close }: ForceSyncProps) {
const { colors } = useThemeColors();
const [acknowledged, setAcknowledged] = useState(true);
const onForceSyncPress = async () => {
if (!acknowledged) return;
close?.();
await sleep(300);
Progress.present();
Sync.run("global", true, mode, () => {
eSendEvent(eCloseSheet);
});
};
const title =
mode === "fetch" ? strings.forcePullChanges() : strings.forcePushChanges();
const description =
mode === "fetch"
? strings.forceSyncPullSheetDesc()
: strings.forceSyncPushSheetDesc();
const actionText =
mode === "fetch" ? strings.forcePullAction() : strings.forcePushAction();
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.error.background
}}
>
<AppIcon
name="warning"
iconFamily="notesnook"
size={16}
color={colors.error.accent}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_0
}}
>
<Heading size={AppFontSize.lg} lineHeight="100%">
{title}
</Heading>
<Paragraph size={AppFontSize.xs}>{description}</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
<Paragraph size={AppFontSize.sm}>
{strings.forceSyncWarningShort()}
</Paragraph>
<View
style={{
gap: Spacing.LEVEL_0
}}
>
<Paragraph size={AppFontSize.sm}>
{strings.forceSyncNeedHelpContact()}
</Paragraph>
<Paragraph
size={AppFontSize.sm}
fontFamily="SEMI_BOLD"
color={colors.primary.accent}
>
support@streetwriters.co
</Paragraph>
</View>
</View>
<Pressable
type="shade"
style={{
width: "100%",
borderRadius: Radius.XS,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_1,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
justifyContent: "flex-start"
}}
onPress={() => setAcknowledged((value) => !value)}
>
<AppIcon
size={16}
name={acknowledged ? "checkbox" : "checkbox-blank-outline"}
iconFamily="notesnook"
color={
acknowledged
? [colors.primary.accent, "white"]
: colors.primary.icon
}
/>
<Paragraph size={AppFontSize.sm}>
{strings.forceSyncRiskAcknowledgement()}
</Paragraph>
</Pressable>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
<Button
title={strings.cancel()}
type="plain-outline"
width="100%"
style={{
flex: 1,
borderRadius: Radius.S
}}
onPress={() => close?.()}
/>
<Button
title={actionText}
type="error"
width="100%"
disabled={!acknowledged}
style={{
flex: 1,
borderRadius: Radius.S,
borderColor: colors.error.border
}}
onPress={onForceSyncPress}
/>
</View>
</View>
);
}
ForceSync.present = (mode: ForceSyncMode) => {
presentSheet({
disableClosing: false,
component: (_ref, close) => <ForceSync mode={mode} close={close} />
});
};
export default ForceSync;

View File

@@ -18,33 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Debug, IssueReportResponse } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { Linking, Platform, View } from "react-native";
import Config from "react-native-config";
import {
getBrand,
getModel,
getSystemVersion,
getVersion
} from "react-native-device-info";
import { Radius, Spacing } from "../../../common/design/spacing";
import { Linking, Platform, Text, TextInput, View } from "react-native";
import { getVersion } from "react-native-device-info";
import { useStoredRef } from "../../../hooks/use-stored-ref";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions";
import AppIcon from "../../ui/AppIcon";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size/index";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import {
createFormRef,
FormInput,
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";
import Config from "react-native-config";
import { eCloseSheet } from "../../../utils/events";
export const Issue = ({
defaultTitle,
@@ -61,27 +54,22 @@ export const Issue = ({
const [done, setDone] = useState(false);
const user = useUserStore((state) => state.user);
const [loading, setLoading] = useState(false);
const formRef = useRef(
createFormRef({
title: title.current || "",
body: body.current || ""
})
);
const bodyRef = useRef<TextInput>(null);
const initialLayout = useRef(false);
const issueReportResponse = useRef<IssueReportResponse>(undefined);
const onPress = async () => {
if (loading) return;
if (!formRef.current.validate()) return;
const titleValue = formRef.current.getValue("title").trim();
const bodyValue = formRef.current.getValue("body").trim();
if (!title.current || !body.current) return;
if (title.current?.trim() === "" || body.current?.trim().length === 0)
return;
try {
setLoading(true);
issueReportResponse.current = await Debug.report({
title: titleValue,
title: title.current,
body:
bodyValue +
body.current +
`\n${defaultBody || ""}` +
`\n_______________
**Device information:**
@@ -105,8 +93,6 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
setLoading(false);
body.reset();
title.reset();
formRef.current.setValue("title", "");
formRef.current.setValue("body", "");
setDone(true);
} catch (e) {
setLoading(false);
@@ -149,196 +135,162 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
const responseInfo = getResponseInfo(issueReportResponse.current);
console.log(responseInfo, issueReportResponse.current);
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%"
}}
>
{done ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3
}}
>
<Heading>{responseInfo?.title}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
{responseInfo?.message}
</Paragraph>
<Button
title={responseInfo?.positiveButtonText || strings.done()}
onPress={() => {
if (responseInfo?.url) {
Linking.openURL(responseInfo?.url);
}
eSendEvent(eCloseSheet);
}}
type="accent"
width="100%"
/>
</View>
) : (
<>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
justifyContent: "center",
alignItems: "center",
gap: 10
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.error.background
}}
>
<AppIcon
name="warning-circle"
iconFamily="notesnook"
size={16}
color={colors.error.accent}
/>
</View>
<View
style={{
flex: 1,
justifyContent: "center",
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{issueTitle || strings.issueTitle()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{issueTitle ? strings.issueDesc() : strings.issueDesc2()}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
gap: Spacing.LEVEL_3
}}
>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
<FormInput
name="title"
formRef={formRef}
label={strings.issueSummary()}
placeholder={strings.issueTitlePlaceholder()}
defaultValue={title.current}
validators={[validators.required(strings.allFieldsRequired())]}
onChangeText={(v) => (title.current = v)}
containerStyle={{
borderRadius: Radius.XS
}}
/>
<FormInput
name="body"
formRef={formRef}
label={strings.details()}
placeholder={strings.issuePlaceholder()}
multiline
numberOfLines={5}
textAlignVertical="top"
validators={[validators.required(strings.allFieldsRequired())]}
onChangeText={(v) => (body.current = v)}
containerStyle={{
borderRadius: Radius.XS,
alignItems: "flex-start"
}}
inputStyle={{
minHeight: 100
}}
/>
</View>
<Button
onPress={onPress}
title={loading ? null : strings.submit()}
loading={loading}
width="100%"
type="error"
style={{
borderRadius: Radius.S,
borderColor: colors.error.border
}}
/>
<Heading>{responseInfo?.title}</Heading>
<Paragraph
color={colors.secondary.paragraph}
fontSize="XS"
style={{
textAlign: "center"
}}
selectable={true}
>
{strings.issueNotice[0]()}{" "}
<Paragraph
onPress={() => {
Linking.openURL(
"https://github.com/streetwriters/notesnook/issues"
);
}}
fontSize="XS"
fontFamily="MEDIUM"
style={{
color: colors.primary.accent
}}
>
Github.
</Paragraph>{" "}
{strings.issueNotice[1]()}{" "}
<Paragraph
style={{
color: colors.primary.accent
}}
fontSize="XS"
fontFamily="MEDIUM"
onPress={async () => {
try {
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
} catch (e) {
console.error(e);
}
}}
>
{strings.issueNotice[2]()}
</Paragraph>
{responseInfo?.message}
</Paragraph>
<Button
title={responseInfo?.positiveButtonText || "Done"}
onPress={() => {
if (responseInfo?.url) {
Linking.openURL(responseInfo?.url);
}
eSendEvent(eCloseSheet);
}}
type="accent"
width="100%"
/>
</View>
</>
) : (
<>
<DialogHeader
title={issueTitle || strings.issueTitle()}
paragraph={issueTitle ? strings.issueDesc() : strings.issueDesc2()}
/>
<Seperator half />
<TextInput
placeholder={strings.title()}
onChangeText={(v) => (title.current = v)}
defaultValue={title.current}
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
fontFamily: "Inter-Regular",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
fontSize: AppFontSize.md,
color: colors.primary.heading
}}
placeholderTextColor={colors.primary.placeholder}
/>
<TextInput
ref={bodyRef}
multiline
placeholder={strings.issuePlaceholder()}
numberOfLines={5}
textAlignVertical="top"
onChangeText={(v) => (body.current = v)}
onLayout={() => {
if (initialLayout.current) return;
initialLayout.current = true;
if (body.current) {
bodyRef.current?.setNativeProps({
text: body.current,
selection: {
start: 0,
end: 0
}
});
}
}}
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
fontFamily: "Inter-Regular",
maxHeight: 200,
fontSize: AppFontSize.sm,
marginBottom: 2.5,
color: colors.primary.paragraph
}}
placeholderTextColor={colors.primary.placeholder}
/>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>{`App version: ${getVersion()} Platform: ${
Platform.OS
} Model: ${getBrand()}-${getModel()}-${getSystemVersion()}`}</Paragraph>
<Seperator />
<Button
onPress={onPress}
title={loading ? null : strings.submit()}
loading={loading}
width="100%"
type="accent"
/>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
textAlign: "center"
}}
>
{strings.issueNotice[0]()}{" "}
<Text
onPress={() => {
Linking.openURL(
"https://github.com/streetwriters/notesnook/issues"
);
}}
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
>
github.com/streetwriters/notesnook.
</Text>{" "}
{strings.issueNotice[1]()}{" "}
<Text
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={async () => {
try {
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
} catch (e) {
console.error(e);
}
}}
>
{strings.issueNotice[2]()}
</Text>
</Paragraph>
</>
)}
</View>
);

View File

@@ -17,310 +17,420 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useIsFeatureAvailable } from "@notesnook/common";
import { ContentBlock, Note, createInternalLink } from "@notesnook/core";
import {
ContentBlock,
Note,
VirtualizedGrouping,
createInternalLink
} from "@notesnook/core";
import type { LinkAttributes } from "@notesnook/editor";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, TextInput, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { editorController } from "../../../screens/editor/tiptap/utils";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import { getElevationStyle } from "../../../utils/elevation";
import AppIcon from "../../ui/AppIcon";
import { presentSheet } from "../../../services/event-manager";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import Navigation from "../../../services/navigation";
import { useUserStore } from "../../../stores/use-user-store";
type LinkMode = "note" | "paragraphs";
const ParagraphItem = ({
block,
selected,
onSelect
const ListNoteItem = ({
id,
items,
onSelectNote
}: {
block: ContentBlock;
selected: boolean;
onSelect: (block: ContentBlock) => void;
id: any;
items: VirtualizedGrouping<Note> | undefined;
onSelectNote: any;
}) => {
const { colors } = useThemeColors();
const content =
!block.content || block.content.trim() === ""
? strings.linkNoteEmptyBlock()
: block.content.length > 200
? block.content.slice(0, 200) + "..."
: block.content;
const [item] = useDBItem(id, "note", items);
return (
<Pressable
type={selected ? "selected" : "transparent"}
onPress={() => onSelect(block)}
onPress={() => {
if (!item) return;
onSelectNote(item as Note);
}}
type={"transparent"}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row",
width: "100%",
padding: Spacing.LEVEL_2,
borderRadius: Radius.S,
borderWidth: selected ? 0 : 1,
borderColor: colors.secondary.border,
alignItems: "flex-start"
justifyContent: "flex-start",
height: 50
}}
>
<Paragraph
fontSize="SM"
numberOfLines={2}
color={selected ? colors.primary.heading : colors.primary.paragraph}
<View
style={{
flexShrink: 1
}}
>
{content}
</Paragraph>
<Paragraph numberOfLines={1}>{item?.title}</Paragraph>
</View>
</Pressable>
);
};
const ListBlockItem = ({
item,
onSelectBlock
}: {
item: ContentBlock;
onSelectBlock: any;
}) => {
const { colors } = useThemeColors();
return (
<Pressable
onPress={() => {
onSelectBlock(item);
}}
type={"transparent"}
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-start",
minHeight: 45
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
columnGap: 10,
alignItems: "flex-start",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
justifyContent: "space-between"
}}
>
<Paragraph
style={{
flexShrink: 1
}}
>
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: !item.content || item.content.trim() === ""
? strings.linkNoteEmptyBlock()
: item.content}
</Paragraph>
<View
style={{
borderRadius: defaultBorderRadius,
backgroundColor: colors.secondary.background,
height: 25,
minWidth: 25,
alignItems: "center",
justifyContent: "center"
}}
>
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
{item.type.toUpperCase()}
</Paragraph>
</View>
</View>
</Pressable>
);
};
export default function LinkNote(props: {
note: Note;
attributes: LinkAttributes;
resolverId: string;
onLinkCreated: () => void;
close?: (ctx?: string) => void;
}) {
const { note, resolverId } = props;
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const blockLinking = useIsFeatureAvailable("blockLinking");
const [mode, setMode] = useState<LinkMode>("note");
const [blocks, setBlocks] = useState<ContentBlock[]>([]);
const [selectedBlockId, setSelectedBlockId] = useState<string>();
const { colors } = useThemeColors();
const query = useRef<string>(undefined);
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const nodesRef = useRef<ContentBlock[]>([]);
const [nodes, setNodes] = useState<ContentBlock[]>([]);
const inputRef = useRef<TextInput>(null);
const [selectedNote, setSelectedNote] = useState<Note>();
const [selectedNodeId, setSelectedNodeId] = useState<string>();
const [blocksLoading, setBlocksLoading] = useState(false);
useEffect(() => {
db.notes.contentBlocks(note.id).then((result) => setBlocks(result));
}, [note.id]);
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
setNotes(notes);
});
}, []);
const onAddLink = () => {
const blockId = mode === "paragraphs" ? selectedBlockId : undefined;
const onChange = async (value: string) => {
query.current = value;
if (!selectedNote) {
const notes = await db.lookup.notes(value).sorted();
setNotes(notes);
} else {
if (value.startsWith("#")) {
const headingNodes = nodesRef.current.filter((n) =>
n.type.match(/(h1|h2|h3|h4|h5|h6)/g)
);
setNodes(
headingNodes.filter((n) => n.content.includes(value.slice(1)))
);
} else {
setNodes(nodesRef.current.filter((n) => n.content.includes(value)));
}
}
};
const onCreateLink = (blockId?: string) => {
if (!selectedNote) return;
const link = createInternalLink(
"note",
note.id,
blockId ? { blockId } : undefined
selectedNote.id,
blockId
? {
blockId: blockId
}
: undefined
);
editorController.current?.postMessage(NativeEvents.resolve, {
data: {
href: link,
title: note.title
title: selectedNote.title
},
resolverId
resolverId: props.resolverId
});
props.close?.();
};
const onSelectNote = async (note: Note) => {
setSelectedNote(note);
setBlocksLoading(true);
inputRef.current?.clear();
setTimeout(async () => {
nodesRef.current = await db.notes.contentBlocks(note.id);
setNodes(nodesRef.current);
setBlocksLoading(false);
});
// Fetch and set note's nodes.
};
const onSelectBlock = (block: ContentBlock) => {
onCreateLink(block.id);
props.onLinkCreated();
ToastManager.show({
message: strings.linkAdded(),
type: "success",
context: "global"
});
props.close?.();
};
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
paddingBottom: Spacing.LEVEL_2,
gap: Spacing.LEVEL_4
paddingHorizontal: DefaultAppStyles.GAP,
paddingBottom: DefaultAppStyles.GAP,
minHeight: "100%",
maxHeight: "100%"
}}
>
<View style={{ gap: Spacing.LEVEL_1 }}>
<Heading fontSize="XL" lineHeight="100%">
{strings.linkOptions()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.linkOptionsDesc()}
</Paragraph>
</View>
<View style={{ gap: Spacing.LEVEL_2 }}>
<Heading fontSize="LG" lineHeight="100%">
{strings.selectedNoteLabel()}
</Heading>
<View
style={{
backgroundColor: colors.secondary.background,
borderRadius: Radius.XS,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3
}}
>
<Paragraph
color={colors.primary.heading}
fontFamily="MEDIUM"
fontSize="SM"
>
{note.title}
</Paragraph>
</View>
</View>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2,
backgroundColor: colors.secondary.background,
padding: Spacing.LEVEL_1,
borderRadius: Radius.S
flexDirection: "column",
width: "100%",
alignItems: "flex-start",
gap: 10
}}
>
{(
[
{ key: "note", label: strings.linkEntireNote() },
{ key: "paragraphs", label: strings.specifyParagraphs() }
] as { key: LinkMode; label: string }[]
).map(({ key, label }) => {
const active = mode === key;
return (
<Pressable
key={key}
type="transparent"
onPress={() => setMode(key)}
style={{
flex: 1,
paddingVertical: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_1,
borderRadius: Radius.XS,
backgroundColor: active
? colors.primary.background
: "transparent",
...(active ? getElevationStyle(2) : {})
}}
>
<Heading
fontSize="MD"
style={{ textAlign: "center" }}
color={
active ? colors.primary.accent : colors.secondary.paragraph
}
>
{label}
</Heading>
</Pressable>
);
})}
</View>
<Input
placeholder={
selectedNote
? strings.searchSectionToLinkPlaceholder()
: strings.searchNoteToLinkPlaceholder()
}
containerStyle={{
width: "100%"
}}
marginBottom={0}
onChangeText={(value) => {
onChange(value);
}}
/>
{mode === "note" ? (
<View style={{ gap: Spacing.LEVEL_1 }}>
<Paragraph fontFamily="MEDIUM" fontSize="SM">
{strings.linkedReferences()}
</Paragraph>
{selectedNote ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
padding: Spacing.LEVEL_2,
borderRadius: Radius.S,
borderWidth: 1,
borderColor: colors.secondary.border
gap: 10
}}
>
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
{strings.linkNoteSelectedNote()}
</Paragraph>
<Pressable
onPress={() => {
setSelectedNote(undefined);
setSelectedNodeId(undefined);
setNodes([]);
}}
>
<AppIcon
name="file-text"
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
</View>
<Heading fontSize="SM" style={{ flexShrink: 1 }}>
{strings.entireNote()}
</Heading>
</View>
</View>
) : (
<View style={{ gap: Spacing.LEVEL_3 }}>
<Heading fontSize="LG" lineHeight="100%">
{strings.selectParagraphs()}
</Heading>
{blockLinking?.isAllowed ? (
<ScrollView
nestedScrollEnabled
keyboardShouldPersistTaps="handled"
style={{ maxHeight: height * 0.4 }}
contentContainerStyle={{ gap: Spacing.LEVEL_2 }}
>
{blocks.map((block) => (
<ParagraphItem
key={block.id}
block={block}
selected={selectedBlockId === block.id}
onSelect={(b) => setSelectedBlockId(b.id)}
/>
))}
</ScrollView>
) : (
<View
style={{
gap: Spacing.LEVEL_2,
backgroundColor: colors.secondary.background,
padding: Spacing.LEVEL_3,
borderRadius: Radius.S,
borderWidth: 0.5,
borderColor: colors.secondary.border,
alignItems: "center"
flexDirection: "row",
width: "100%",
justifyContent: "flex-start",
height: 45,
borderWidth: 1,
borderColor: colors.primary.accent,
paddingHorizontal: DefaultAppStyles.GAP
}}
type="secondaryAccented"
>
<Paragraph color={colors.secondary.paragraph}>
{blockLinking?.error}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%"
}}
>
<Paragraph
style={{
flexShrink: 1
}}
numberOfLines={1}
>
{selectedNote?.title}
</Paragraph>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{strings.tapToDeselect()}
</Paragraph>
</View>
</Pressable>
{nodes?.length > 0 ? (
<Paragraph
style={{
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{strings.linkNoteToSection()}
</Paragraph>
<Button
title={strings.upgradePlan()}
style={{ width: "100%" }}
type="accent"
/>
</View>
) : null}
</View>
) : null}
</View>
{selectedNote ? (
<FlatList
renderItem={({ item, index }) => (
<ListBlockItem item={item} onSelectBlock={onSelectBlock} />
)}
</View>
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
keyboardShouldPersistTaps="handled"
windowSize={3}
keyExtractor={(item) => item.id}
ListEmptyComponent={
!blockLinking || blocksLoading ? (
<ActivityIndicator size={25} color={colors.primary.accent} />
) : !blockLinking.isAllowed ? (
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: colors.secondary.background,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius,
borderWidth: 0.5,
borderColor: colors.secondary.border,
alignItems: "center"
}}
>
<Paragraph color={colors.secondary.paragraph}>
{blockLinking?.error}
</Paragraph>
<Button
title={strings.upgradePlan()}
style={{
width: "100%"
}}
type="accent"
onPress={() => {
Navigation.navigate("PayWall", {
context: useUserStore.getState().user
? "logged-in"
: "logged-out",
canGoBack: true
});
props.close?.();
}}
/>
</View>
) : null
}
data={blockLinking?.isAllowed ? nodes : []}
/>
) : (
<FlatList
renderItem={({ item, index }: any) => (
<ListNoteItem
id={index}
items={notes}
onSelectNote={onSelectNote}
/>
)}
keyboardShouldPersistTaps="handled"
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
windowSize={3}
data={notes?.placeholders}
/>
)}
<Button
title={strings.addLink()}
type="accent"
disabled={
mode === "paragraphs" &&
selectedBlockId === undefined &&
blocks.length !== 0
}
width="100%"
onPress={onAddLink}
/>
{selectedNote ? (
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
title={strings.createLink()}
type="accent"
width="100%"
onPress={() => {
onCreateLink();
props.onLinkCreated();
props.close?.();
}}
/>
) : null}
</View>
);
}
LinkNote.present = (
note: Note,
resolverId: string,
onLinkCreated: () => void
) => {
LinkNote.present = (attributes: LinkAttributes, resolverId: string) => {
let didCreateLink = false;
presentSheet({
component: (ref, close) => (
<LinkNote
note={note}
attributes={attributes}
resolverId={resolverId}
onLinkCreated={onLinkCreated}
onLinkCreated={() => {
didCreateLink = true;
}}
close={close}
/>
)
),
onClose: () => {
if (!didCreateLink) {
editorController?.current.commands.dismissCreateInternalLinkRequest(
resolverId
);
}
}
});
};

View File

@@ -1,188 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import { presentSheet } from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const OPTIONS = [
1000 * 60 * 1,
1000 * 60 * 5,
1000 * 60 * 10,
1000 * 60 * 15,
1000 * 60 * 30,
1000 * 60 * 45,
1000 * 60 * 60,
-1
];
const formatValue = (item: number) => {
return item === -1
? strings.never()
: item < 1000 * 60 * 60
? strings.minutes(item / (1000 * 60))
: strings.hours(item / (1000 * 60 * 60));
};
type LockVaultTimerProps = {
close?: (ctx?: string) => void;
};
function LockVaultTimer({ close }: LockVaultTimerProps) {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [currentValue, setCurrentValue] = useState(
useSettingStore.getState().vaultLockAfter
);
const onChange = async (item: number) => {
await db.settings.setVaultLockAfter(item);
useSettingStore.setState({
vaultLockAfter: item
});
setCurrentValue(item);
close?.();
};
return (
<ScrollView
style={{
maxHeight: height * 0.8
}}
>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingTop: Spacing.LEVEL_2
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="clock"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.lockVaultAfter()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.lockVaultAfterDesc()}
</Paragraph>
</View>
</View>
</View>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2
}}
>
{OPTIONS.map((item) => {
const selected = currentValue === item;
return (
<Pressable
key={item}
type={selected ? "selected" : "transparent"}
onPress={() => onChange(item)}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.primary.border
}}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={colors.primary.heading}
>
{formatValue(item)}
</Heading>
<AppIcon
name={selected ? "radio-button" : "ellipse"}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
</Pressable>
);
})}
</View>
</ScrollView>
);
}
LockVaultTimer.present = () => {
presentSheet({
component: (_ref, close) => <LockVaultTimer close={close} />
});
};
export default LockVaultTimer;

View File

@@ -37,8 +37,8 @@ import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { FontSizes } from "../../../common/design/font";
import { Radius, Spacing } from "../../../common/design/spacing";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { AuthMode } from "../../auth/common";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
@@ -87,188 +87,181 @@ export default function PaywallSheet<Tid extends FeatureId>(props: {
SubscriptionProvider.GOOGLE &&
Platform.OS === "android");
if (!pricingPlans.currentPlan) return null;
const features = [
{
title: `${
PlanOverView[pricingPlans.currentPlan.id as keyof typeof PlanOverView]
.storage
} ${strings.cloudStorage()}`,
description: strings.cloudStorageBenefit()
},
...(pricingPlans.currentPlan.id !== "essential"
? [
{
title: strings.appLockSecurity(),
description: strings.appLockSecurityBenefit()
}
]
: []),
{
title: strings.advancedNoteElements(),
description: strings.advancedNoteElementsBenefit()
}
];
const onUpgrade = () => {
if (PremiumService.get()) {
if (
pricingPlans.user?.subscription.plan === SubscriptionPlan.LEGACY_PRO ||
!isCurrentPlatform
) {
ToastManager.show({
message: strings.cannotChangePlan(),
context: "local"
});
return;
}
if (isSubscribedOnWeb) {
ToastManager.show({
message: strings.changePlanOnWeb(),
context: "local"
});
return;
}
}
eSendEvent(eCloseSheet);
if (!useUserStore.getState().user) {
Navigation.navigate("Auth", {
mode: AuthMode.login
});
return;
}
Navigation.navigate("PayWall", {
context: "logged-in",
state: {
planId: pricingPlans.currentPlan?.id,
productId: isGithubRelease
? "yearly"
: (pricingPlans.selectProduct as any).productId,
billingType: "annual"
}
});
};
return (
return !pricingPlans.currentPlan ? null : (
<View
style={{
width: "100%",
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_3
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View style={{ gap: Spacing.LEVEL_3, width: "100%" }}>
<>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
width: "100%"
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<Paragraph>
<AppIcon
name="crown-simple"
iconFamily="notesnook"
size={16}
name="crown"
size={AppFontSize.md}
color={colors.static.orange}
/>
</View>
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
<Heading fontSize="XL" lineHeight="100%">
{strings.tryItForFree()}
{strings.upgradePlanTo(pricingPlans.currentPlan?.name)}
</Paragraph>
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL_SMALL,
width: "100%"
}}
>
<Heading>{strings.tryItForFree()}</Heading>
<Heading size={AppFontSize.sm}>
{strings.getThisAndSoMuchMore()}
</Heading>
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
{strings.unlockPremiumFeatures()}
</Paragraph>
</View>
</View>
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
<View style={{ gap: Spacing.LEVEL_2, width: "100%" }}>
{features.map((feature) => (
<View
key={feature.title}
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
width: "100%"
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon
name="check"
iconFamily="notesnook"
size={16}
color={colors.primary.accent}
style={{ marginTop: 1 }}
/>
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
<Heading fontFamily="MEDIUM" fontSize="SM" lineHeight="100%">
{feature.title}
</Heading>
<Paragraph fontSize="XS" color={colors.primary.paragraph}>
{feature.description}
<AppIcon name="cloud" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
<Heading size={AppFontSize.sm}>
{
PlanOverView[
pricingPlans.currentPlan.id as keyof typeof PlanOverView
].storage
}
</Heading>{" "}
{strings.cloudSpace()}
</Paragraph>
</View>
{pricingPlans.currentPlan.id !== "essential" ? (
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="lock" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
<Heading size={AppFontSize.sm}>{strings.appLock()}</Heading>{" "}
{strings.appLockFeatureBenefit()}
</Paragraph>
</View>
) : null}
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="vector-link" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
{strings.advancedNoteTaking[0]()}{" "}
<Heading size={AppFontSize.sm}>
{strings.advancedNoteTaking[1]()}
</Heading>{" "}
{strings.advancedNoteTaking[2]()}
</Paragraph>
</View>
))}
</View>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0 + 2,
padding: Spacing.LEVEL_1,
borderRadius: Radius.XS,
backgroundColor: colors.selected.background
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph fontSize="XS" color={colors.secondary.heading}>
{strings.sevenDayFreeTrial()}
</Paragraph>
<View
<Paragraph
style={{
width: 4,
height: 4,
borderRadius: Radius.MD,
backgroundColor: colors.secondary.heading
marginVertical: 10
}}
size={AppFontSize.xs}
>
<Heading size={AppFontSize.xs}>{strings.cancelAnytime()}</Heading>{" "}
{strings.googleReminderTrial()}
</Paragraph>
<Button
type="accent"
title={strings.upgrade()}
style={{
marginVertical: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
onPress={() => {
if (PremiumService.get()) {
if (
pricingPlans.user?.subscription.plan ===
SubscriptionPlan.LEGACY_PRO ||
!isCurrentPlatform
) {
ToastManager.show({
message: strings.cannotChangePlan(),
context: "local"
});
return;
}
if (isSubscribedOnWeb) {
ToastManager.show({
message: strings.changePlanOnWeb(),
context: "local"
});
return;
}
}
eSendEvent(eCloseSheet);
if (!useUserStore.getState().user) {
Navigation.navigate("Auth", {
mode: AuthMode.login
});
return;
}
Navigation.navigate("PayWall", {
context: "logged-in",
state: {
planId: pricingPlans.currentPlan?.id,
productId: isGithubRelease
? "yearly"
: (pricingPlans.selectProduct as any).productId,
billingType: "annual"
}
});
}}
/>
<Paragraph fontSize="XS" color={colors.secondary.heading}>
{strings.cancelAnytimeShort()}
</Paragraph>
</View>
</View>
<View
style={{ flexDirection: "row", gap: Spacing.LEVEL_2, width: "100%" }}
>
{isSubscribedOnWeb ? null : (
<Button
type="plain-outline"
title={strings.viewPlans()}
fontSize={FontSizes.MD}
style={{
flex: 1,
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_3
}}
type="plain"
title={strings.exploreAllPlans()}
icon="arrow-right"
iconPosition="right"
onPress={() => {
eSendEvent(eCloseSheet);
Navigation.navigate("PayWall", {
@@ -279,18 +272,7 @@ export default function PaywallSheet<Tid extends FeatureId>(props: {
}}
/>
)}
<Button
type="accent"
title={strings.upgrade()}
fontSize={FontSizes.MD}
style={{
flex: 1,
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_3
}}
onPress={onUpgrade}
/>
</View>
</>
</View>
);
}

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