Compare commits

...

90 Commits

Author SHA1 Message Date
Ammar Ahmed
4f212453a6 mobile: fix sort options not working in trash 2026-05-02 11:04:11 +05:00
Ammar Ahmed
2c1b9081bf mobile: fix group type usage
Explicitly pass group type to list and sort sheet.
2026-05-02 10:49:08 +05:00
Ammar Ahmed
46d802548b mobile: fix Archive screen does not show correct date/time on note based on sorting 2026-04-30 13:27:52 +05:00
Ammar Ahmed
2bf42c207d Merge pull request #9776 from streetwriters/mobile/release-3.3.21
mobile: v3.3.21 release
2026-04-30 10:05:04 +05:00
Ammar Ahmed
65e42e5d8b mobile: prepare for v3.3.21 release 2026-04-29 16:36:09 +05:00
Ammar Ahmed
c85007f0dd Merge pull request #9705 from streetwriters/fix/screenguard
mobile: fix screenguard on mobile & privacy mode
2026-04-29 16:20:06 +05:00
Ammar Ahmed
36058145b5 mobile: disable privacy mode on user logout 2026-04-29 16:19:18 +05:00
Ammar Ahmed
a376a6b497 mobile: fix screenguard on mobile & privacy mode 2026-04-29 16:19:18 +05:00
Ammar Ahmed
cd62d51e45 Merge pull request #9752 from streetwriters/mobile/fix-locked-note-merge-conflict
mobile: fix saving incoming note in merge conflict dialog does not work
2026-04-29 16:16:38 +05:00
Abdullah Atta
3f6fcbacbc global: fix dialog on successful bug report (#9750)
* core: handle different issue report responses

* web: better handling for issue report responses

* mobile: update issue report dialog to show correct info of where issue is reported

* mobile: close sheet on clicking "done"

* web: do not include email in issue success dialog

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-04-29 14:19:29 +05:00
01zulfi
2a2bf67fea web: use expire verbiage in subscription subtitle when provider is gift card (#9759)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-29 08:45:52 +05:00
01zulfi
b8fca5cd98 Merge pull request #9765 from streetwriters/release/3.3.16
web: bump version to 3.3.16
2026-04-28 15:48:34 +05:00
01zulfi
4d87155fdd web: bump version to 3.3.16
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-28 10:17:27 +05:00
01zulfi
ed38bb6299 editor: fix crash in backspace handler (#9740)
* editor: fix crash in backspace handler
* replace nextNode with .nodeAfter

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

* editor: fix position of node when deleting nextNode

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-28 09:08:31 +05:00
Abdullah Atta
7c9e3e79df web: small refactor to make the build pass (#9761) 2026-04-27 12:54:44 +05:00
Chloe Oletto
43b90070a9 core: Limit note subtitle/headline length. (#9732)
Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-23 22:27:33 +05:00
Ammar Ahmed
04f6e1068b mobile: fix saving incoming note in merge conflict dialog does not work 2026-04-23 10:43:07 +05:00
01zulfi
9bea4adcf9 web: show titlebar on auth pages (#9093)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-22 13:51:21 +05:00
Chloe Oletto
a5790de23c desktop: Stop urgent reminder causes segfault & app freeze on Linux. (#9737)
* desktop: Stop urgent reminder causes segfault & app freeze on Linux.

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

* desktop: Linux segfault
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Apply suggestion from @thecodrr

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-04-22 11:56:47 +05:00
Abdullah Atta
eb4edeea8f web: fix crash on non-paddle related iframe event (#9747)
Stack trace:
```
undefined is not an object (evaluating 'w.data')
E@https://app.notesnook.com/assets/paddle-Z43isEce-h-M.js:23:45428
E@https://app.notesnook.com/assets/paddle-Z43isEce-h-M.js:23:45846
```
2026-04-22 09:46:29 +05:00
01zulfi
e3ccd504da ui: fix submenu cutting off when there are a lot of lazy items (#9739)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-22 09:11:28 +05:00
Ammar Ahmed
4627d7d5df Merge pull request #9736 from streetwriters/mobile/release-3.3.20
mobile: release 3.3.20
2026-04-22 08:46:43 +05:00
01zulfi
f216c1b65d web: fix scrollbar blocking date in compact note list item (#9719)
* web: fix scrollbar blocking date in compact note list item
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: adjust notes list scrollbar width, compact item padding, & group header padding
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-21 11:08:30 +05:00
Abdullah Atta
4692ba7183 desktop: use newer appimage bundler (#9744)
this should fix libfuse and libz errors
2026-04-21 10:55:52 +05:00
Ammar Ahmed
543825918c mobile: fix useEffect deps in use-feature-manager hook 2026-04-21 08:46:19 +05:00
Ammar Ahmed
9f995b861b mobile: fix toast position incorrect with keyboard opened 2026-04-21 08:45:45 +05:00
Ammar Ahmed
9963a6c655 mobile: delete vault action keeps loading when entering incorrect password 2026-04-21 08:45:29 +05:00
Ammar Ahmed
1696ec5c72 mobile: fix unable to switch between yearly/monthly plans 2026-04-21 08:14:38 +05:00
Ammar Ahmed
7c95faad21 mobile: fix clear trash option is disabled on login 2026-04-20 15:43:39 +05:00
Ammar Ahmed
ca80441c14 mobile: fix trash count does not update on calling clear trash 2026-04-20 15:30:26 +05:00
01zulfi
9c497fd2bb Merge pull request #9724 from streetwriters/release/3.3.15
web: bump version to 3.3.15
2026-04-20 12:21:46 +05:00
Ammar Ahmed
ef5e5e5b4c mobile: release 3.3.20 2026-04-20 12:03:12 +05:00
Abdullah Atta
ede900fdbd ci: add linux arm64 preview builds (#9735) 2026-04-20 11:56:10 +05:00
01zulfi
1758f03f93 intl: regen locale files (#9734)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-20 11:39:13 +05:00
01zulfi
f18db748e0 web: bump version to 3.3.15
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-20 10:38:18 +05:00
01zulfi
2fba2af126 web: fix pdf viewer's search dialog unreadable UI in dark mode (#9720)
* web: fix pdf viewer's search dialog unreadable UI in dark mode
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix pdf search dialog ui

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-18 10:46:09 +05:00
Ammar Ahmed
1ef3d712e1 mobile: fix parent notebook can be moved into it's child notebooks. (#9691)
* mobile: fix parent notebook can be moved into it's child notebooks.

1. Ensure notebook that is being moved and all it's children are not visible in move-notebook dialog
2. Ensure child cannot be moved into it's parent again
3. Ensure child can be moved into it's sibling notebooks

* mobile: add e2e tests

* mobile: fix toast position
2026-04-18 09:28:39 +05:00
Ammar Ahmed
405adff8f4 mobile: fix linting not working in vscode and fix lint errors (#9710) 2026-04-18 08:52:39 +05:00
Ammar Ahmed
9a21ca1cf4 mobile: fix discount value shows as undefined on iOS when buying plan (#9711)
* mobile: fix discount value is undefined

* mobile: fix discount calculation and price formatting

Respect spacing when writing price in a currency.
Fix incorrect discount calculation when comparing products of a plan based on int values, use parseFloat to parse correct values for comparision

* mobile: fix ui inconsistencies

* mobile: conditionally include trial plan conditions based on GitHub release status
2026-04-18 08:40:50 +05:00
01zulfi
49252e66ce web: fix RCE via stored XSS in note html export (#9709)
* sandbox the export iframe
* escape fields when building html template

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-17 18:10:06 +05:00
Abdullah Atta
4ba2e900c5 desktop: disable autoInstallOnAppQuit (#9726) 2026-04-17 17:07:21 +05:00
01zulfi
b6714b559e web: make dates text reactive to date format setting (#9707)
the following dates text are now reactive:
* dates in note list item
* date in notebook header
* dates in properties panel
* date in reminder list item

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-16 12:42:10 +05:00
Ammar Ahmed
a6c1757cf6 mobile: fix list-items do not show correct notes count in side menu (#9697)
* mobile: fix list-items do not show correct notes count in side menu

* mobile: fix notebook note count not updated on deleting note

* mobile: fix monograph count does not update on unpublishing note in side-menu

* mobile: fix notebook and tag shortcut count does not update in sidemenu
2026-04-15 21:43:45 +05:00
01zulfi
3d847ea00c web: fix time field not aligning in reminder dialog (#9202)
* also capitalize month string

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-15 12:49:22 +05:00
Abdullah Atta
63453942ee Merge pull request #9686 from streetwriters/web/clear-trash
web: add clear trash option in trash nav item context menu
2026-04-15 12:48:50 +05:00
Ammar Ahmed
cb5a7904d3 mobile: add clear trash option in Trash properties
mobile: show clear trash success message

mobile: add e2e tests

mobile: fix clear trash option remains disabled after moving item to trash after login

mobile: Disable "Move to trash" for deleted notes.
2026-04-15 12:48:24 +05:00
01zulfi
f2c088fb18 web: add clear trash option in trash nav item context menu
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: disable clear trash if empty && update confirm message
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-15 12:48:10 +05:00
01zulfi
4e2fd9dde6 web: show error text on settings number input's out of range (#9171)
* web: show error text on settings number input's out of range
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: minor refactor and fixes

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-15 08:04:58 +05:00
Ammar Ahmed
92c1ca7b6f mobile: fix configure toolbar do not render properly on adding/removing groups (#9699)
* editor: make editor tools object a function

Required for string localization to work properly

* mobile: fix tools and groups do not re-render properly on changing preset or adding tools

* mobile: fix empty toast message shows on removing tools from group
2026-04-14 22:46:41 +05:00
01zulfi
5fcad35cdc Merge pull request #9712 from streetwriters/release/3.3.14
web: bump version to 3.3.14
2026-04-14 15:14:39 +05:00
01zulfi
2b11fcea41 web: bump version to 3.3.14
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-14 13:14:49 +05:00
Chloe Oletto
3f0c24d27b core: duplicating a note should copy all the flags (#9250)
Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-14 08:23:50 +05:00
01zulfi
71088fca99 editor: handle crash on inserting incorrect syntax in math nodes (#9690)
* editor: handle crash on inserting incorrect syntax in math nodes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix 'math error' text shown on newly empty math node
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 20:35:57 +05:00
01zulfi
d6f7f0c0be editor: fix crash when app is unlocked after a relock (#9698)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 20:35:16 +05:00
01zulfi
4044b68701 webclipper: fix notebooks getting unselected on tag select (#9706)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 14:55:20 +05:00
Abdullah Atta
610ced00af global: whitelist postinstall packages on bootstrap (#9629)
* global: enable npm ignore scripts by default

* config: disable ignore-scripts in .npmrc

* mobile: update lockfiles

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-04-13 14:27:32 +05:00
Abdullah Atta
f9fecda1f1 ci: enable CSC_FOR_PULL_REQUEST for desktop preview builds (#9695) 2026-04-10 14:45:24 +05:00
Ammar Ahmed
74921ae1b3 mobile: release 3.3.19 (#9683)
* mobile: release 3.3.19

* mobile: fix ios production xcconfig
2026-04-08 09:37:56 +05:00
01zulfi
40f2c88ead Merge pull request #9682 from streetwriters/release/3.3.13
web: bump version to 3.3.13
2026-04-07 15:49:17 +05:00
01zulfi
7fe410ae89 web: bump version to 3.3.13
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-07 14:00:37 +05:00
Ammar Ahmed
4cbdd085d6 mobile: fix editor stops updating notes after first two notes edited (#9681) 2026-04-07 13:18:50 +05:00
01zulfi
24556dab83 web: show better error message on invalid recovery key (#9680)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-07 13:00:02 +05:00
Abdullah Atta
dd8116eba8 Merge pull request #9677 from streetwriters/fix/table-cell-selection-mobile
Fix table cell selection on mobile
2026-04-06 23:55:18 +05:00
Abdullah Atta
ae82a8c0a0 editor: only allow resizing column from resize handle 2026-04-06 23:49:04 +05:00
Chloe Oletto
af4dc5e9ea web: fix all tabs closed on sync when any note is deleted from another device (#9252)
* web: All tabs closed when any note is deleted.

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

* web: remove redundant noteID check, fix check to ensure locked notes are reopened.

Signed-off By: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-06 23:02:12 +05:00
Abdullah Atta
71b03a90dd editor: disable scroll during column resize 2026-04-06 21:01:25 +05:00
Abdullah Atta
728e50cd13 editor: fix awkward selection behaviour in table cells
this includes fixes to multiple UX bugs:

1. scroll now stops when user starts cell selection
2. moving cell selection to either edge auto scrolls the table
3. text selection does not trigger cell selection
2026-04-06 21:00:23 +05:00
01zulfi
c8e8935b34 web: add back button in recovery forms (#9676)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-06 16:27:33 +05:00
01zulfi
21f4b906d7 web: fix html entities shown in match result (#9645)
* web: fix non-breaking space shown as encoded in match result
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: decode matches in db.lookup.notesWithHighlighting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: decode entities at parse time

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-06 14:20:10 +05:00
01zulfi
d49d42b736 common: guard against undefined backStack/forwardStack in TabSessionHistory (#9674)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-06 12:42:00 +05:00
SahilAslam001
267fe4e453 mobile: remove restoring app state on app launch #6294 (#9586)
Signed-off-by: Sahil Aslam <sahil.aslam011@gmail.com>
2026-04-06 11:20:30 +05:00
01zulfi
413ca61ef5 core: $headline$ format should get 60 chars from first p or h tag (#9658)
* core: note title headline format should get content from first p or h tag
* also decrease character limit to 60

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

* Apply suggestion from @thecodrr

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-06 09:20:21 +05:00
Ammar Ahmed
02b96470bc Merge pull request #9631 from streetwriters/fix/file-size-check-fail
mobile: fix file size check shows error but continues uploading the file
2026-04-02 13:13:56 +05:00
Ammar Ahmed
7234128f78 docs(help): add reminders help page (#9637)
* docs: add remindershelp page

* Apply suggestions from code review

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-04-02 12:38:28 +05:00
01zulfi
27e070e555 web: redirect automatically after final step in reset password flow (#9642)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-02 12:29:16 +05:00
01zulfi
539950bdc5 core: perform fuzzy search again if query contains non-alphanumeric chars (#9634)
* core: simplify fuzzy separator regex
* remove lookahead and lookbehind

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

* core: do 2 pass fuzzy search if query contains non-alphanumeric chars

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-02 12:13:06 +05:00
Ammar Ahmed
83280e7cee docs: clarify what happens on account deletion (#9636)
* docs: clarify what happens on account deletion

* intl: clarify what happens when account is deleted
2026-04-01 15:23:57 +05:00
01zulfi
cee0275565 web: handle crash on preview attachment (#9626)
* web: handle crash on preview attachment
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: better error message on attachment preview failure

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-01 13:07:17 +05:00
Ammar Ahmed
87bddf0037 mobile: fix file size check shows error but continues uploading the file 2026-04-01 11:06:33 +05:00
SahilAslam001
e736e3d810 mobile: do not auto fill auto generated title when user clears title field in editor (#9623)
Signed-off-by: Sahil Aslam <sahil.aslam011@gmail.com>
2026-04-01 09:31:21 +05:00
01zulfi
092f15406c editor: fix table column toolbar not showing in correct position (#9583)
* editor: fix table column toolbar not showing in correct position
* also scroll to the newly created column if it is not visible

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

* editor: use tr.scrollIntoView instead of manually adjusting position on new column
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: add tr.scrollIntoView() for table's selectRow()
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-31 13:28:03 +05:00
01zulfi
befcd0f112 web: refresh user on change email & run timer on first email sent (#9620)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-31 09:53:15 +05:00
Ammar Ahmed
8b92936d05 Add workflow to build android/ios previews on each PR (#9604)
Builds and publishes iOS and Android Preview apps that are instantly available to registered testers and quality assurance teams. We are using Firebase App Distribution for this purpose that works seamlessly on both android and iOS.

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2026-03-31 09:42:24 +05:00
01zulfi
aec135de83 web: fix plans page being cut-off from top (#9594)
* web: add top padding to plans page
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix plans page being cut-off from top
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-31 09:41:28 +05:00
01zulfi
f9738168e5 editor: fix callout first heading being collapsed/expanded if clicked towards right of it (#9547)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-30 20:15:03 +05:00
Ammar Ahmed
e873b88dbd Merge pull request #9618 from streetwriters/mobile-release/3.3.18
mobile: release 3.3.18
2026-03-30 09:43:53 +05:00
Abdullah Atta
214f5b8a98 ci: fix desktop pr preview workflow (#9612)
* ci: fix desktop pr preview workflow

Removed dependency on build-linux-arm64 for posting PR comments.

Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>

* ci: do not sign windows staging build

* clipper(web): regen package-lock.json

* ci: fix `Unknown target: x64`

---------

Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-28 14:05:43 +05:00
Abdullah Atta
70cf17fdd5 ci: add workflow for desktop pr preview (#9607)
* ci: add workflow for desktop pr preview

* ci: workflow didn't run

* ci: limit workflow to specific paths
2026-03-28 13:25:55 +05:00
Abdullah Atta
7d557810cb ci: add workflow for notesnook web pr preview (#9593)
* ci: add workflow for notesnook web pr preview

* ci: remove benchmarks workflow

* ci: use `setup-node-with-cache`

* ci: only publish pr preview when packages/** or apps/web/** changes
2026-03-28 12:47:17 +05:00
Abdullah Atta
ed24282703 web: bump version to 3.3.12 (#9602) 2026-03-28 11:09:19 +05:00
224 changed files with 15622 additions and 19002 deletions

View File

@@ -0,0 +1,135 @@
name: Notesnook Android Preview
on:
pull_request_target:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/android.preview.firebase.yml"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
runs-on: ubuntu-22.04
env:
STAGING_BUILD: true
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/android && chmod +x ./gradlew
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build arm64-v8a apk
run: |
cd apps/mobile/android
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
- name: Get app version
id: package-version
uses: saionaro/extract-package-version@master
with:
path: apps/mobile
- name: Publish to firebase CLI
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- android-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/generated/sourcemaps/**/*.map

301
.github/workflows/desktop.preview.yml vendored Normal file
View File

@@ -0,0 +1,301 @@
name: Preview @notesnook/desktop
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/desktop/**"
- "apps/web/**"
- "packages/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.preview.yml"
jobs:
build:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Generate desktop build (stable)
run: npm run tx @notesnook/web:build:desktop
- name: Build desktop bundle
working-directory: ./apps/desktop
run: npm run bundle
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
build-macos:
name: Build for macOS
needs: build
runs-on: macos-14
outputs:
macos-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install setuptools
run: brew install python-setuptools
- name: Setup notarization
run: |
mkdir -p ~/private_keys/
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/desktop
run: |
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
echo ::set-output name=bundle_version::$(cat package.json | jq -r .build.mac.bundleVersion)
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
- name: Build dmg
env:
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
CSC_FOR_PULL_REQUEST: true
run: |
npm run tx @notesnook/desktop:release
cd apps/desktop
yarn electron-builder --config=electron-builder.config.js --mac dmg --arm64 -p never
- name: Upload dmg artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: macos-build
path: apps/desktop/output/*.dmg
build-linux-x64:
name: Build for Linux x64
needs: build
runs-on: ubuntu-22.04
outputs:
linux-x64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
working-directory: ./apps/desktop
- name: Upload AppImage artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: linux-x64-build
path: apps/desktop/output/*.AppImage
build-linux-arm64:
name: Build for Linux arm64
needs: build
runs-on: ubuntu-22.04-arm
outputs:
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
working-directory: ./apps/desktop
- name: Upload AppImage artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: linux-arm64-build
path: apps/desktop/output/*.AppImage
build-windows:
name: Build for Windows
needs: build
runs-on: windows-latest
outputs:
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build
run: node scripts/execute.mjs @notesnook/desktop:release
- name: Publish
env:
NOTESNOOK_STAGING: true
run: yarn electron-builder --config=electron-builder.config.js --win nsis:x64 --publish never
working-directory: ./apps/desktop
- name: Upload exe artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: windows-build
path: apps/desktop/output/*.exe
post-pr-comment:
name: Post PR comment with preview URLs
needs:
[build, build-macos, build-linux-x64, build-linux-arm64, build-windows]
runs-on: ubuntu-latest
steps:
- name: Post or update PR comment
uses: actions/github-script@v6
env:
macos_artifact_url: ${{ needs.build-macos.outputs.macos-artifact-url }}
linux_x64_artifact_url: ${{ needs.build-linux-x64.outputs.linux-x64-artifact-url }}
linux_arm64_artifact_url: ${{ needs.build-linux-arm64.outputs.linux-arm64-artifact-url }}
windows_artifact_url: ${{ needs.build-windows.outputs.windows-artifact-url }}
with:
script: |
const marker = '<!-- desktop-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = [
{ platform: 'macOS', url: process.env.macos_artifact_url },
{ platform: 'Linux x64', url: process.env.linux_x64_artifact_url },
{ platform: 'Linux arm64', url: process.env.linux_arm64_artifact_url },
{ platform: 'Windows x64', url: process.env.windows_artifact_url },
].filter(u => u.url).map(u => `- [${u.platform}](${u.url})`).join('\n');
const body = `${marker}\n**Desktop Previews**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -0,0 +1,168 @@
name: Notesnook iOS Preview
on:
pull_request_target:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/ios.preview.firebase.yml"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npm run tx @notesnook/mobile:build
- name: Cache Pods
uses: actions/cache@v3
id: pods-cache
with:
path: apps/mobile/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/ios
pod install
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Get marketing version
id: marketing-version
run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Make staging xcconfig active
run: |
cd apps/mobile/ios/build-configs
./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}}
- name: Build iOS App
uses: ammarahm-ed/ios-build-action@master
with:
bundle-identifier: org.streetwriters.notesnook
scheme: Notesnook
configuration: "Release"
export-options: apps/mobile/ios/ExportOptionsStaging.plist
export-method: "ad-hoc"
project-path: apps/mobile/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
update-targets: |
Notesnook
Make Note
NotesWidgetExtension
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
code-signing-identity: Apple Distribution
team-id: ${{ secrets.APPLE_TEAM_ID }}
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
output-path: Notesnook.ipa
mobileprovision-base64: |
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-preview-ipa
path: Notesnook.ipa
if-no-files-found: error
retention-days: 1
publish:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 20
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: ios-preview-ipa
path: .
- name: Publish to firebase CLI
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_IOS_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: Notesnook.ipa
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- ios-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

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

80
.github/workflows/web.preview.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Notesnook Web PR Preview
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/web/**"
- "packages/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.preview.yml"
jobs:
build-and-deploy:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install dependencies
run: npm ci
- name: Build web
run: npm run build:web
- name: Deploy to Cloudflare Pages
id: deploy
working-directory: ./apps/web
run: |
set -euo pipefail
BRANCH=pr-${{ github.event.number }}-$(echo "${{ github.sha }}" | cut -c1-7)
echo "Deploying branch: $BRANCH"
DEPLOY_OUT=$(npx --yes wrangler pages deploy --project-name=notesnook-app --branch="$BRANCH" ./build 2>&1) || { echo "$DEPLOY_OUT"; exit 1; }
echo "$DEPLOY_OUT"
PREVIEW_URL=$(printf "%s" "$DEPLOY_OUT" | grep -Eo 'https?://[^ ]+' | head -1 || true)
if [ -z "$PREVIEW_URL" ]; then
echo "WARNING: could not parse preview URL from wrangler output"
fi
echo "preview_url=$PREVIEW_URL" >> $GITHUB_ENV
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ env.preview_url }}
with:
script: |
const marker = '<!-- pages-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Cloudflare Pages Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
min-release-age=7 # days
# ignore-scripts=true

View File

@@ -198,6 +198,9 @@ module.exports = {
}
}
},
toolsets: {
appimage: "1.0.2"
},
snap: {
autoStart: false,
confinement: "strict",

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.3.11",
"version": "3.3.16",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -48,7 +48,7 @@
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^37.0.0",
"electron-builder": "^26.0.12",
"electron-builder": "^26.8.1",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
"node-gyp-build": "^4.8.4",

View File

@@ -27,6 +27,7 @@ import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.join(__dirname, "..");
const RUNNING_PROCESSES = [];
const RESTARTABLE_PROCESSES = [];
let lastBundleHash = null;
@@ -36,7 +37,7 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
process.chdir(root);
await onChange(true);
@@ -55,13 +56,16 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec(
"npm run postinstall --verbose",
path.join(root, "node_modules", "electron")
);
await exec("yarn electron-builder install-app-deps");
await exec("yarn electron-builder install-app-deps", root);
}
await exec(`yarn run bundle`);
await exec(`yarn run build`);
await exec(`yarn run bundle`, root);
await exec(`yarn run build`, root);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -111,7 +115,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
async function exec(cmd, cwd) {
try {
console.log(">", cmd);
console.log(">", cmd, cwd);
return execSync(cmd, {
env: ENV,

View File

@@ -178,7 +178,9 @@ export const osIntegrationRouter = t.router({
})
});
notification.show();
if (input.urgency === "critical") {
if (input.urgency === "critical" && process.platform !== "linux") {
// due to an Electron bug in versions below 40.x, shell.beep() causes a segfault on Linux.
// TODO: Remove when migrating to Electron 40.x or newer.
shell.beep();
}

View File

@@ -37,7 +37,11 @@ async function configureAutoUpdater() {
config.releaseTrack === "stable" &&
autoUpdater.currentVersion.prerelease.length > 0;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
// Do NOT auto-install on quit. On Windows, if the system shuts down while
// the NSIS installer is running, it first removes all old files and then
// gets killed before copying new ones — leaving an empty install directory.
// Updates should only be installed when the user explicitly triggers it.
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.disableWebInstaller = true;
}

View File

@@ -5,6 +5,7 @@ apply plugin: 'kotlin-parcelize'
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
import com.android.build.OutputFile
import groovy.json.JsonSlurper
import org.apache.tools.ant.taskdefs.condition.Os
@@ -94,6 +95,18 @@ def fdroidBuild() {
return project.hasProperty("fdroidBuild") && project.fdroidBuild == "true"
}
def prBuildNumber() {
def value = project.getProperties().get("prBuildNumber")
return value ? value : "1000"
}
def stagingReleaseBuild() {
// Enable with STAGING_BUILDtrue or -PstagingReleaseBuild=true
def fromEnv = System.getenv("STAGING_BUILD") == "true"
def fromProp = project.hasProperty("stagingReleaseBuild") && project.stagingReleaseBuild == "true"
return fromEnv || fromProp
}
def getNpmVersion() {
def inputFile = file("$rootDir/../package.json")
def jsonPackage = new JsonSlurper().parseText(inputFile.text)
@@ -118,14 +131,17 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
namespace "com.streetwriters.notesnook"
defaultConfig {
applicationId "com.streetwriters.notesnook"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3098
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3102
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -192,6 +208,8 @@ android {
output.versionCodeOverride =
defaultConfig.versionCode * 5 + versionCodes.get(abi)
println("Fdroid Version code: ${output.versionCodeOverride} for abi ${versionCodes.get(abi)}");
} else if (stagingReleaseBuild()) {
output.versionCodeOverride = defaultConfig.versionCode + versionCodes.get(abi)
} else {
if (isBuildingAAB) {
output.versionCodeOverride = 4 * 1048576 + defaultConfig.versionCode
@@ -233,7 +251,6 @@ dependencies {
} else {
implementation jscFlavor
}
}

View File

@@ -199,11 +199,7 @@
</intent-filter>
</activity>
<service
android:name=".OnClearFromRecentService"
android:enabled="true"
android:exported="true"
android:stopWithTask="false" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
<service

View File

@@ -28,10 +28,6 @@ public class MainActivity extends ReactActivity {
WebView.setWebContentsDebuggingEnabled(true);
}
try {
startService(new Intent(getBaseContext(), OnClearFromRecentService.class));
} catch (Exception ignored) {}
}
/**

View File

@@ -1,36 +0,0 @@
package com.streetwriters.notesnook;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.util.Log;
public class OnClearFromRecentService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
try {
SharedPreferences appStateDetails = getApplicationContext().getSharedPreferences("appStateDetails", MODE_PRIVATE);
SharedPreferences.Editor edit = appStateDetails.edit();
edit.remove("appState");
edit.apply();
stopSelf();
} catch (UnsatisfiedLinkError | Exception e) {
}
}
}

View File

@@ -93,21 +93,6 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void setAppState(final String appState) {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = appStateDetails.edit();
edit.putString("appState", appState);
edit.apply();
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String getAppState() {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
String appStateValue = appStateDetails.getString("appState", "");
return appStateValue.isEmpty() ? null : appStateValue;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public int getWidgetId() {
return NotePreviewConfigureActivity.appWidgetId;

View File

@@ -36,6 +36,8 @@ allprojects {
url "$rootDir/../node_modules/detox/Detox-android"
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}

View File

@@ -1,3 +1,3 @@
- Minor bug fixes and performance improvements
- Bug fixes and improvements
Thank you for using Notesnook!
Thank you for using Notesnook!

View File

@@ -23,7 +23,7 @@ import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import React, { PropsWithChildren, useEffect } from "react";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -66,6 +66,9 @@ const App = (props: { configureMode: "note-preview" }) => {
useEffect(() => {
SettingsService.onFirstLaunch();
changeSystemBarColors();
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
setTimeout(async () => {
await Notifications.get();
if (SettingsService.get().notifNotes) {
@@ -106,8 +109,10 @@ let currTheme =
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: (props: any) => JSX.Element) => {
return function AppWithThemeProvider(props: any) {
export const withTheme = (
Element: (props: PropsWithChildren) => JSX.Element
) => {
return function AppWithThemeProvider(props: PropsWithChildren) {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,

View File

@@ -25,7 +25,6 @@ import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
@@ -205,9 +204,8 @@ export async function getDatabaseKey(appLockPassword?: string) {
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
const userKeyCredentials =
await Keychain.getInternetCredentials("notesnook");
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(

View File

@@ -72,7 +72,7 @@ export async function setupDatabase(password?: string) {
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
}) as ICompressor,
batchSize: 50,
sqliteOptions: {
dialect: (name) => ({

View File

@@ -109,8 +109,8 @@ class RNSqliteConnection implements DatabaseConnection {
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);

View File

@@ -112,8 +112,8 @@ export async function writeEncryptedBase64(
async function deleteLocalFile(filename: string) {
try {
await createCacheDir();
let path = cacheDir + `/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
const path = cacheDir + `/${filename}`;
const exists = await RNFetchBlob.fs.exists(path);
if (Platform.OS === "ios" && !exists) {
const iosAppGroup =
Platform.OS === "ios"
@@ -309,7 +309,9 @@ export async function deleteDCacheFiles() {
});
}
}
} catch (e) {}
} catch (e) {
/** Empty */
}
}
export async function getCachePathForFile(filename: string) {

View File

@@ -35,6 +35,8 @@ import Upload from "@ammarahmed/react-native-upload";
import { CloudUploader } from "react-native-nitro-cloud-uploader";
import { useUserStore } from "../../stores/use-user-store";
import { sleep } from "../../utils/time";
import { isFeatureAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
// Upload constants
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
@@ -199,6 +201,20 @@ export async function uploadFile(
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
const featureResult = await isFeatureAvailable(
"fileSize",
fileInfo.size || 0
);
if (!featureResult.isAllowed) {
ToastManager.show({
heading: strings.fileTooLarge(),
message: featureResult.error,
type: "error"
});
return false;
}
if (
remoteFileSize > FileSizeResult.Empty &&
remoteFileSize === fileInfo.size

View File

@@ -36,7 +36,7 @@ export const Announcement = () => {
state.announcements,
state.remove
]);
let announcement = announcements.length > 0 ? announcements[0] : null;
const announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !announcement || selectionMode ? null : (

View File

@@ -32,7 +32,7 @@ import { Action } from "../../stores/use-message-store";
export const Cta = (props: BodyItemProps) => {
const { colors } = useThemeColors();
let buttons =
const buttons =
props.item.actions.filter((item) => allowedOnPlatform(item.platforms)) ||
[];

View File

@@ -53,7 +53,6 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { strings } from "@notesnook/intl";
import { AppFontSize } from "../../utils/size";
import { editorController } from "../../screens/editor/tiptap/utils";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
@@ -239,8 +238,8 @@ const AppLocked = () => {
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,

View File

@@ -46,7 +46,6 @@ import {
eOnLoadNote
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { openNote } from "../list-items/note/wrapper";

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { LegendList } from "@legendapp/list";
import {
Attachment,
FilteredSelector,

View File

@@ -53,7 +53,7 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
}
setLoading(true);
try {
let lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
const lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
if (
lastRecoveryEmailTime &&
Date.now() - lastRecoveryEmailTime < 60000 * 3

View File

@@ -22,14 +22,9 @@ import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -195,8 +190,8 @@ export const Login = ({
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,

View File

@@ -92,7 +92,7 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.tokenManager.getToken();
const res = await db.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
@@ -102,7 +102,7 @@ export const SessionExpired = () => {
Sync.run("global", false, "full", async (complete) => {
if (!complete) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
setVisible(true);
@@ -115,7 +115,7 @@ export const SessionExpired = () => {
setVisible(false);
});
} catch (e) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
setFocused(false);

View File

@@ -99,7 +99,7 @@ export const Signup = ({
try {
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(email.current!.toLowerCase(), password.current!);
let user = await db.user.getUser();
const user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
clearMessage();

View File

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

View File

@@ -23,7 +23,6 @@ import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
SafeAreaView,
StyleSheet,
TouchableOpacity,
@@ -136,8 +135,8 @@ const BaseDialog = ({
backgroundColor: background
? background
: transparent
? "transparent"
: "rgba(0,0,0,0.3)"
? "transparent"
: "rgba(0,0,0,0.3)"
}}
>
<KeyboardAvoidingView
@@ -155,8 +154,8 @@ const BaseDialog = ({
justifyContent: centered
? "center"
: bottom
? "flex-end"
: "flex-start"
? "flex-end"
: "flex-start"
}
]}
>

View File

@@ -22,7 +22,6 @@ import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { Button, ButtonProps } from "../ui/button";
import { PressableProps } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";

View File

@@ -201,12 +201,14 @@ export const AppLockPassword = () => {
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
}
/>
@@ -364,7 +366,9 @@ export const AppLockPassword = () => {
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(SettingsService.get());
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
@@ -383,8 +387,8 @@ export const AppLockPassword = () => {
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"

View File

@@ -110,61 +110,6 @@ export const VaultDialog: React.FC = () => {
const confirmPasswordRef = useRef<string | null>(null);
const newPasswordRef = useRef<string | null>(null);
const open = useCallback(async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
if (data.item) {
const locked = await db.vaults.itemExists(data.item);
noteLockedRef.current = locked;
if (!locked) {
const content = await db.content.findByNoteId(data.item!.id);
if (content && isEncryptedContent(content)) {
noteLockedRef.current = true;
}
}
}
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
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;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setVisible(true);
}
}, []);
const close = useCallback(() => {
if (loading) {
ToastManager.show({
@@ -249,6 +194,7 @@ export const VaultDialog: React.FC = () => {
close();
}, 100);
} else {
setLoading(false);
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
@@ -282,6 +228,10 @@ export const VaultDialog: React.FC = () => {
setLoading(false);
close();
eSendEvent("vaultUpdated");
ToastManager.show({
message: strings.vaultCleared(),
type: "success"
});
} catch (e) {
ToastManager.show({
heading: strings.passwordIncorrect(),
@@ -292,6 +242,45 @@ export const VaultDialog: React.FC = () => {
setLoading(false);
}, [close]);
const enrollFingerprint = useCallback(
async (password: string) => {
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
close();
} catch (e) {
close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setLoading(false);
}
},
[close]
);
const takeErrorAction = useCallback(() => {
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}, []);
const lockNote = useCallback(async () => {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
@@ -333,7 +322,13 @@ export const VaultDialog: React.FC = () => {
.catch((e) => {
takeErrorAction();
});
}, [close, biometricUnlock, isBiometryEnrolled]);
}, [
biometricUnlock,
isBiometryEnrolled,
close,
enrollFingerprint,
takeErrorAction
]);
const openInEditor = useCallback(
(note: Note & { content?: NoteContent<false> }) => {
@@ -387,19 +382,7 @@ export const VaultDialog: React.FC = () => {
} catch (e) {
takeErrorAction();
}
}, [close]);
const takeErrorAction = useCallback(() => {
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}, []);
}, [close, takeErrorAction]);
const openNote = useCallback(async () => {
try {
@@ -440,6 +423,7 @@ export const VaultDialog: React.FC = () => {
}, [
biometricUnlock,
isBiometryEnrolled,
enrollFingerprint,
openInEditor,
shareNote,
deleteNote,
@@ -464,33 +448,6 @@ export const VaultDialog: React.FC = () => {
}
}, [permanantUnlock, openNote]);
const enrollFingerprint = useCallback(
async (password: string) => {
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
close();
} catch (e) {
close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setLoading(false);
}
},
[close]
);
const createVault = useCallback(async () => {
await db.vault.create(passwordRef.current || "");
@@ -536,31 +493,6 @@ export const VaultDialog: React.FC = () => {
}
}, []);
const onPressFingerprintAuth = useCallback(
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[]
);
const onPress = useCallback(async () => {
const requestType = requestTypeRef.current;
@@ -689,6 +621,89 @@ export const VaultDialog: React.FC = () => {
deleteVault
]);
const onPressFingerprintAuth = useCallback(
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[onPress]
);
const open = useCallback(
async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
if (data.item) {
const locked = await db.vaults.itemExists(data.item);
noteLockedRef.current = locked;
if (!locked) {
const content = await db.content.findByNoteId(data.item!.id);
if (content && isEncryptedContent(content)) {
noteLockedRef.current = true;
}
}
}
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
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;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setVisible(true);
}
},
[onPressFingerprintAuth]
);
useEffect(() => {
eSubscribeEvent(eOpenVaultDialog, open);
eSubscribeEvent(eCloseVaultDialog, close);
@@ -731,7 +746,8 @@ export const VaultDialog: React.FC = () => {
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingTop: 12,
overflow: "hidden"
}}
>
<DialogHeader
@@ -798,7 +814,8 @@ export const VaultDialog: React.FC = () => {
!isBiometryAvailable ||
isCreateVault ||
isChangePassword ||
isCustomAction ? null : (
isCustomAction ||
isDeleteVault ? null : (
<Button
onPress={() =>
onPressFingerprintAuth(strings.unlockNote(), "")

View File

@@ -22,7 +22,6 @@ import React, {
RefObject,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from "react";
@@ -39,8 +38,6 @@ import Animated, {
WithSpringConfig,
withTiming
} from "react-native-reanimated";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { getAppState } from "../../screens/editor/tiptap/utils";
import { eSendEvent } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { eClearEditor } from "../../utils/events";
@@ -84,18 +81,9 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
}: TabProps,
ref
) {
const appState = useMemo(() => getAppState(), []);
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const translateX = useSharedValue(
widths
? appState &&
appState?.movedAway === false &&
useTabStore.getState().getCurrentNoteId()
? widths.sidebar + widths.list
: widths.sidebar
: 0
);
const translateX = useSharedValue(widths ? widths.sidebar : 0);
const startX = useSharedValue(0);
const currentTab = useSharedValue(1);
const previousTab = useSharedValue(1);
@@ -125,10 +113,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = 0;
} else {
if (prevWidths.current?.sidebar !== widths.sidebar) {
translateX.value =
appState && appState?.movedAway === false
? editorPosition
: widths.sidebar;
translateX.value = widths.sidebar;
if (translateX.value === editorPosition) {
onChangeTab?.({ i: 2, from: 1 });
}
@@ -136,15 +121,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
}
isLoaded.current = true;
prevWidths.current = widths;
}, [
deviceMode,
widths,
fullscreen,
translateX,
editorPosition,
appState,
onChangeTab
]);
}, [deviceMode, widths, fullscreen, translateX, editorPosition, onChangeTab]);
useEffect(() => {
const sub = BackHandler.addEventListener("hardwareBackPress", () => {

View File

@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import {
GroupHeader,
GroupingKey,
GroupOptions,
ItemType
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
@@ -40,6 +45,7 @@ type SectionHeaderProps = {
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
group: GroupingKey;
onOpenJumpToDialog: () => void;
};
@@ -53,6 +59,7 @@ export const SectionHeader = React.memo<
color,
screen,
groupOptions,
group,
onOpenJumpToDialog
}: SectionHeaderProps) {
const { colors } = useThemeColors();
@@ -133,6 +140,7 @@ export const SectionHeader = React.memo<
<Sort
screen={screen}
type={dataType}
group={group}
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}

View File

@@ -53,6 +53,7 @@ type ListProps = {
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
groupType: GroupingKey;
id?: string;
};
@@ -73,16 +74,7 @@ export default function List(props: ListProps) {
props.dataType === "notebook" ||
notebooksListMode === "compact";
const groupType =
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
const groupOptions = useGroupOptions(props.groupType);
const _onRefresh = async () => {
Sync.run("global", false, "full", () => {
@@ -94,7 +86,7 @@ export default function List(props: ListProps) {
(item: number | boolean, index: number) => {
return props.data?.type(index);
},
[]
[props.data]
);
const renderItem = React.useCallback(
@@ -105,7 +97,7 @@ export default function List(props: ListProps) {
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={groupType as GroupingKey}
group={props.groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
@@ -115,7 +107,7 @@ export default function List(props: ListProps) {
},
[
groupOptions,
groupType,
props.groupType,
props.customAccentColor,
props.data,
props.dataType,

View File

@@ -53,7 +53,7 @@ import TagItem from "../list-items/tag";
import { SearchResult } from "../list-items/search-result";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
group: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
isSheet: boolean;
index: number;
@@ -182,6 +182,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -218,6 +219,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -245,6 +247,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -271,6 +274,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -297,6 +301,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}

View File

@@ -30,7 +30,7 @@ import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
import { IconButton } from "../ui/icon-button";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import { strings } from "@notesnook/intl";
import { ToastManager } from "../../services/event-manager";
@@ -142,7 +142,7 @@ function ReorderableList<T extends { id: string }>({
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
return items;
}, [data, customizableSidebarFeature?.isAllowed]);
}, [customizableSidebarFeature?.isAllowed, data, itemOrderState]);
return (
<View style={styles.container}>

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { getFormattedDate } from "@notesnook/common";
import {
EncryptedContentItem,
isEncryptedContent,
Note,
UnencryptedContentItem
} from "@notesnook/core";
@@ -75,9 +74,11 @@ const MergeConflicts = () => {
const { height } = useSettingStore((state) => state.dimensions);
const applyChanges = async () => {
let contentToSave = selectedContent;
const contentToSave = selectedContent;
if (!contentToSave) return;
let note = await db.notes.note(contentToSave.noteId);
const note = await db.notes.note(
(content.current as UnencryptedContentItem).noteId
);
if (!note) return;
await db.notes.add({
id: note.id,
@@ -462,7 +463,8 @@ const MergeConflicts = () => {
<ReadonlyEditor
editorId="conflictSecondary"
onLoad={async (loadContent) => {
const note = await db.notes.note(content.current?.noteId!);
if (!content.current?.noteId) return;
const note = await db.notes.note(content.current?.noteId);
if (!note) return;
loadContent({
id: note.id,

View File

@@ -63,22 +63,25 @@ const HistoryItem = ({
}${_end_time}`;
};
const preview = useCallback(async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
});
}, []);
const preview = useCallback(
async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
});
},
[note]
);
return (
<Pressable
@@ -138,7 +141,7 @@ export default function NoteHistory({
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} note={note} />
),
[history]
[history, note]
);
return (

View File

@@ -42,7 +42,6 @@ import {
isEncryptedContent,
Note,
NoteContent,
SessionContentItem,
TrashOrItem
} from "@notesnook/core";

View File

@@ -18,14 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFeaturesTable } from "@notesnook/common";
import {
EV,
EVENTS,
Plan,
SKUResponse,
SubscriptionPlan,
User
} from "@notesnook/core";
import { EVENTS, Plan, SubscriptionPlan, User } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
@@ -55,6 +48,7 @@ import {
TECHLORE_SVG,
XDA_SVG
} from "../../assets/images/assets";
import { db } from "../../common/database";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import usePricingPlans, {
PlanOverView,
@@ -76,7 +70,6 @@ import { IconButton } from "../ui/icon-button";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { db } from "../../common/database";
const Steps = {
select: 1,
@@ -119,7 +112,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
}
setStep(Steps.buy);
}
}, [routeParams.state]);
}, [pricingPlans, routeParams.state]);
useEffect(() => {
let listener: NativeEventSubscription;
@@ -137,7 +130,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
return () => {
listener?.remove();
};
}, [isFocused, step]);
}, [isFocused, routeParams.context, step]);
useEffect(() => {
const sub = db.eventManager.subscribe(
@@ -154,7 +147,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
return () => {
sub?.unsubscribe();
};
}, []);
}, [routeParams.context]);
const is5YearPlanSelected = (
isGithubRelease
@@ -936,7 +929,10 @@ const PricingPlanCard = ({
setStep: (step: number) => void;
}) => {
const { colors } = useThemeColors();
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
const regionalDiscount =
annualBilling && plan.id === "pro"
? pricingPlans?.regionalDiscount
: undefined;
const { width } = useWindowDimensions();
const isTablet = width > 600;
@@ -959,26 +955,6 @@ const PricingPlanCard = ({
annualBilling
);
useEffect(() => {
if (pricingPlans?.isGithubRelease || !annualBilling) return;
pricingPlans
?.getRegionalDiscount(
plan.id,
pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: `notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
)
.then((value) => {
setRegionaDiscount(value);
});
}, [annualBilling]);
useEffect(() => {
if (!annualBilling) {
setRegionaDiscount(undefined);
}
}, [annualBilling]);
const isSubscribed =
product?.productId &&
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
@@ -1008,8 +984,8 @@ const PricingPlanCard = ({
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}

View File

@@ -27,7 +27,11 @@ import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import {
eSendEvent,
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
@@ -63,6 +67,7 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(item.id, "color");
eSendEvent(refreshNotesPage);
};
@@ -127,7 +132,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}, []);
}, [colorFeature]);
return (
<>
@@ -140,6 +145,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(color.id, "color");
eSendEvent(refreshNotesPage);
}}
/>

View File

@@ -35,7 +35,7 @@ export const DateMeta = ({ item }: { item: Item }) => {
const [dateCreated, setDateCreated] = useState(item.dateCreated);
function getDateMeta() {
let keys = Object.keys(item);
const keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),

View File

@@ -173,15 +173,15 @@ export const Items = ({
DDS.isTab
? AppFontSize.xxl
: shouldShrink
? AppFontSize.xxl
: AppFontSize.lg
? AppFontSize.xxl
: AppFontSize.lg
}
color={
item.checked
? item.activeColor || colors.primary.accent
: item.id.match(/(delete|trash)/g)
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
</Pressable>
@@ -212,8 +212,8 @@ export const Items = ({
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
? colors.error.paragraph
: colors.primary.paragraph
}}
testID={"icon-" + item.id}
onPress={item.onPress}
@@ -277,8 +277,8 @@ export const Items = ({
item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
@@ -318,7 +318,9 @@ export const Items = ({
[
colors.error.icon,
colors.primary.accent,
colors.primary.border,
colors.secondary.icon,
colors.static.orange,
columnItemWidth,
topBarSorting
]

View File

@@ -77,7 +77,7 @@ export const TagStrip = ({ item, close }) => {
.then((tags) => {
setTags(tags);
});
}, []);
}, [item]);
return tags?.length > 0 ? (
<View

View File

@@ -42,8 +42,6 @@ import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import ExportNotesSheet from "../sheets/export-notes";
import { MoveNotebook } from "../../screens/move-notebook";
import { IconButton } from "../ui/icon-button";
import NativeTooltip from "../../utils/tooltip";
import ManageTags from "../../screens/manage-tags";

View File

@@ -16,13 +16,14 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Plan, SKUResponse } from "@notesnook/core";
import { Plan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
Linking,
Platform,
ScrollView,
Text,
TouchableOpacity,
@@ -31,7 +32,6 @@ import {
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { WebView } from "react-native-webview";
import { db } from "../../../common/database";
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { ToastManager } from "../../../services/event-manager";
@@ -211,16 +211,19 @@ export const BuyPlan = (props: {
? strings["5yearPlanConditions"]()
: [
strings.trialPlanConditions[0](
billingDuration?.duration as number
billingDuration?.duration as number as never
),
strings.trialPlanConditions[1](0)
...(isGithubRelease
? []
: [strings.trialPlanConditions[1](Platform.OS as never)])
]
).map((item) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10
gap: 10,
flex: 1
}}
key={item}
>
@@ -229,7 +232,13 @@ export const BuyPlan = (props: {
size={AppFontSize.lg}
name="check"
/>
<Paragraph>{item}</Paragraph>
<Paragraph
style={{
flexShrink: 1
}}
>
{item}
</Paragraph>
</View>
))}
</View>
@@ -243,8 +252,8 @@ export const BuyPlan = (props: {
is5YearPlanSelected
? strings.purchase()
: pricingPlans?.userCanRequestTrial
? strings.subscribeAndStartTrial()
: strings.subscribe()
? strings.subscribeAndStartTrial()
: strings.subscribe()
}
onPress={async () => {
if (isGithubRelease) {
@@ -321,7 +330,10 @@ const ProductItem = (props: {
productId: string;
}) => {
const { colors } = useThemeColors();
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
const regionalDiscount =
props.productId === "notesnook.pro.yearly"
? props.pricingPlans.regionalDiscount
: undefined;
const product =
props.pricingPlans?.currentPlan?.subscriptions?.[
regionalDiscount?.sku || props.productId
@@ -358,27 +370,19 @@ const ProductItem = (props: {
props.pricingPlans.user?.subscription?.productId ===
(product as Plan)?.id);
useEffect(() => {
props.pricingPlans
?.getRegionalDiscount(
props.pricingPlans.currentPlan?.id as string,
props.pricingPlans.isGithubRelease
? ((product as Plan)?.period as string)
: props.productId
)
.then((value) => {
if (
value &&
value.sku?.startsWith(
(props.pricingPlans.selectedProduct as RNIap.Subscription)
?.productId
)
) {
props.pricingPlans.selectProduct(value?.sku as string);
}
setRegionaDiscount(value);
});
}, []);
const discountValue =
(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount)
? regionalDiscount
? regionalDiscount.discount
: isGithubRelease
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)
: undefined;
return (
<TouchableOpacity
@@ -420,12 +424,11 @@ const ProductItem = (props: {
{isAnnual
? strings.yearly()
: is5YearProduct
? strings.fiveYearPlan()
: strings.monthly()}
? strings.fiveYearPlan()
: strings.monthly()}
</Heading>
{(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount) ? (
{discountValue ? (
<View
style={{
backgroundColor: colors.static.red,
@@ -436,18 +439,7 @@ const ProductItem = (props: {
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.bestValue()} -{" "}
{strings.percentOff(
(regionalDiscount
? regionalDiscount.discount
: isGithubRelease
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)) as string
)}
{strings.bestValue()} - {strings.percentOff(`${discountValue}`)}
</Heading>
</View>
) : null}

View File

@@ -21,7 +21,6 @@ import React from "react";
import { View } from "react-native";
import FileViewer from "react-native-file-viewer";
import { ToastManager } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Button } from "../../ui/button";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";

View File

@@ -17,18 +17,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Debug } from "@notesnook/core";
import { Debug, IssueReportResponse } from "@notesnook/core";
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { Linking, Platform, Text, TextInput, View } from "react-native";
import { getVersion } from "react-native-device-info";
import { useStoredRef } from "../../../hooks/use-stored-ref";
import { ToastManager } from "../../../services/event-manager";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { openLinkInBrowser } from "../../../utils/functions";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size/index";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
@@ -37,17 +37,26 @@ import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import Config from "react-native-config";
import { eCloseSheet } from "../../../utils/events";
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
export const Issue = ({
defaultTitle,
defaultBody,
issueTitle
}: {
defaultTitle?: string;
defaultBody?: string;
issueTitle?: string;
}) => {
const { colors } = useThemeColors();
const body = useStoredRef("issueBody");
const body = useStoredRef("issueBody", "");
const title = useStoredRef("issueTitle", defaultTitle);
const [done, setDone] = useState(false);
const user = useUserStore((state) => state.user);
const [loading, setLoading] = useState(false);
const bodyRef = useRef();
const bodyRef = useRef<TextInput>(null);
const initialLayout = useRef(false);
const issueUrl = useRef();
const issueReportResponse = useRef<IssueReportResponse>(undefined);
const onPress = async () => {
if (loading) return;
@@ -57,7 +66,7 @@ export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
try {
setLoading(true);
issueUrl.current = await Debug.report({
issueReportResponse.current = await Debug.report({
title: title.current,
body:
body.current +
@@ -72,7 +81,7 @@ Logged in: ${user ? "yes" : "no"}
Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
userId: user?.id
});
if (!issueUrl.current) {
if (!issueReportResponse.current) {
setLoading(false);
ToastManager.show({
heading: "Failed to report issue on github",
@@ -88,12 +97,46 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
} catch (e) {
setLoading(false);
ToastManager.show({
heading: e.message,
heading: (e as Error).message,
type: "error"
});
}
};
function getResponseInfo(response?: IssueReportResponse) {
if (!response || "error" in response) return;
switch (response.type) {
case "email": {
return {
title: strings.yourSupportRequestHasBeenForwarded(),
message: strings.supportEmailMessage()
};
}
case "discussion": {
const url = response.url;
return {
title: strings.thankYouForFeedback(),
positiveButtonText: strings.copyLink(),
message: strings.featureRequestMessage(url),
url: url
};
}
case "issue": {
const url = response.url;
return {
title: strings.thankYouForReporting(),
positiveButtonText: strings.copyLink(),
message: strings.bugReportMessage(url),
url: url
};
}
}
}
const responseInfo = getResponseInfo(issueReportResponse.current);
console.log(responseInfo, issueReportResponse.current);
return (
<View
style={{
@@ -105,38 +148,28 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
<>
<View
style={{
height: 250,
justifyContent: "center",
alignItems: "center",
gap: 10
}}
>
<Heading>{strings.issueCreatedHeading()}</Heading>
<Heading>{responseInfo?.title}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
{strings.issueCreatedDesc[0]()}
<Paragraph
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={() => {
Linking.openURL(issueUrl.current);
}}
>
{issueUrl.current}
</Paragraph>
. {strings.issueCreatedDesc[1]()}
{responseInfo?.message}
</Paragraph>
<Button
title={strings.openIssue()}
title={responseInfo?.positiveButtonText || "Done"}
onPress={() => {
Linking.openURL(issueUrl.current);
if (responseInfo?.url) {
Linking.openURL(responseInfo?.url);
}
eSendEvent(eCloseSheet);
}}
type="accent"
width="100%"
@@ -248,10 +281,7 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://discord.gg/zQBK97EE22",
colors
);
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
} catch (e) {
console.error(e);
}

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import React, { useEffect } from "react";
import { View } from "react-native";
import {
eSendEvent,
@@ -28,7 +28,11 @@ import {
ToastManager
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { eCloseSheet } from "../../../utils/events";
import {
eAfterSync,
eCloseSheet,
eMenuItemUpdate
} from "../../../utils/events";
import { SideMenuItem } from "../../../utils/menu-items";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
@@ -37,12 +41,25 @@ import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import PaywallSheet from "../paywall";
import { presentDialog } from "../../dialog/functions";
import { db } from "../../../common/database";
import { useTrashStore } from "../../../stores/use-trash-store";
import { useSettingStore } from "../../../stores/use-setting-store";
export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
const { colors } = useThemeColors();
const featuresAvailable = useAreFeaturesAvailable([
"customHomepage",
"customizableSidebar"
]);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const trash = useTrashStore((state) => state.items);
useEffect(() => {
if (!isAppLoading) {
useTrashStore.getState().refresh();
}
}, [isAppLoading]);
return !featuresAvailable ? null : (
<View
style={{
@@ -100,7 +117,38 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
},
icon: "sort-ascending",
locked: !featuresAvailable?.customizableSidebar.isAllowed
}
},
...(item.id === "Trash"
? [
{
title: strings.clearTrash(),
onPress: async () => {
if (!trash || trash?.length === 0) return;
eSendEvent(eCloseSheet);
setTimeout(() => {
presentDialog({
title: strings.clearTrashConfirm(),
paragraph: strings.clearTrashDesc(),
positiveText: strings.clear(),
positivePress: async () => {
await db.trash.clear();
useTrashStore.getState().clear();
eSendEvent(eMenuItemUpdate);
eSendEvent(eAfterSync);
ToastManager.show({
message: strings.trashCleared(),
type: "success"
});
return true;
}
});
}, 500);
},
icon: "delete-sweep-outline",
disabled: !trash || trash?.length === 0
}
]
: [])
].map((item) => (
<Pressable
key={item.title}
@@ -112,7 +160,7 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
gap: DefaultAppStyles.GAP_SMALL,
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP,
opacity: item.locked ? 0.6 : 1
opacity: item.disabled || item.locked ? 0.6 : 1
}}
onPress={() => {
item.onPress();

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { Notebook } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";

View File

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

View File

@@ -1,3 +1,22 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import {
FeatureUsage,
formatBytes,
@@ -76,10 +95,10 @@ export function PlanLimits() {
{item.total === Infinity
? strings.unlimited()
: item.id === "storage"
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
</Paragraph>
</View>
</View>

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { hosts, Monograph, Note } from "@notesnook/core";
import { hosts, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
@@ -28,12 +28,15 @@ import {
TouchableOpacity,
View
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { openLinkInBrowser } from "../../../utils/functions";
@@ -46,22 +49,17 @@ import Input from "../../ui/input";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useAsync } from "react-async-hook";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { eMenuItemUpdate } from "../../../utils/events";
import { useIsFeatureAvailable } from "@notesnook/common";
async function fetchMonographData(noteId: string) {
const monographId = db.monographs.monograph(noteId);
const monograph = monographId
? await db.monographs.get(monographId)
: undefined;
const analyticsFeature = await isFeatureAvailable("monographAnalytics");
const analytics =
monographId && analyticsFeature
? await db.monographs.analytics(monographId)
: undefined;
return {
monograph,
monographId,
analytics
monographId
};
}
@@ -118,6 +116,7 @@ const PublishNoteSheet = ({
await monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
requestInAppReview();
@@ -144,6 +143,7 @@ const PublishNoteSheet = ({
await db.monographs.unpublish(note.id);
monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
} catch (e) {
@@ -381,9 +381,6 @@ const PublishNoteSheet = ({
}}
>
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
<Paragraph>
{monographData?.result?.analytics?.totalViews || 0}
</Paragraph>
</View>
</View>
</View>

View File

@@ -121,6 +121,17 @@ export const RelationsList = ({
<List
data={items}
loading={false}
groupType={
referenceType === "note"
? "notes"
: referenceType === "tag"
? "tags"
: referenceType === "notebook"
? "notebooks"
: referenceType === "reminder"
? "reminders"
: "notes"
}
dataType={referenceType as any}
isRenderedInActionSheet={true}
/>

View File

@@ -181,6 +181,7 @@ export default function ReminderNotify({
data={references}
loading={false}
dataType="note"
groupType="notes"
isRenderedInActionSheet={true}
/>
</View>

View File

@@ -45,25 +45,16 @@ import Paragraph from "../../ui/typography/paragraph";
const Sort = ({
type,
screen,
hideGroupOptions
hideGroupOptions,
group: groupType
}: {
type: ItemType;
screen?: RouteName;
group: GroupingKey;
hideGroupOptions?: boolean;
}) => {
const { colors } = useThemeColors();
const groupType =
screen === "Archive"
? "archive"
: screen === "Search"
? "search"
: screen === "Notes"
? "home"
: screen === "Trash" || type === "trash"
? "trash"
: ((type + "s") as GroupingKey);
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(groupType)
);
@@ -157,10 +148,15 @@ const Sort = ({
>
{Object.keys(SORT).map((item) => {
const sortOptionVisibility = {
relevance: screen === "Search",
dueDate: screen === "Reminders",
dateModified: screen === "Tags" || screen === "Reminders",
dateEdited: screen !== "Tags" && screen !== "Reminders"
dateCreated: groupType !== "trash",
relevance: groupType === "search",
dueDate: groupType === "reminders",
dateModified: groupType === "reminders" || groupType === "tags",
dateEdited:
groupType !== "tags" &&
groupType !== "reminders" &&
groupType !== "trash",
dateDeleted: groupType === "trash"
};
// Check if this sort option should be skipped for the current screen
@@ -188,10 +184,7 @@ const Sort = ({
onPress={async () => {
const _groupOptions: GroupOptions = {
...groupOptions,
sortBy:
type === "trash"
? "dateDeleted"
: (item as SortOptions["sortBy"])
sortBy: item as SortOptions["sortBy"]
};
await updateGroupOptions(_groupOptions);
}}

View File

@@ -79,6 +79,7 @@ type SimpleTabViewProps = {
const createSceneMap = (
scenes: Record<string, React.ComponentType<any>>
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
// eslint-disable-next-line react/display-name
return ({ route }: { route: SimpleRoute }) => {
const SceneComponent = scenes[route.key];
if (!SceneComponent) return null;
@@ -136,8 +137,8 @@ const SimpleTabView = ({
{loadedKeysRef.current.has(route.key)
? getSceneForRoute(route)
: navigationState.index === routeIndex
? getSceneForRoute(route)
: null}
? getSceneForRoute(route)
: null}
</View>
))}
</View>
@@ -432,12 +433,12 @@ const TabBar = (props: SimpleTabBarProps) => {
name="plus"
testID="sidebar-add-button"
size={AppFontSize.lg - 2}
top={10}
color={colors.primary.icon}
onPress={async () => {
if (props.navigationState.index === 1) {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
const notebooksFeature =
await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
PaywallSheet.present(notebooksFeature);
return;
@@ -485,6 +486,7 @@ const TabBar = (props: SimpleTabBarProps) => {
? "sort-ascending"
: "sort-descending"
}
top={10}
testID="sidebar-sort-button"
color={colors.primary.icon}
onPress={() => {
@@ -496,6 +498,11 @@ const TabBar = (props: SimpleTabBarProps) => {
? "notebook"
: "tag"
}
group={
props.navigationState.index === 1
? "notebooks"
: "tags"
}
hideGroupOptions
/>
)
@@ -520,6 +527,7 @@ const TabBar = (props: SimpleTabBarProps) => {
width: 28,
height: 28
}}
top={10}
testID="sidebar-theme-button"
color={colors.primary.icon}
name={isDark ? "weather-night" : "weather-sunny"}

View File

@@ -24,18 +24,22 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useTotalNotes } from "../../hooks/use-db-item";
import { db } from "../../common/database";
import { eSubscribeEvent } from "../../services/event-manager";
import {
eSubscribeEvent,
subscribeToItemUpdate
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import useNavigationStore, {
RouteParams
} from "../../stores/use-navigation-store";
import { eAfterSync } from "../../utils/events";
import { eAfterSync, eMenuItemUpdate } from "../../utils/events";
import { SideMenuItem } from "../../utils/menu-items";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useSideBarDraggingStore } from "./dragging-store";
import { useRelationStore } from "../../stores/use-relation-store";
export function MenuItem({
item,
@@ -56,13 +60,19 @@ export function MenuItem({
const totalNotes = useTotalNotes(
item.dataType as "notebook" | "tag" | "color"
);
const update = useRelationStore((state) => state.updater);
const getTotalNotesRef = useRef(totalNotes.getTotalNotes);
getTotalNotesRef.current = totalNotes.getTotalNotes;
const menuItemCount = !item.data
? itemCount
: totalNotes.totalNotes(item.data.id);
useEffect(() => {
if (item.data) {
getTotalNotesRef.current([item.data?.id]);
}
}, [item.data, update]);
useEffect(() => {
const onSyncComplete = async () => {
try {
@@ -84,7 +94,9 @@ export function MenuItem({
setItemCount(await db.notes.archived.count());
break;
case "Trash":
setItemCount((await db.trash.all()).length);
{
setItemCount((await db.trash.all()).length);
}
break;
}
} else {
@@ -94,10 +106,20 @@ export function MenuItem({
/** Empty */
}
};
const event = eSubscribeEvent(eAfterSync, onSyncComplete);
const events = [eSubscribeEvent(eAfterSync, onSyncComplete)];
if (!item.data) {
events.push(eSubscribeEvent(eMenuItemUpdate, onSyncComplete));
}
if (item.data?.id) {
events.push(
subscribeToItemUpdate(item?.data?.id, item?.data?.type, onSyncComplete)
);
}
onSyncComplete();
return () => {
event?.unsubscribe();
events?.forEach((e) => e?.unsubscribe());
};
}, [item.data, item.id]);
@@ -151,8 +173,8 @@ export function MenuItem({
item.icon === "crown"
? colors.static.yellow
: isFocused
? colors.selected.icon
: colors.secondary.icon
? colors.selected.icon
: colors.secondary.icon
}
size={AppFontSize.md}
/>

View File

@@ -24,7 +24,8 @@ import { StoreApi, UseBoundStore } from "zustand";
import { useTotalNotes } from "../../hooks/use-db-item";
import {
eSubscribeEvent,
eUnSubscribeEvent
eUnSubscribeEvent,
ToastManager
} from "../../services/event-manager";
import { TreeItem } from "../../stores/create-notebook-tree-stores";
import { SelectionStore } from "../../stores/item-selection-store";
@@ -35,6 +36,7 @@ import AppIcon from "../ui/AppIcon";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useRelationStore } from "../../stores/use-relation-store";
export const NotebookItem = ({
index,
@@ -70,13 +72,14 @@ export const NotebookItem = ({
const notebook = item.notebook;
const isFocused = focused;
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
const updater = useRelationStore(state => state.updater);
const getTotalNotesRef = React.useRef(getTotalNotes);
getTotalNotesRef.current = getTotalNotes;
const { colors } = useThemeColors();
useEffect(() => {
getTotalNotesRef.current([item.notebook.id]);
}, [item.notebook]);
}, [item.notebook, updater]);
useEffect(() => {
const onNotebookUpdate = (id?: string) => {
@@ -101,7 +104,8 @@ export const NotebookItem = ({
? 15 * item.depth
: 15 * 5,
width: "100%",
marginTop: 2
marginTop: 2,
opacity: item.disabled ? 0.5 : 1
}}
>
<Pressable

View File

@@ -76,7 +76,7 @@ export const PinnedSection = React.memo(
onPress: onPress,
onLongPress: onLongPress
})) as SideMenuItem[],
[menuPins, onPress]
[menuPins, onLongPress, onPress]
);
const renderItem = React.useCallback(({ item }: { item: SideMenuItem }) => {

View File

@@ -40,6 +40,7 @@ import {
useSideMenuNotebookTreeStore
} from "./stores";
import { LegendList } from "@legendapp/list";
import { useRelationStore } from "../../stores/use-relation-store";
useSideMenuNotebookSelectionStore.setState({
multiSelect: true
});
@@ -52,6 +53,7 @@ export const SideMenuNotebooks = () => {
const [filteredNotebooks, setFilteredNotebooks] = React.useState(notebooks);
const searchTimer = React.useRef<NodeJS.Timeout>(undefined);
const lastQuery = React.useRef<string>(undefined);
const updater = useRelationStore(state => state.updater);
const loadRootNotebooks = React.useCallback(async () => {
if (!filteredNotebooks) return;
const _notebooks: Notebook[] = [];
@@ -66,9 +68,6 @@ export const SideMenuNotebooks = () => {
const updateNotebooks = React.useCallback(() => {
if (lastQuery.current) {
// useSideMenuNotebookTreeStore.setState({
// isSearching: true
// });
db.lookup
.notebooks(lastQuery.current)
.sorted(db.settings.getGroupOptions("notebooks"))
@@ -76,16 +75,13 @@ export const SideMenuNotebooks = () => {
setFilteredNotebooks(filtered);
});
} else {
// useSideMenuNotebookTreeStore.setState({
// isSearching: false
// });
setFilteredNotebooks(notebooks);
}
}, [notebooks]);
useEffect(() => {
updateNotebooks();
}, [updateNotebooks]);
}, [updateNotebooks,updater]);
useEffect(() => {
(async () => {

View File

@@ -38,6 +38,7 @@ import { SideMenuHeader } from "./side-menu-header";
import { SideMenuListEmpty } from "./side-menu-list-empty";
import { useSideMenuTagsSelectionStore } from "./stores";
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
import { useRelationStore } from "../../stores/use-relation-store";
const TagItem = (props: {
tags: VirtualizedGrouping<Tag>;
@@ -55,12 +56,13 @@ const TagItem = (props: {
const totalNotes = useTotalNotes("tag");
const totalNotesRef = React.useRef(totalNotes);
totalNotesRef.current = totalNotes;
const updater = useRelationStore(state => state.updater);
useEffect(() => {
if (item?.id) {
totalNotesRef.current?.getTotalNotes([item?.id]);
}
}, [item]);
}, [item, updater]);
return (
<View

View File

@@ -38,10 +38,10 @@ import {
import { getElevationStyle } from "../../utils/elevation";
import { eHideToast, eShowToast } from "../../utils/events";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
export const Toast = ({ context = "global" }) => {
const { colors, isDark } = useThemeColors();
@@ -119,11 +119,7 @@ export const Toast = ({ context = "global" }) => {
width: DDS.isTab ? dimensions.width / 2 : "100%",
alignItems: "center",
alignSelf: "center",
bottom:
Platform.OS === "android"
? Math.max(insets.bottom, 40)
: Math.max(insets.bottom, 40) +
(keyboard.keyboardShown ? keyboard.keyboardHeight : 0),
bottom: insets.bottom + 15,
position: "absolute",
zIndex: 999,
elevation: 15
@@ -158,22 +154,22 @@ export const Toast = ({ context = "global" }) => {
toastOptions.icon
? toastOptions.icon
: toastOptions.type === "success"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
}
size={isFullToastMessage ? AppFontSize.xxxl : AppFontSize.xl}
color={
toastOptions?.icon
? toastOptions?.icon
: toastOptions.type === "error"
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
}
/>

View File

@@ -83,12 +83,13 @@ const SheetWrapper = ({
: 0
};
}, [
colors.primary.background,
colors.primary.border,
largeTablet,
smallTablet,
width,
insets.bottom
colors.primary.background,
colors.primary.border,
bottomInsets,
isGestureNavigationEnabled
]);
const _onOpen = () => {

View File

@@ -17,33 +17,14 @@ 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 { SubscriptionPlan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Linking, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import {
COMMUNITY_SVG,
LAUNCH_ROCKET,
SUPPORT_SVG,
WELCOME_SVG
} from "../../assets/images/assets";
import useRotator from "../../hooks/use-rotator";
import { eSendEvent } from "../../services/event-manager";
import { getContainerBorder } from "../../utils/colors";
import { getElevationStyle } from "../../utils/elevation";
import { eOpenAddNotebookDialog } from "../../utils/events";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { WELCOME_SVG } from "../../assets/images/assets";
import { useUserStore } from "../../stores/use-user-store";
import { planToId, SubscriptionPlan } from "@notesnook/core";
import { planToDisplayName } from "../../utils/constants";
import AppIcon from "../ui/AppIcon";
import { SvgView } from "../ui/svg";
export type TStep = {
text?: string;

View File

@@ -33,7 +33,7 @@ import { useThemeColors } from "@notesnook/theme";
import { DisplayedNotification } from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { InteractionManager, Platform, View } from "react-native";
import { InteractionManager, Platform } from "react-native";
import Share from "react-native-share";
import { DatabaseLogger, db } from "../common/database";
import { AttachmentDialog } from "../components/attachments";
@@ -72,8 +72,8 @@ import { useUserStore } from "../stores/use-user-store";
import { eCloseSheet, eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import DatePickerComponent from "../components/date-picker";
@@ -196,7 +196,7 @@ export const useActions = ({
"expiringNotes"
]);
const [item, setItem] = useState(propItem);
const { colors, isDark } = useThemeColors();
const { colors } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(item.id)
@@ -382,6 +382,16 @@ export const useActions = ({
}
const deleteItem = async () => {
if (isPublished) {
ToastManager.show({
heading: strings.notePublished(),
message: strings.unpublishToDelete(),
type: "error",
context: "local"
});
return;
}
close();
await sleep(300);
@@ -1237,7 +1247,8 @@ export const useActions = ({
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",
onPress: deleteItem
onPress: deleteItem,
locked: isPublished
});
}
@@ -1275,7 +1286,10 @@ export const useActions = ({
(item as Note).headline || (item as Notebook).description || "",
(item as Color).colorCode
);
} catch (e) {}
} catch (e) {
/**
empty */
}
}
});
}

View File

@@ -30,6 +30,7 @@ import {
import { strings } from "@notesnook/intl";
import notifee from "@notifee/react-native";
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
import dayjs from "dayjs";
import React, { useCallback, useEffect, useRef } from "react";
import {
AppState,
@@ -48,6 +49,7 @@ import * as RNIap from "react-native-iap";
import { DatabaseLogger, db, setupDatabase } from "../common/database";
import { initializeLogger } from "../common/database/logger";
import { MMKV } from "../common/database/mmkv";
import { deleteDCacheFiles } from "../common/filesystem/io";
import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
@@ -58,11 +60,7 @@ import {
resetTabStore,
useTabStore
} from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
editorState
} from "../screens/editor/tiptap/utils";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
import BiometricService from "../services/biometrics";
@@ -89,6 +87,7 @@ import { clearAllStores, initAfterSync } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useMessageStore } from "../stores/use-message-store";
import { useRelationStore } from "../stores/use-relation-store";
import { useSettingStore } from "../stores/use-setting-store";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
@@ -105,12 +104,8 @@ import {
} from "../utils/events";
import { getGithubVersion } from "../utils/github-version";
import { fluidTabsRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import useFeatureManager from "./use-feature-manager";
import { deleteDCacheFiles } from "../common/filesystem/io";
import dayjs from "dayjs";
import { useRelationStore } from "../stores/use-relation-store";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -173,7 +168,6 @@ const onAppOpenedFromURL = async (event: {
if (url.startsWith("https://app.notesnook.com/account/verified")) {
await onUserEmailVerified();
} else if (url.startsWith("ShareMedia://QuickNoteWidget")) {
clearAppState();
editorState().movedAway = false;
eSendEvent(eOnLoadNote, { newNote: true });
fluidTabsRef.current?.goToPage("editor", false);
@@ -358,23 +352,9 @@ async function checkForShareExtensionLaunchedInBackground() {
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
} catch (e) {}
}
async function saveEditorState() {
if (!editorState().movedAway) {
const id = useTabStore.getState().getCurrentNoteId();
const note = id ? await db.notes.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note));
if (locked) return;
const state = JSON.stringify({
editing: editorState().currentlyEditing,
movedAway: editorState().movedAway,
timestamp: Date.now()
});
NotesnookModule.setAppState(state);
} else {
NotesnookModule.setAppState("");
} catch (e) {
/**
empty */
}
}
@@ -592,13 +572,13 @@ export const useAppEvents = () => {
}, [isAppLoading, onSyncComplete]);
useEffect(() => {
if (initialUrl) {
if (initialUrl && !isAppLoading) {
onAppOpenedFromURL({
url: initialUrl!,
isInitialUrl: true
});
}
}, [initialUrl]);
}, [initialUrl, isAppLoading]);
const subscribeToPurchaseListeners = useCallback(async () => {
if (Platform.OS === "android") {
@@ -838,7 +818,6 @@ export const useAppEvents = () => {
Sync.run("global", false, "full");
reconnectSSE();
await checkForShareExtensionLaunchedInBackground();
NotesnookModule.setAppState("");
let user = await db.user.getUser();
if (user && !user?.isEmailConfirmed) {
try {
@@ -861,7 +840,6 @@ export const useAppEvents = () => {
eSendEvent(eEditorReset);
}
} else {
await saveEditorState();
if (
SettingsService.canLockAppInBackground() &&
!useSettingStore.getState().requestBiometrics &&

View File

@@ -135,7 +135,7 @@ export const useNoteLocked = (noteId: string | undefined) => {
//@ts-ignore
!globalThis["IS_SHARE_EXTENSION"]
) {
let unsub = useSettingStore.subscribe((state) => {
const unsub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
unsub();
db.vaults

View File

@@ -1,15 +1,28 @@
/*
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 { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useEffect } from "react";
import { db } from "../common/database";
import { presentDialog } from "../components/dialog/functions";
import { useDragState } from "../screens/settings/editor/state";
import { eSendEvent } from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import SettingsService from "../services/settings";
import { useUserStore } from "../stores/use-user-store";
import { eCloseSimpleDialog } from "../utils/events";
export default function useFeatureManager() {
const features = useAreFeaturesAvailable([
@@ -24,11 +37,10 @@ export default function useFeatureManager() {
"disableTrashCleanup",
"fullOfflineMode"
]);
const user = useUserStore((state) => state.user);
const plan = useUserStore((state) => state.user?.subscription?.plan);
useEffect(() => {
if (!user || !features) return;
if (!useUserStore.getState().user || !features) return;
if (!features.createNoteFromNotificationDrawer.isAllowed) {
SettingsService.setProperty("notifNotes", false);
@@ -69,6 +81,7 @@ export default function useFeatureManager() {
db.settings.setDefaultTag(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [features, plan]);
return true;

View File

@@ -16,16 +16,17 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Plan, SubscriptionPlan, SubscriptionPlanId } from "@notesnook/core";
import { useEffect, useState } from "react";
import { Plan, SubscriptionPlan } from "@notesnook/core";
import React, { useEffect, useState } from "react";
import { useAsync } from "react-async-hook";
import { Platform } from "react-native";
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import { DatabaseLogger, db } from "../common/database";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import { useSettingStore } from "../stores/use-setting-store";
import { useUserStore } from "../stores/use-user-store";
import SettingsService from "../services/settings";
function numberWithCommas(x: string) {
const parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -155,9 +156,18 @@ const planIdToIndex = (planId: string) => {
return planIndex;
};
let WebPlanCache: Plan[];
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const usePricingPlans = (options?: PricingPlansOptions) => {
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const user = useUserStore((state) => state.user);
const regionalDiscount = useAsync(
() =>
db.pricing.sku(
Platform.OS === "android" ? "google" : "apple",
"yearly",
"pro"
),
[]
);
const [currentPlan, setCurrentPlan] = useState<string>(
options?.planId || pricingPlans[2].id
);
@@ -172,17 +182,20 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const [userCanRequestTrial, setUserCanRequestTrial] = useState(false);
const [webPricingPlans, setWebPricingPlans] = useState<Plan[]>([]);
const getProduct = (planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
const getProduct = React.useCallback(
(planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
return (
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
};
return (
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
},
[plans, webPricingPlans]
);
const getProductAndroid = (planId: string, skuId: string) => {
if (isGithubRelease)
@@ -196,37 +209,45 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
return getProduct(planId, skuId) as RNIap.SubscriptionIOS;
};
const hasTrialOffer = (planId?: string, productId?: string) => {
if (!selectedProductSku && !productId) return false;
const hasTrialOffer = React.useCallback(
(planId?: string, productId?: string) => {
if (!selectedProductSku && !productId) return false;
if (productId?.includes("5year")) return false;
if (isGithubRelease) {
if (
user?.subscription?.trialsAvailed?.some(
(plan) => plan === planIdToIndex(planId || currentPlan)
)
) {
return false;
} else {
return true;
if (productId?.includes("5year")) return false;
if (isGithubRelease) {
if (
user?.subscription?.trialsAvailed?.some(
(plan) => plan === planIdToIndex(planId || currentPlan)
)
) {
return false;
} else {
return true;
}
}
}
return Platform.OS === "ios"
? (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionIOS
)?.introductoryPricePaymentModeIOS === "FREETRIAL"
: (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
?.length > 1;
};
return Platform.OS === "ios"
? (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionIOS
)?.introductoryPricePaymentModeIOS === "FREETRIAL"
: (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
?.length > 1;
},
[
currentPlan,
getProduct,
selectedProductSku,
user?.subscription?.trialsAvailed
]
);
// user && (!user.subscription || !user.subscription.expiry) ? true : false;
@@ -255,12 +276,15 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const products = WebPlanCache || (await db.pricing.products());
WebPlanCache = products;
setWebPricingPlans(products);
} catch (e) {}
} catch (e) {
/**
empty */
}
}
setLoadingPlans(false);
};
loadPlans();
}, [options?.promoOffer, cancelPromo]);
}, [options?.promoOffer, cancelPromo, hasTrialOffer]);
function getLocalizedPrice(
product: RNIap.Subscription | RNIap.Product | Plan
@@ -276,8 +300,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? (product as Plan).price.gross
: (product as Plan).period === "5-year"
? (product as Plan).price.gross
: (product as Plan).price.gross
? (product as Plan).price.gross
: (product as Plan).price.gross
}`;
}
@@ -432,8 +456,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
return period.endsWith("W")
? "week"
: period.endsWith("M")
? "month"
: "year";
? "month"
: "year";
} else {
const unit = (product as RNIap.SubscriptionIOS)
?.subscriptionPeriodUnitIOS;
@@ -475,8 +499,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
type: phase.billingPeriod.endsWith("W")
? "week"
: phase.billingPeriod.endsWith("M")
? "month"
: "year"
? "month"
: "year"
};
} else {
const productIos = product as RNIap.SubscriptionIOS;
@@ -530,14 +554,15 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const formattedPrice = numberWithCommas(monthlyPrice.toFixed(2));
return isAtLeft
? `${symbol} ${formattedPrice}`
: `${formattedPrice} ${symbol}`;
? `${symbol}${formattedPrice}`
: `${formattedPrice}${symbol}`;
};
const getDiscountValue = (p1: string, p2: string, splitToMonth?: boolean) => {
let price1 = Platform.OS === "ios" ? parseInt(p1) : parseInt(p1) / 1000000;
let price1 =
Platform.OS === "ios" ? parseFloat(p1) : parseFloat(p1) / 1000000;
const price2 =
Platform.OS === "ios" ? parseInt(p2) : parseInt(p2) / 1000000;
Platform.OS === "ios" ? parseFloat(p2) : parseFloat(p2) / 1000000;
price1 = splitToMonth ? price1 / 12 : price1;
@@ -587,7 +612,7 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
} else {
priceValue = price / 1000000;
}
const priceSymbol = localizedPrice.replace(/[\s\d,.]+/, "");
const priceSymbol = localizedPrice.replace(/[\d,.]+/, "");
return { priceValue, priceSymbol, localizedPrice };
};
@@ -607,8 +632,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? ((product as Plan).price.gross / 12).toFixed(2)
: (product as Plan).period === "5-year"
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
}`;
}
@@ -645,21 +670,6 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
);
};
async function getRegionalDiscount(plan: string, productId: string) {
if (productId !== "notesnook.pro.yearly") {
return;
}
try {
return await db.pricing.sku(
Platform.OS === "android" ? "google" : "apple",
"yearly",
plan as SubscriptionPlanId
);
} catch (e) {
console.log(e);
}
}
function isSubscribedToPlan(planId: string) {
if (!PremiumService.get()) return false;
return user?.subscription?.productId?.includes(planId);
@@ -730,7 +740,7 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(plan) => plan.plan === planIndex && plan.period === period
);
},
getRegionalDiscount,
regionalDiscount: regionalDiscount.result,
isGithubRelease: isGithubRelease,
isSubscribed: () => user?.subscription?.plan !== SubscriptionPlan.FREE,
finish: () => options?.onBuy?.()

View File

@@ -21,7 +21,6 @@ import { useEffect } from "react";
import { NativeEventEmitter, NativeModule } from "react-native";
import { useRef } from "react";
import { Platform } from "react-native";
import { Linking } from "react-native";
import deviceInfoModule from "react-native-device-info";
import { strings } from "@notesnook/intl";
const ShortcutsEmitter = new NativeEventEmitter(
@@ -54,14 +53,9 @@ export const useShortcutManager = ({
}, [shortcuts]);
useEffect(() => {
Linking.getInitialURL().then((url) => {
if (url?.startsWith("ShareMedia://QuickNoteWidget")) {
onShortcutPressed(defaultShortcuts[0]);
}
});
if (!isSupported()) return;
Shortcuts.getInitialShortcut().then((shortcut) => {
if (initialShortcutRecieved.current) return;
if (initialShortcutRecieved.current || !shortcut) return;
onShortcutPressed(shortcut);
initialShortcutRecieved.current = true;
});

View File

@@ -47,12 +47,7 @@ import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { useShortcutManager } from "../hooks/use-shortcut-manager";
import { hideAllTooltips } from "../hooks/use-tooltip";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
editorState,
getAppState
} from "../screens/editor/tiptap/utils";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
import { DDS } from "../services/device-detection";
import {
eSendEvent,
@@ -118,13 +113,9 @@ export const FluidPanelsView = React.memo(
useShortcutManager({
onShortcutPressed: async (item) => {
if (!item && getAppState()) {
editorState().movedAway = false;
fluidTabsRef.current?.goToPage("editor", false);
return;
}
if (!item) return;
if (item?.type === "notesnook.action.newnote") {
clearAppState();
if (!fluidTabsRef.current) {
setTimeout(() => {
eSendEvent(eOnLoadNote, { newNote: true });
@@ -148,7 +139,7 @@ export const FluidPanelsView = React.memo(
if (deviceMode === "smallTablet") {
fluidTabsRef.current?.openDrawer(false);
}
}, [deviceMode, dimensions.width, setFullscreen]);
}, [deviceMode, setFullscreen]);
const closeFullScreenEditor = useCallback(
(current: string) => {
@@ -167,7 +158,7 @@ export const FluidPanelsView = React.memo(
fluidTabsRef.current?.goToIndex(2, false);
}
},
[deviceMode, dimensions.width, setFullscreen]
[deviceMode, setFullscreen]
);
const toggleView = useCallback(
@@ -205,7 +196,6 @@ export const FluidPanelsView = React.memo(
eSendEvent(eCloseFullscreenEditor, current);
}
const state = getAppState();
setTimeout(() => {
switch (current) {
case "tablet":
@@ -217,23 +207,15 @@ export const FluidPanelsView = React.memo(
}
break;
case "mobile":
if (
state &&
editorState().movedAway === false &&
useTabStore.getState().getCurrentNoteId()
) {
fluidTabsRef.current?.goToPage("editor", false);
} else {
fluidTabsRef.current?.goToPage(
fluidTabsRef.current?.page(),
false
);
}
fluidTabsRef.current?.goToPage(
fluidTabsRef.current?.page(),
false
);
break;
}
}, 0);
},
[deviceMode, fullscreen, setDeviceModeState]
[fullscreen, setDeviceModeState]
);
const checkDeviceType = React.useCallback(
@@ -243,18 +225,18 @@ export const FluidPanelsView = React.memo(
const nextDeviceMode = DDS.isLargeTablet()
? "tablet"
: DDS.isSmallTab
? "smallTablet"
: "mobile";
? "smallTablet"
: "mobile";
setDeviceMode(nextDeviceMode, size);
},
[orientation, setDeviceMode, setDimensions]
[orientation, setDeviceMode]
);
useEffect(() => {
if (orientation !== "UNKNOWN") {
checkDeviceType(dimensions);
}
}, [orientation, dimensions]);
}, [orientation, dimensions, checkDeviceType]);
const _onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
@@ -269,13 +251,7 @@ export const FluidPanelsView = React.memo(
setOrientation(OrientationType["PORTRAIT"]);
}
},
[
checkDeviceType,
deviceMode,
dimensions.width,
orientation,
setDeviceMode
]
[setDimensions]
);
const PANE_OFFSET = useMemo(

View File

@@ -16,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

View File

@@ -400,17 +400,45 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
recurringMode === RecurringModes.Year
? null
: recurringMode === RecurringModes.Week
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(item) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
return [...days];
}
days.push(item);
return [...days];
});
}}
/>
)
)
: MonthDays.map((item, index) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
key={index + "monthday"}
title={index + 1 + ""}
type={
selectedDays.indexOf(item) > -1
selectedDays.indexOf(index + 1) > -1
? "selected"
: "plain"
}
@@ -422,44 +450,16 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
return [...days];
}
days.push(item);
days.push(index + 1);
return [...days];
});
}}
/>
)
)
: MonthDays.map((item, index) => (
<Button
key={index + "monthday"}
title={index + 1 + ""}
type={
selectedDays.indexOf(index + 1) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
return [...days];
}
days.push(index + 1);
return [...days];
});
}}
/>
))}
))}
</ScrollView>
</View>
) : null}
@@ -542,24 +542,24 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
dayjs(date).format("hh:mm A")
)
: recurringMode === RecurringModes.Year
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
</Paragraph>
</>
</View>
@@ -587,8 +587,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
mode === "Silent"
? "minus-circle"
: mode === "Vibrate"
? "vibrate"
: "volume-high"
? "vibrate"
: "volume-high"
}
fontSize={AppFontSize.xs}
height={35}
@@ -636,6 +636,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
<Heading size={AppFontSize.md}>{strings.referencedIn()}</Heading>
{referencedNotes.value.map((item) => (
<Pressable
key={item.id}
style={{
justifyContent: "space-between",
flexDirection: "row",

View File

@@ -70,6 +70,7 @@ export const Archive = ({ navigation, route }: NavigationProps<"Archive">) => {
onRefresh={() => {
refresh();
}}
groupType="archive"
renderedInRoute="Archive"
loading={loading}
placeholder={{

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium";
import { isFeatureAvailable } from "@notesnook/common";
import { isImage } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
DocumentPickerOptions,
@@ -34,6 +33,7 @@ import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress";
import { santizeUri } from "../../../common/filesystem/utils";
import AttachImage from "../../../components/dialogs/attach-image-dialog";
import { ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
@@ -41,7 +41,6 @@ import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { santizeUri } from "../../../common/filesystem/utils";
type PickerOptions = {
noteId?: string;
@@ -90,6 +89,7 @@ const file = async (fileOptions: PickerOptions) => {
message: featureResult.error,
type: "error"
});
return;
}
if (fileCopyUri.status === "error") {
@@ -102,7 +102,7 @@ const file = async (fileOptions: PickerOptions) => {
return;
}
let uri = santizeUri(fileCopyUri.localUri);
const uri = santizeUri(fileCopyUri.localUri);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"
@@ -272,6 +272,7 @@ const handleImageResponse = async (
message: featureResult.error,
type: "error"
});
return;
}
const b64 = `data:${image.mime};base64, ` + image.data;

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { ToolbarGroupDefinition } from "@notesnook/editor";
import { useEditor } from "./use-editor";
import { FeatureId, FeatureResult } from "@notesnook/common";
import { DayFormat } from "@notesnook/core";
export type useEditorType = ReturnType<typeof useEditor>;

View File

@@ -207,7 +207,7 @@ export const useEditorEvents = (
return () => {
subscriptions.forEach((subscription) => subscription.remove());
};
}, [editor.commands, editor.postMessage]);
}, [editor, editor.commands, editor.postMessage]);
useEffect(() => {
if (loading) return;
if (typeof defaultFontFamily === "object") {
@@ -471,9 +471,8 @@ export const useEditorEvents = (
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),
onAdd: async () => {
const reminderFeature = await isFeatureAvailable(
"activeReminders"
);
const reminderFeature =
await isFeatureAvailable("activeReminders");
if (!reminderFeature.isAllowed) {
ToastManager.show({
type: "info",
@@ -560,7 +559,7 @@ export const useEditorEvents = (
.then((data: any) => {
console.log(
"Got attachment data:",
data,
!!data,
editorMessage.resolverId
);
editor.postMessage(NativeEvents.resolve, {
@@ -765,7 +764,7 @@ export const useEditorEvents = (
let filePath: string;
let fileName: string;
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
const file = await ScopedStorage.createDocument(
"table.csv",
"text/csv",
csv,

View File

@@ -38,15 +38,14 @@ import { strings } from "@notesnook/intl";
import { useThemeEngineStore } from "@notesnook/theme";
import { Mutex } from "async-mutex";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Linking } from "react-native";
import WebView from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
eSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { NotePreviewWidget } from "../../../services/note-preview-widget";
@@ -69,15 +68,7 @@ import Commands from "./commands";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
import {
clearAppState,
defaultState,
getAppState,
isContentInvalid,
isEditorLoaded,
post
} from "./utils";
import { Linking } from "react-native";
import { defaultState, isContentInvalid, isEditorLoaded, post } from "./utils";
const loadNoteMutex = new Mutex();
@@ -167,7 +158,7 @@ export const useEditor = (
const tags = useTagStore((state) => state.items);
const insets = useGlobalSafeAreaInsets();
const isDefaultEditor = editorId === "";
const saveCount = useRef(0);
const saveCount = useRef<Record<string, number>>({});
const lastContentChangeTime = useRef<Record<string, number>>({});
const lock = useRef(false);
const currentLoadingNoteId = useRef<string>(undefined);
@@ -253,9 +244,9 @@ export const useEditor = (
clearTimeout(timers.current["loading-images" + noteId]);
}
saveCount.current = 0;
currentLoadingNoteId.current = undefined;
lock.current = false;
saveCount.current[tabId] = 0;
resetContent && postMessage(NativeEvents.title, "", tabId);
resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags(tabId));
@@ -388,7 +379,7 @@ export const useEditor = (
if (
oldNote?.title !== currentNotes.current[id]?.title &&
!noteData.title
typeof noteData.title === "undefined"
) {
postMessage(
NativeEvents.title,
@@ -436,10 +427,9 @@ export const useEditor = (
lastContentChangeTime.current[id] = note.dateEdited;
if (
saveCount.current < 2 ||
saveCount.current[tabId] < 2 ||
currentNotes.current[id]?.title !== note.title ||
currentNotes.current[id]?.headline?.slice(0, 200) !==
note.headline?.slice(0, 200)
currentNotes.current[id]?.headline !== note.headline
) {
Navigation.queueRoutesForUpdate();
}
@@ -455,7 +445,11 @@ export const useEditor = (
currentNotes.current[id] = note;
}
saveCount.current++;
if (!saveCount.current[tabId]) {
saveCount.current[tabId] = 1;
} else {
saveCount.current[tabId]++;
}
clearTimeout(timers.current.onsave);
timers.current.onsave = setTimeout(async () => {
@@ -564,6 +558,7 @@ export const useEditor = (
useTabStore.getState().newTabSession(tabId, {});
}
}
saveCount.current[tabId] = 0;
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
@@ -696,6 +691,7 @@ export const useEditor = (
currentNotes.current[item.id] = item;
editorSessionHistory.newSession(item.id);
saveCount.current[tabId] = 0;
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
@@ -1025,22 +1021,11 @@ export const useEditor = (
);
const restoreEditorState = useCallback(async () => {
const appState = getAppState();
if (!appState) return;
state.current.isRestoringState = true;
state.current.currentlyEditing = true;
if (fluidTabsRef.current?.page() === "editor") {
state.current.movedAway = false;
}
if (!state.current.editorStateRestored) {
state.current.isRestoringState = true;
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor", false);
}
}
clearAppState();
state.current.isRestoringState = false;
}, []);
const onContentChanged = (noteId?: string) => {

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { parseInternalLink } from "@notesnook/core";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { createRef, MutableRefObject, RefObject } from "react";
import { TextInput } from "react-native";
import WebView from "react-native-webview";
@@ -28,10 +29,8 @@ import {
eUnSubscribeEvent
} from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { AppState, EditorState, useEditorType } from "./types";
import { EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
export const textInput = createRef<TextInput>();
export const editorController =
@@ -141,34 +140,6 @@ export function isContentInvalid(content: string | undefined) {
);
}
const canRestoreAppState = (appState: AppState) => {
return appState.editing && Date.now() < appState.timestamp + 3600000;
};
let appState: AppState | undefined;
export function setAppState(state: AppState) {
appState = state;
}
export function getAppState() {
if (appState && canRestoreAppState(appState)) return appState as AppState;
const json = NotesnookModule.getAppState();
if (json) {
appState = JSON.parse(json) as AppState;
if (canRestoreAppState(appState)) {
return appState;
} else {
clearAppState();
return null;
}
}
return null;
}
export function clearAppState() {
appState = undefined;
NotesnookModule.setAppState("");
}
export async function openInternalLink(url: string) {
const data = parseInternalLink(url);
if (!data?.id) return false;

View File

@@ -70,6 +70,7 @@ export const Favorites = ({
<List
data={favorites}
dataType="note"
groupType="favorites"
onRefresh={() => {
refresh();
}}

View File

@@ -70,6 +70,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
<List
data={notes}
dataType="note"
groupType="home"
renderedInRoute={route.name}
loading={loading || !isFocused}
headerTitle={strings.routes[route.name]()}

View File

@@ -45,7 +45,10 @@ import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { Notice } from "../../components/ui/notice";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { isFeatureAvailable } from "@notesnook/common";
import { ToastManager } from "../../services/event-manager";
import {
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import PaywallSheet from "../../components/sheets/paywall";
const {
@@ -175,6 +178,7 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
updateNotebook(item.id);
}
}
sendItemUpdateEvent(id, "notebook");
}
Navigation.queueRoutesForUpdate();
@@ -237,9 +241,8 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
button={{
icon: "plus",
onPress: async () => {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
const notebooksFeature =
await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,
@@ -388,8 +391,8 @@ const NotebookItemWrapper = React.memo(
!selected
? "selected"
: !state.initialState[item.notebook.id]
? undefined
: "deselected"
? undefined
: "deselected"
);
}}
canDisableSelectionMode={false}

View File

@@ -33,7 +33,10 @@ import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { ToastManager } from "../../services/event-manager";
import {
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import {
ItemSelection,
@@ -80,7 +83,10 @@ const useTagItemSelection = createItemSelectionStore(true);
const ManageTags = (props: NavigationProps<"ManageTags">) => {
const { colors } = useThemeColors();
const ids = props.route.params.ids || [];
const ids = React.useMemo(
() => props.route.params.ids || [],
[props.route.params.ids]
);
const [tags, setTags] = useState<Tag[]>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
@@ -121,7 +127,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
tags.splice(0, 0, ...noteTags);
setTags(tags);
},
[]
[ids]
);
const refreshTags = useCallback(() => {
@@ -135,7 +141,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
.sorted(db.settings.getGroupOptions("tags"))
.then(sortAndSetTags);
}
}, [query]);
}, [query, sortAndSetTags]);
useEffect(() => {
refreshTags();
@@ -199,6 +205,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
}
useRelationStore.getState().update();
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
setQuery(undefined);
} catch (e) {
@@ -242,6 +249,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
console.error(e);
}
}
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
useRelationStore.getState().update();
refreshTags();
@@ -417,15 +425,15 @@ const TagItem = ({
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
)}

View File

@@ -46,6 +46,7 @@ import {
import {
checkParentSelected,
findRootNotebookId,
getAllNotebookChildren,
getParentNotebookId
} from "../../utils/notebooks";
import { DefaultAppStyles } from "../../utils/styles";
@@ -124,17 +125,25 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
useEffect(() => {
async function filterNotebooks() {
const excludedItems: string[] = [];
const disabledItems: string[] = [];
for (const notebook of selectedNotebooks) {
const children = await getAllNotebookChildren(notebook.id, []);
excludedItems.push(...children);
const parent = await getParentNotebookId(notebook.id);
if (!excludedItems.includes(parent)) {
excludedItems.push(parent);
if (!disabledItems.includes(parent)) {
disabledItems.push(parent);
}
excludedItems.push(notebook.id);
}
const filtered = tree.filter(
(treeItem) => !excludedItems.includes(treeItem.notebook.id)
// Exclude and disable items as needed
const filtered = tree.filter(item => !excludedItems.includes(item.notebook.id)).map(
(treeItem) => {
return {
...treeItem,
disabled: disabledItems.includes(treeItem.notebook.id)
}
}
);
return filtered;
}
filterNotebooks().then((filtered) => {
@@ -149,6 +158,13 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
index={index}
item={item}
onPress={async () => {
if (item.disabled) {
ToastManager.show({
type: "info",
"message": "You cannot move the selected notebook(s) here"
})
return;
}
const selectedNotebook = item.notebook;
presentDialog({
title: strings.moveNotebooks(selectedNotebooks.length),
@@ -239,6 +255,7 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
updateNotebooks();
}, 300);
}}
testID="move-notebook-search"
button={{
icon: "plus",
onPress: async () => {

View File

@@ -39,6 +39,7 @@ import { createItemSelectionStore } from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sendItemUpdateEvent } from "../../services/event-manager";
const useItemSelectionStore = createItemSelectionStore(true);
export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
@@ -198,6 +199,7 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
);
updateNotebook(currentNotebook.id);
sendItemUpdateEvent(currentNotebook.id, "notebook");
Navigation.queueRoutesForUpdate();
Navigation.goBack();
}}
@@ -281,8 +283,8 @@ const SelectableNoteItem = React.memo(
selected
? colors.selected.icon
: exists && !selected
? colors.static.red
: colors.primary.icon
? colors.static.red
: colors.primary.icon
}
/>
</>

View File

@@ -185,6 +185,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
<List
data={notes}
dataType="note"
groupType="notes"
onRefresh={() => {
onRequestUpdate();
}}
@@ -202,7 +203,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
) : undefined
}
placeholder={{
title: notebook?.title!,
title: notebook?.title || "",
paragraph: strings.notesEmpty(),
button: strings.addFirstNote(),
action: openEditor,

View File

@@ -214,6 +214,7 @@ const NotesPage = ({
<List
data={notes}
dataType="note"
groupType="notes"
onRefresh={onRequestUpdate}
loading={false}
renderedInRoute={route.name}

View File

@@ -90,6 +90,7 @@ export const Reminders = ({
<List
data={reminders}
dataType="reminder"
groupType="reminders"
headerTitle={strings.routes[route.name]()}
renderedInRoute="Reminders"
loading={loading}
@@ -98,9 +99,8 @@ export const Reminders = ({
paragraph: strings.remindersEmpty(),
button: strings.setReminder(),
action: async () => {
const reminderFeature = await isFeatureAvailable(
"activeReminders"
);
const reminderFeature =
await isFeatureAvailable("activeReminders");
if (!reminderFeature.isAllowed) {
ToastManager.show({
type: "info",

View File

@@ -154,6 +154,7 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
data={results}
dataType={route.params?.type}
renderedInRoute={route.name}
groupType="search"
loading={loading}
placeholder={{
title: route.name,

View File

@@ -17,23 +17,22 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { isFeatureAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { StyleSheet, View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax";
import Animated, { FadeInDown, FadeOutDown } from "react-native-reanimated";
import PaywallSheet from "../../../components/sheets/paywall";
import { Button } from "../../../components/ui/button";
import { Notice } from "../../../components/ui/notice";
import Paragraph from "../../../components/ui/typography/paragraph";
import PremiumService from "../../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import { ToastManager } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { Group } from "./group";
import { DragState, useDragState } from "./state";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { isFeatureAvailable } from "@notesnook/common";
import { ToastManager } from "../../../services/event-manager";
import PaywallSheet from "../../../components/sheets/paywall";
export const ConfigureToolbar = () => {
const data = useDragState((state) => state.data);
const preset = useDragState((state) => state.preset);
@@ -41,7 +40,7 @@ export const ConfigureToolbar = () => {
const renderGroups = () => {
return data?.map((item, index) => (
<Group key={`group-${index}`} item={item} index={index} />
<Group key={`group-${index}-${item.length}`} item={item} index={index} />
));
};
@@ -131,6 +130,7 @@ export const ConfigureToolbar = () => {
style={{
flex: 1
}}
key={preset}
scrollEventThrottle={13}
showsVerticalScrollIndicator={false}
>

View File

@@ -31,7 +31,7 @@ import { renderTool } from "./common";
import { DraggableItem, useDragState } from "./state";
import ToolSheet from "./tool-sheet";
import { useIsFeatureAvailable } from "@notesnook/common";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import type { ToolId } from "@notesnook/editor";
import { strings } from "@notesnook/intl";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -64,78 +64,87 @@ export const Group = ({
const { colors } = useThemeColors();
const featureAvailable = useIsFeatureAvailable("customToolbarPreset");
const onDrop = (data: DraxDragWithReceiverEventData) => {
if (!featureAvailable?.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
});
return;
}
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
const dragged = data.dragged.payload;
const reciever = data.receiver.payload;
const _data = useDragState.getState().data.slice();
if (dragged.type === "group") {
const fromIndex = dragged.index;
const toIndex = isDroppedAbove
? Math.max(0, reciever.index)
: reciever.index + 1;
_data.splice(
toIndex > fromIndex ? toIndex - 1 : toIndex,
0,
_data.splice(fromIndex, 1)[0]
);
}
// Always insert sub group at the end of the group.
if (dragged.type === "subgroup") {
const fromIndex = dragged.index;
const insertAt = _data[reciever.index] as string[];
const insertFrom = _data[dragged.groupIndex] as string[];
if (typeof insertAt[insertAt.length - 1] !== "string") {
setRecieving(false);
return data.dragAbsolutePosition;
const onDrop = React.useCallback(
(data: DraxDragWithReceiverEventData) => {
if (!featureAvailable?.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error || strings.featureNotAvailable()
});
return;
}
insertAt.push(insertFrom.splice(fromIndex, 1)[0]);
}
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
const dragged = data.dragged.payload;
const reciever = data.receiver.payload;
const _data = useDragState.getState().data.slice();
if (dragged.type === "tool") {
const insertFrom =
typeof dragged.parentIndex === "number"
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
: (_data[dragged.groupIndex] as string[]);
_data[groupIndex].push(insertFrom.splice(dragged.index, 1)[0] as ToolId);
}
if (dragged.type === "group") {
const fromIndex = dragged.index;
const toIndex = isDroppedAbove
? Math.max(0, reciever.index)
: reciever.index + 1;
setData(_data);
setRecieving(false);
return data.dragAbsolutePosition;
};
_data.splice(
toIndex > fromIndex ? toIndex - 1 : toIndex,
0,
_data.splice(fromIndex, 1)[0]
);
}
const onRecieveData = (data: DraxDragWithReceiverEventData) => {
setRecieving(true);
if (data.dragged.payload.type !== "group")
return setRecievePosition("below");
if (data.receiver.receiveOffsetRatio.y < 0.5) {
setRecievePosition("above");
} else {
setRecievePosition("below");
}
};
// Always insert sub group at the end of the group.
if (dragged.type === "subgroup") {
const fromIndex = dragged.index;
const insertAt = _data[reciever.index] as string[];
const insertFrom = _data[dragged.groupIndex] as string[];
if (typeof insertAt[insertAt.length - 1] !== "string") {
setRecieving(false);
return data.dragAbsolutePosition;
}
insertAt.push(insertFrom.splice(fromIndex, 1)[0]);
}
if (dragged.type === "tool") {
const insertFrom =
typeof dragged.parentIndex === "number"
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
: (_data[dragged.groupIndex] as string[]);
_data[groupIndex].push(
insertFrom.splice(dragged.index, 1)[0] as ToolId
);
}
setData(_data);
setRecieving(false);
return data.dragAbsolutePosition;
},
[featureAvailable]
);
const onRecieveData = React.useCallback(
(data: DraxDragWithReceiverEventData) => {
setRecieving(true);
if (data.dragged.payload.type !== "group")
return setRecievePosition("below");
if (data.receiver.receiveOffsetRatio.y < 0.5) {
setRecievePosition("above");
} else {
setRecievePosition("below");
}
},
[]
);
const buttons = [
{
name: "minus",
onPress: () => {
if (!featureAvailable?.isAllowed) {
onPress: async () => {
const feature = await isFeatureAvailable("customToolbarPreset");
if (!feature.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
message: feature?.error
});
return;
}
@@ -157,11 +166,12 @@ export const Group = ({
},
{
name: "plus",
onPress: () => {
if (!featureAvailable?.isAllowed) {
onPress: async () => {
const feature = await isFeatureAvailable("customToolbarPreset");
if (!feature.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
message: feature?.error
});
return;
}

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