Compare commits

..

507 Commits

Author SHA1 Message Date
Abdullah Atta
780bebe141 web: fix crash on webkit (safari) 2023-06-05 20:55:41 +05:00
ammarahm-ed
f707b43efb mobile: release v2.5.1 2023-06-05 20:40:34 +05:00
Abdullah Atta
92e9cd740f web: fix crash on opening notebook context menu 2023-06-05 19:06:53 +05:00
Abdullah Atta
20bef5e70c web: fix crash when opening notebook context menu 2023-06-05 19:05:39 +05:00
ammarahm-ed
c9b45875a0 mobile: remove patch 2023-06-05 18:54:36 +05:00
Abdullah Atta
f180b79a3e web: bump version to 2.5.1 2023-06-05 18:40:56 +05:00
ammarahm-ed
438a3c2686 mobile: update deps 2023-06-05 18:33:52 +05:00
ammarahm-ed
64d23d3de0 mobile: show loading when updating marketing consent 2023-06-05 18:27:51 +05:00
ammarahm-ed
e251c6f71c mobile: add marketing consent settings 2023-06-05 18:21:07 +05:00
Abdullah Atta
40cb6ae8bf web: allow opting out of marketing emails 2023-06-05 18:21:07 +05:00
Abdullah Atta
06aaa7e2c2 web: clarify marketing consent on sign up screen 2023-06-05 18:21:07 +05:00
Abdullah Atta
ab29a01649 core: allow changing marketing emails consent settings 2023-06-05 18:21:07 +05:00
Abdullah Atta
49e7bdf202 editor: allow starting lists with arbitrary number 2023-06-05 18:04:13 +05:00
ammarahm-ed
2e0f9d8524 mobile: do not move login dialog on keyboard change 2023-06-05 16:40:33 +05:00
Ammar Ahmed
092b26be57 mobile: add support for image & file sharing (#2652)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2023-06-05 16:26:39 +05:00
Abdullah Atta
3525feffcf web: fix color not setting on icons 2023-06-05 16:21:40 +05:00
Abdullah Atta
6cb5a5e473 web: fix incorrect loading state of icons 2023-06-05 16:20:40 +05:00
Abdullah Atta
0992d4eeab editor: make image selectable in readonly mode 2023-06-05 16:02:50 +05:00
Abdullah Atta
828cd6be35 editor: disable selection persist in readonly mode 2023-06-05 16:02:50 +05:00
Abdullah Atta
1b1a962ec2 editor: minor refactor 2023-06-05 16:02:50 +05:00
Abdullah Atta
0a88502e59 editor: hide replace button in readonly mode 2023-06-05 16:02:50 +05:00
Muhammad Ali
50ff39845a editor: changed absolute to relative 2023-06-05 16:02:50 +05:00
Muhammad Ali
1cb34d0783 editor: added image-tools for readonly-mode 2023-06-05 16:02:50 +05:00
Abdullah Atta
bbb43bc8b4 web: replace date-time in test snapshot with xxx 2023-06-05 15:29:10 +05:00
ammarahm-ed
1521a7d120 mobile: formatted dates in editor 2023-06-05 15:29:10 +05:00
Abdullah Atta
3b6a2cb9de web: use editor date/time format from settings 2023-06-05 15:29:10 +05:00
Abdullah Atta
0deaed4ccd editor: make date-time format configurable 2023-06-05 15:29:10 +05:00
Abdullah Atta
8ebb58b70c core: fix reminder date time formatting 2023-06-05 15:29:10 +05:00
ammarahm-ed
f691d113d2 mobile: update wording 2023-06-05 15:29:10 +05:00
ammarahm-ed
a57eaf9be6 mobile: sync with core changes 2023-06-05 15:29:10 +05:00
ammarahm-ed
c5a264270c core: use only first 10 words of headline in title 2023-06-05 15:29:10 +05:00
Abdullah Atta
e75c62e6e1 core: use 2 digits for 12 hour time format 2023-06-05 15:29:10 +05:00
Abdullah Atta
6c3583c9e5 web: add support for setting default notebook/topic 2023-06-05 15:29:10 +05:00
Abdullah Atta
58ba453f57 web: use default note title from settings 2023-06-05 15:29:10 +05:00
Abdullah Atta
7f5d5fcb82 core: simplify default notebook object 2023-06-05 15:29:10 +05:00
Abdullah Atta
e218194402 web: add settings for default date, time & title formats 2023-06-05 15:29:10 +05:00
Abdullah Atta
c36f532bbb web: use date & time formats from settings 2023-06-05 15:29:10 +05:00
ammarahm-ed
b3b7bd44ef core: fix note count incorrect 2023-06-05 15:29:10 +05:00
ammarahm-ed
c85242a610 core: format time correctly 2023-06-05 15:29:10 +05:00
ammarahm-ed
58f7a206d9 mobile: add settings 2023-06-05 15:29:10 +05:00
Ammar Ahmed
25bf4fee82 mobile: redesign onboarding (#2633) 2023-06-05 15:13:19 +05:00
Abdullah Atta
c2ab5ded47 desktop: fix start as minimized on windows 2023-06-05 12:14:33 +05:00
Ammar Ahmed
ad6a0ebfab mobie: enable search in readonly mode (#2659) 2023-06-05 11:16:18 +05:00
ammarahm-ed
cb9e6578c1 mobile: downgrade react-native-bootsplash version 2023-06-01 21:16:17 +05:00
Abdullah Atta
17a1475ded editor: do not create 2 toolbar groups for static tools on mobile 2023-06-01 21:15:17 +05:00
Abdullah Atta
2cee5a4d89 editor: set preview attachment icon to eye 2023-06-01 21:15:17 +05:00
Abdullah Atta
337f88c373 editor: fix link popup font family 2023-06-01 21:15:17 +05:00
Abdullah Atta
53d5073690 editor: double click to preview image 2023-06-01 21:15:17 +05:00
Ramiro Pruis
9cc9660b48 editor: allow download & preview of attachments in readonly mode (#2621)
Signed-off-by: Ramiro Pruis<ramapruis@gmail.com>
2023-06-01 20:39:10 +05:00
ammarahm-ed
e6162b2580 mobile: fix crash on download all attachement 2023-05-31 11:31:51 +05:00
ammarahm-ed
abc14d2d1f mobile: set fdroid build to false 2023-05-30 00:17:39 +05:00
ammarahm-ed
fa11b5fabb mobile: update packages 2023-05-29 23:52:43 +05:00
ammarahm-ed
d904916863 mobile: release v2.5.0 2023-05-29 23:01:53 +05:00
Abdullah Atta
0cfc2e1609 web: bump version to 2.5.0 2023-05-29 19:14:53 +05:00
Abdullah Atta
3742e923c9 editor: fix crash due to attachment.mime being undefined 2023-05-29 13:12:28 +05:00
Abdullah Atta
792a254513 web: fix split pane sash styling 2023-05-29 12:09:50 +05:00
Abdullah Atta
17928ba182 web: disable loading announcements during tests 2023-05-27 16:50:24 +05:00
Abdullah Atta
2da0f164c6 web: fix list crash on shift+click when there's no selection 2023-05-27 16:47:54 +05:00
Abdullah Atta
4eabe79e7e web: fix attachment caching issues 2023-05-27 16:14:12 +05:00
Abdullah Atta
89dd8f5b6c web: minor ui fixes for pdf attachment preview 2023-05-27 16:14:12 +05:00
Abdullah Atta
b4d3662897 web: never delete image & pdf attachments 2023-05-27 16:14:12 +05:00
ammarahm-ed
675e1e2c6f mobile: do not cleanup documents from cache after download 2023-05-27 16:14:12 +05:00
ammarahm-ed
0f7f3bae7e mobile: save pdf snapshot 2023-05-27 16:14:12 +05:00
Abdullah Atta
c917d95a7b web: remove unnecessary css import 2023-05-27 16:14:12 +05:00
Abdullah Atta
3ae9001504 mobile: remove rn-fetch-blob patch 2023-05-27 16:14:12 +05:00
Abdullah Atta
cc97b8beed global: update package-lock.json 2023-05-27 16:14:12 +05:00
Abdullah Atta
954bdebcef web: remove @react-pdf-viewer/default-layout 2023-05-27 16:14:12 +05:00
Abdullah Atta
04b62f4e53 misc: update package-lock.json 2023-05-27 16:14:12 +05:00
Abdullah Atta
a2dcb67b36 web: add support for previewing pdf attachments 2023-05-27 16:14:12 +05:00
Abdullah Atta
cc9141c981 editor: enable preview attachment button on desktop 2023-05-27 16:14:12 +05:00
Abdullah Atta
85484b1762 fs: add support for getting actual file size 2023-05-27 16:14:12 +05:00
Abdullah Atta
1a0c94896a config: add new "fs" scope for commits 2023-05-27 16:14:12 +05:00
ammarahm-ed
068a99463d mobile: fix loading 2023-05-27 16:14:12 +05:00
ammarahm-ed
b413315777 mobile: add pdf attachment previews 2023-05-27 16:14:12 +05:00
ammarahm-ed
7227513783 editor: enable attachment previews for files 2023-05-27 16:14:12 +05:00
Abdullah Atta
083e4e1150 web: add basic support for attachment previews 2023-05-27 16:14:12 +05:00
Abdullah Atta
09098410a5 editor: enable previewing attachments on desktop/web 2023-05-27 16:14:12 +05:00
Abdullah Atta
2c9f5df530 web: fix font family in unlock note view 2023-05-25 16:15:01 +05:00
Abdullah Atta
3ad346362b web: fix notebooks in move note dialog not getting checked sometimes 2023-05-25 16:15:01 +05:00
ammarahm-ed
e176b1bf76 mobile: reset premium status 2023-05-25 15:55:14 +05:00
ammarahm-ed
b368232598 mobile: fix app hangs on adding >3 notebooks on iOS 2023-05-25 15:54:54 +05:00
Abdullah Atta
bcd27e10c1 web: add support for search inside notebook 2023-05-25 15:52:58 +05:00
Abdullah Atta
af9b9b7083 web: fix notebook item date font family 2023-05-25 15:44:09 +05:00
ammarahm-ed
ea1565048a mobile: tag multiple notes from manage tags sheet 2023-05-25 15:41:43 +05:00
Muhammad Ali
59b081704b web: add support for setting tags in bulk (#2328)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-25 14:10:32 +05:00
Muhammad Ali
2761222b36 web: add support for printing notes (#2551)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-25 12:19:21 +05:00
Muhammad Ali
571f15b556 web: close deleted note on sync (#2580)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-25 12:14:49 +05:00
Muhammad Ali
283efa6e41 editor: prevent table column size reset in readonly mode (#2586) 2023-05-25 12:11:57 +05:00
Abdullah Atta
b3b1b7470e web: do not show save file dialog if download is canceled 2023-05-25 12:02:50 +05:00
Abdullah Atta
a9e59b17dc web: use fixed size columns in attachment manager 2023-05-25 12:02:50 +05:00
Abdullah Atta
22d946b677 web: fix app lag when downloading attachment 2023-05-25 12:02:50 +05:00
ammarahm-ed
6a748c3f98 mobile: fix download all attachments progress 2023-05-25 10:18:21 +05:00
Abdullah Atta
520296106a editor: bump version to 1.5.0 2023-05-25 09:48:22 +05:00
Abdullah Atta
3b727e6f64 editor: update tiptap packages to v2.0.3 2023-05-25 09:47:20 +05:00
ammarahm-ed
b77711a53d mobile: use isImage check from core 2023-05-23 12:52:50 +05:00
ammarahm-ed
aebb2d414b mobile: allow attaching images from file manager 2023-05-23 12:52:50 +05:00
ammarahm-ed
e0e505371c mobile: update download attachments 2023-05-23 12:52:50 +05:00
ammarahm-ed
b9040a9358 mobile: show error on file check failed 2023-05-23 12:52:50 +05:00
ammarahm-ed
68f10ce308 core: add isAudio function 2023-05-23 12:52:50 +05:00
Abdullah Atta
8a475f654f web: improve attachments manager ui/ux 2023-05-23 12:52:50 +05:00
ammarahm-ed
1043b7df55 mobile: improved attachments manager 2023-05-23 12:52:50 +05:00
ammarahm-ed
8383e09243 mobile: show currently downloading file 2023-05-23 12:52:50 +05:00
ammarahm-ed
df58a6cfc4 mobile: fix recheck attachments 2023-05-23 12:52:50 +05:00
ammarahm-ed
a9df577478 mobile: update deps 2023-05-23 12:52:50 +05:00
ammarahm-ed
e021d93893 mobile: download mutliple attachments 2023-05-23 12:52:50 +05:00
Abdullah Atta
24e13f488e web: fully move to using web streams everywhere 2023-05-23 12:52:50 +05:00
Muhammad Ali
6f88b70937 web: add support for downloading all attachments as zip 2023-05-23 12:52:50 +05:00
ammarahm-ed
a90f957aa4 mobile: always check editor is ready on enter foreground 2023-05-23 08:08:10 +05:00
ammarahm-ed
748953b3eb mobile: don't lock app on showing app review 2023-05-23 08:07:27 +05:00
Muhammad Ali
874c892986 editor: make cell properties work with header cells (#2506) 2023-05-23 07:17:29 +05:00
Muhammad Ali
ab678fa3a9 core: fix duplication of notes that are directly inside a notebook (#2552)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-23 06:40:07 +05:00
Muhammad Ali
c2351ab2d3 editor: add support for link markdown syntax (#2576) 2023-05-23 06:36:34 +05:00
Muhammad Ali
3fd3e02256 web: fix wrong date font in notebook item (#2560)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-23 06:20:12 +05:00
Muhammad Ali
d2fd26abd5 editor: fix position of outline list bullet/arrow (#2555) 2023-05-20 08:03:39 +05:00
ammarahm-ed
dd84b9ca2f mobile: add fdroid changelog 2023-05-18 20:06:37 +05:00
Abdullah Atta
269f083dce clipper: bump version to 0.2.1 2023-05-18 15:14:25 +05:00
Abdullah Atta
e18867d3f1 clipper: request scripting permission 2023-05-18 15:13:44 +05:00
Abdullah Atta
e221882f8e clipper: change version to 0.2.0 2023-05-17 11:35:52 +05:00
Abdullah Atta
153ee4bce3 clipper: load content scripts on demand to reduce host permissions 2023-05-17 11:35:52 +05:00
ammarahm-ed
a2ecdab61c mobile: release 2.4.17 2023-05-17 11:24:29 +05:00
Ammar Ahmed
eb089e6211 Merge pull request #2543 from streetwriters/feat/in-app-review
mobile: add in-app-review flow
2023-05-17 11:18:22 +05:00
ammarahm-ed
1981c3a2f8 mobile: add in-app-review flow 2023-05-17 11:14:49 +05:00
Abdullah Atta
68b3ebe318 clipper: get rid of <all_urls> permission 2023-05-17 10:11:47 +05:00
ammarahm-ed
8a0b6264d2 mobile: fix pro features locked for pro users 2023-05-17 10:09:37 +05:00
Abdullah Atta
239174c3e7 web: bump version to 2.4.11 2023-05-17 09:29:16 +05:00
Abdullah Atta
db1503d828 clipper: remove contextMenus permission 2023-05-16 18:05:20 +05:00
Abdullah Atta
7b1cbba9e7 clipper: bump version to 0.2 2023-05-16 17:39:21 +05:00
Abdullah Atta
2c3c193629 clipper: default to simplified mode if user is no longer pro 2023-05-16 17:35:59 +05:00
Abdullah Atta
899e56e9cd clipper(core): escape % & ' during css stringify 2023-05-16 17:35:59 +05:00
Abdullah Atta
9c9582ae12 clipper: improve error messages 2023-05-16 17:35:59 +05:00
Abdullah Atta
b918667b63 clipper: adjust icon size in inline tag 2023-05-16 17:35:59 +05:00
Abdullah Atta
878c901e06 clipper: add support for assigning multiple notebooks 2023-05-16 17:35:59 +05:00
Muhammad Ali
fdd3897a60 web: close tag dropdown on tab press (#2508) 2023-05-16 17:32:00 +05:00
ammarahm-ed
c958657b01 mobile: release 2.4.16 2023-05-16 12:11:43 +05:00
Abdullah Atta
e7175f48a0 clipper: do not exclude localhost tabs in production mode 2023-05-13 17:51:10 +05:00
Abdullah Atta
4e1862d653 clipper: improve connection reliability
this adds support for working with multiple Notesnook tabs
even though it is not currently recommended that you
open multiple Notesnook tabs
2023-05-13 17:51:10 +05:00
Abdullah Atta
1d5ac37222 clipper: always clip tab from current active window 2023-05-13 17:50:53 +05:00
Abdullah Atta
32d027fff6 clipper(core): don't clip images in simplified mode 2023-05-13 17:50:41 +05:00
ammarahm-ed
9434993373 mobile: remove usesCleartextTraffic in release builds 2023-05-08 18:30:36 +05:00
Abdullah Atta
2bd0f1a304 web: allow users to view their billing history 2023-05-08 15:25:20 +05:00
Abdullah Atta
7f873f240e web: allow requesting refunds from settings 2023-05-08 15:25:20 +05:00
Abdullah Atta
a0229729b7 web: use sandbox checkout during development 2023-05-08 15:25:20 +05:00
Abdullah Atta
dd368a4170 web: show success on checkout completion 2023-05-08 15:25:20 +05:00
ammarahm-ed
6d3e985c9d mobile: fix failing build 2023-05-08 09:27:55 +05:00
ammarahm-ed
15858db21b mobile: sync quick notes only when online 2023-05-08 08:19:17 +05:00
Ammar Ahmed
b69a94a96f mobile: fix premium toast is not shown when using premium features on mobile (#2504) 2023-05-08 08:18:38 +05:00
Muhammad Ali
73b965eb7c web: close properties pane when note is switched (#2486) 2023-05-06 16:16:01 +05:00
Abdullah Atta
5738f48360 core: expire session if key is not found 2023-05-06 15:27:58 +05:00
Muhammad Ali
de74b8c2cd editor: position search box correctly in readonly mode (#2478) 2023-05-03 13:22:25 +05:00
Muhammad Ali
2d07d79057 web: fix issue where notes directly in a notebook could not be unlinked (#2466)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-02 12:49:24 +05:00
ammarahm-ed
5f6a38c8b1 mobile: release 2.4.15 2023-04-29 09:07:46 +05:00
Muhammad Ali
3496e6bfa9 editor: increased hit Area 2023-04-29 08:33:20 +05:00
ammarahm-ed
fc5b62ba61 mobile: remove editor flicker on app launch 2023-04-29 08:29:48 +05:00
ammarahm-ed
9d0a206240 mobile: fix editor not rendered when enter foreground 2023-04-29 08:29:48 +05:00
ammarahm-ed
ee6bbca18c mobile: only call verify biometrics once on focus 2023-04-29 05:47:05 +05:00
ammarahm-ed
e6d5472514 mobile: fix rerender crash 2023-04-29 05:46:06 +05:00
Abdullah Atta
1e5f13087b web: try to reconnect sync on window focus 2023-04-28 10:39:59 +05:00
Abdullah Atta
abb89ecbc3 web: bump version to 2.4.10 2023-04-27 15:15:46 +05:00
Abdullah Atta
aa168bec5e desktop: revert "desktop: remove custom protocol interception for https requests" 2023-04-27 15:14:30 +05:00
Abdullah Atta
f9116b22db web: bump version to 2.4.9 2023-04-27 13:16:01 +05:00
Abdullah Atta
5b957f5f0d web: fix Can't resolve './network.worker' error 2023-04-27 13:08:13 +05:00
Abdullah Atta
a578aae3dc web: wait for internet before running sync on device wake 2023-04-27 13:08:13 +05:00
Abdullah Atta
43e5d2fa2d editor: fix table properties tooltip text 2023-04-27 13:08:03 +05:00
Abdullah Atta
b58d034716 theme: use outline instead of boxShadow for input border 2023-04-27 12:59:26 +05:00
Abdullah Atta
3b77d79997 web: prevent pro users from resubscribing 2023-04-27 12:59:26 +05:00
Abdullah Atta
ef4c9af7dc web: rename setters from onXYZ to xyz 2023-04-27 12:59:26 +05:00
ammarahm-ed
8ba3cf172e mobile: release v2.4.14 2023-04-27 12:49:02 +05:00
ammarahm-ed
3e446ce9dd mobile: fix crash on get search items 2023-04-27 11:27:48 +05:00
ammarahm-ed
394aa98713 mobile: remove usage of lazy imports 2023-04-27 10:59:04 +05:00
Muhammad Ali
a2ac882a05 desktop: remove custom protocol interception for https requests (#2426) 2023-04-27 10:13:37 +05:00
ammarahm-ed
cae36487f9 mobile: fix notebook not updated on note 2023-04-27 09:38:44 +05:00
ammarahm-ed
fade5eddbf mobile: replace newline with paragraph in generated html 2023-04-27 08:32:09 +05:00
ammarahm-ed
66b2ffce31 mobile: fix webpack config 2023-04-27 08:32:09 +05:00
ammarahm-ed
13cd2d4daa mobile: refactor 2023-04-27 08:18:25 +05:00
ammarahm-ed
8442faeeb1 mobile: close editor on navigate to topic 2023-04-26 20:06:40 +05:00
Ammar Ahmed
6d18383b22 mobile: sync notes created from quick note/notification notes (#2428)
* mobile: sync quick notes

* Update apps/mobile/app/services/notifications.ts

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>

* mobile: ensure full db init when sync is required

---------

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-04-26 15:15:08 +05:00
ammarahm-ed
f9bcfe2f7a mobile: obfuscate short emails 2023-04-26 15:02:56 +05:00
ammarahm-ed
3621f95dbc web: obfuscate short emails 2023-04-26 15:02:56 +05:00
ammarahm-ed
536dead44d mobile: always load notes on init 2023-04-26 14:59:05 +05:00
ammarahm-ed
39d74e80a9 mobile: ensure that overlay gets hidden when entering foreground 2023-04-26 14:59:05 +05:00
ammarahm-ed
46e03dd248 mobile: only show app lock for main activity 2023-04-26 14:59:05 +05:00
ammarahm-ed
152fee2c29 mobile: reload editor if rendered view lost 2023-04-26 14:59:05 +05:00
ammarahm-ed
492a4d16b2 mobile: fix note options ui 2023-04-26 14:58:37 +05:00
ammarahm-ed
3d7c1fa1e7 mobile: fix configure toolbar sheet close with swipe 2023-04-26 14:58:21 +05:00
ammarahm-ed
69bb0e3656 editor: fix toolbar dropdown buttons size on mobile 2023-04-26 14:54:30 +05:00
ammarahm-ed
4875cfa108 core: duplicate note without content 2023-04-26 14:47:52 +05:00
ammarahm-ed
a28beb62c6 core: deep clone note when duplicating 2023-04-26 14:46:24 +05:00
Abdullah Atta
21c8960146 web: fix "Unauthorized" error on subscription cancel 2023-04-26 11:39:48 +05:00
Abdullah Atta
2ba638b123 web: fix font family on input field labels 2023-04-25 14:47:29 +05:00
ammarahm-ed
8548940cb9 mobile: refactor 2023-04-25 10:18:20 +05:00
Elmin Didic
909bdfa0cf docs: fix grammar & other typos in CONTRIBUTING.md (#2384)
Signed-off-by: Elmin Didic <35606145+ElminD@users.noreply.github.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2023-04-25 10:13:56 +05:00
ammarahm-ed
bfb3c56abe mobile: remove unused patch file 2023-04-18 02:10:16 +05:00
ammarahm-ed
e06836f701 mobile: fix icon color in dark mode 2023-04-18 02:04:55 +05:00
ammarahm-ed
bc16cb79f2 mobile: fix failing tests 2023-04-18 01:54:51 +05:00
ammarahm-ed
176c46e3f2 mobile: update word count on exit 2023-04-18 01:05:14 +05:00
Abdullah Atta
af65055ce9 web: bump version to 2.4.8 2023-04-18 00:55:37 +05:00
ammarahm-ed
ad23ff19de mobile: update release notes 2023-04-18 00:55:24 +05:00
ammarahm-ed
cf65fc4f57 editor: make drag handle size dynamic 2023-04-18 00:50:41 +05:00
ammarahm-ed
9ab83e7fbb editor: reduce size of task list header 2023-04-18 00:50:20 +05:00
ammarahm-ed
1acf2aadba editor: remove top level font family on headings 2023-04-18 00:32:17 +05:00
Abdullah Atta
06833bcac9 editor: make sure editor font settings apply to all nodes 2023-04-18 00:30:40 +05:00
Ammar Ahmed
3bf6b5293a feat: Change default font size & font family in editor 2023-04-17 23:45:28 +05:00
ammarahm-ed
c40d33df37 mobile: ensure editor reloads properly 2023-04-17 23:32:28 +05:00
ammarahm-ed
fe609f8275 mobile: release v2.4.13 2023-04-17 23:08:29 +05:00
Abdullah Atta
3566f47f2d editor: all toolbars should scroll if buttons overflow 2023-04-17 22:48:43 +05:00
ammarahm-ed
4b5526dffd mobile: fix text exports 2023-04-17 22:43:56 +05:00
ammarahm-ed
4977390394 mobile: return if some days are already selected 2023-04-17 22:41:12 +05:00
ammarahm-ed
87804de452 mobile: auto select current day when repeat mode is selected 2023-04-17 22:41:12 +05:00
Ammar Ahmed
bc55d2b476 mobile: Fix toast shown forever sometimes (#2362) 2023-04-17 22:40:43 +05:00
Abdullah Atta
3ff34a5c82 core: use official html-to-text lib instead of fork 2023-04-17 18:09:29 +05:00
Abdullah Atta
7e01e90e22 core: do not clean relations on removing reminders 2023-04-17 17:25:03 +05:00
Abdullah Atta
d568be6536 editor: fix crash while editing links on mobile 2023-04-17 17:04:24 +05:00
ammarahm-ed
734e79604f mobile: use getFontById from editor 2023-04-16 01:51:49 +05:00
Abdullah Atta
227a962042 core: fix rename tag/color test 2023-04-16 01:04:02 +05:00
Abdullah Atta
a1640175e5 editor: remove unnecessary props from ToolProps 2023-04-16 01:01:01 +05:00
Abdullah Atta
7cf61570ef web: remove default-font component 2023-04-16 00:59:16 +05:00
Abdullah Atta
83762c668e web: refactor default font settings 2023-04-16 00:57:44 +05:00
Abdullah Atta
ecd4a08b1c editor: encapsulate font family utility 2023-04-16 00:57:30 +05:00
ammarahm-ed
7b66462300 mobile: fix crash when tap on notification and
reminder does not exist.
2023-04-15 23:50:24 +05:00
ammarahm-ed
41639fd325 editor: improve editor performance on mobile 2023-04-15 23:25:45 +05:00
ammarahm-ed
2fe82841ef editor: remove useIsKeyboardOpen 2023-04-15 23:25:45 +05:00
Ammar Ahmed
9b7af87611 mobile: fix db initialization in background (#2322)
* core: init collections separately

* mobile: fix db init in background

* mobile: don't call db.init on logout

* mobile: improve db init in app/share extension

* mobile: cleanup

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2023-04-15 23:23:04 +05:00
Muhammad Ali
d99ccbc243 editor: fix jumping text/bg color popup (#2306)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-04-15 17:53:01 +05:00
Abdulrehman-Jafer
0f191eba5c editor: fix code block highlighting not updating in some cases (#2267)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2023-04-15 17:50:03 +05:00
Ammar Ahmed
26657c7ee3 mobile: fix app font sizes (#2355) 2023-04-15 17:01:49 +05:00
Abdulrehman-Jafer
a12749eb82 core: disable input sanitization on color/tag rename (#2274)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-04-15 16:42:25 +05:00
Abdulrehman-Jafer
1a56d38116 web: migrate use-database.js to TypeScript (#2287)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2023-04-15 16:41:14 +05:00
Ammar Ahmed
0b9025af11 mobile: redesign reminder/notebook sheets (#2321) 2023-04-15 16:40:36 +05:00
Muhammad Ali
6f5a44cd11 web: do not show success toast if notebook failed to create (#2307)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-04-15 16:37:32 +05:00
Abdulrehman-Jafer
5e1cc307b1 web: migrate use-media-query.js to Typescript (#2326)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-04-15 16:35:03 +05:00
Ammar Ahmed
2197f1ec5e mobile: fix tag suggestions in share widget (#2354) 2023-04-15 16:31:59 +05:00
NedimHodzic
1da5fd3661 docs: fix grammar issues in mobile/web READMEs (#2345)
Signed-off-by: Nedim Hodzic hodzicnedim11@gmail.com
2023-04-15 05:22:18 +05:00
ammarahm-ed
49e79b52b2 core: add remote note if local note does not exist 2023-04-15 02:17:56 +05:00
ammarahm-ed
c9a657298c mobile: fix crash when data is null in list 2023-04-15 02:03:36 +05:00
Nick Thomas
cfd7caf16c Updated grammar readme
docs: Updated grammar readme
Signed-off-by: Nick Thomas nick02@iastate.edu

Signed-off-by: Nick Thomas <73144044+NickThomas20@users.noreply.github.com>
2023-04-14 06:42:57 +05:00
ammarahm-ed
9744679550 mobile: add support for setting default font-size & font-family 2023-04-08 01:53:46 +05:00
ammarahm-ed
9cdc502ce5 web: fix font family not applied when set on editor 2023-04-08 01:14:33 +05:00
Ammar Ahmed
300a104949 Merge branch 'master' into change-default-font
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2023-04-07 23:17:23 +05:00
ammarahm-ed
efad6520a2 mobile: release 2.4.12 2023-04-07 22:46:56 +05:00
ammarahm-ed
cc58351731 core: fix notes marked as local only get deleted
on sync
2023-04-07 22:40:10 +05:00
ammarahm-ed
cf2f4c4bbb mobile: fix tag not updated on list item on rename 2023-04-07 14:46:28 +05:00
Abdulrehman-Jafer
cc161b844c web: migrate use-is-user-premium to ts (#2297)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-04-07 14:10:52 +05:00
ammarahm-ed
45ebf0670f mobile: show error when note is locked 2023-04-07 02:22:49 +05:00
ammarahm-ed
9cfaa90232 mobile: use text exporter from core 2023-04-07 02:22:49 +05:00
ammarahm-ed
edf4a248ed web: remove unnecessary param passed to export 2023-04-07 02:22:49 +05:00
ammarahm-ed
14d7ec6b4e core: allow exports with custom content 2023-04-07 02:22:49 +05:00
Ammar Ahmed
cd2ca94c3d mobile: Fix crash when leaving note after search (#2289) 2023-04-07 01:10:52 +05:00
Abdullah Atta
f1609aaa46 editor: do not convert \n characters to paragraphs 2023-04-06 01:40:55 +05:00
ammarahm-ed
851364c0bf mobile: release 2.4.11 2023-04-05 02:45:02 +05:00
ammarahm-ed
913a1743b5 mobile: update deps 2023-04-05 02:13:49 +05:00
ammarahm-ed
dc5572d4b9 editor: enable passing custom sessionId 2023-04-05 02:13:49 +05:00
ammarahm-ed
554560903c mobile: improve share extension search 2023-04-05 02:13:49 +05:00
ammarahm-ed
885e4a325c mobile: fix snap points for sheet on android 2023-04-05 02:13:49 +05:00
ammarahm-ed
156ce7e711 mobile: ensure note is saved when change note 2023-04-05 02:13:49 +05:00
Abdullah Atta
71ecc05bd6 web: bump version to v2.4.7 2023-04-05 01:54:51 +05:00
Abdullah Atta
98b93d42c1 theme: publish 2023-04-05 01:53:29 +05:00
Abdullah Atta
2e7935cacc editor: publish 2023-04-05 01:53:16 +05:00
Abdullah Atta
50d64af7f6 web: update export html test snapshots 2023-04-04 23:35:35 +05:00
Abdullah Atta
e452fd3d2f core: update export tests snapshots 2023-04-04 23:16:04 +05:00
Abdullah Atta
64dd481402 web: fix cannot read property id of undefined 2023-04-04 22:04:46 +05:00
Abdullah Atta
28f647fa06 web: fix a couple of state mgmt issues with move note dialog 2023-04-04 22:04:14 +05:00
ammarahm-ed
9eb1061cd1 mobile: refactor 2023-04-04 22:04:14 +05:00
ammarahm-ed
3cab264213 mobile: increase the timeout 2023-04-04 22:04:14 +05:00
ammarahm-ed
fb56e7c866 mobile: exit current topic if it is deleted. 2023-04-04 22:04:14 +05:00
ammarahm-ed
234e368c4d mobile: cleanup 2023-04-04 22:04:14 +05:00
ammarahm-ed
4de4693505 mobile: fix topic sheet snaps to top in topics screen 2023-04-04 22:04:14 +05:00
ammarahm-ed
5563c7d5f8 mobile: remove floating button on topic screen 2023-04-04 22:04:14 +05:00
ammarahm-ed
93225e5a30 mobile: fix topic sheet related state issues 2023-04-04 22:04:14 +05:00
ammarahm-ed
0ab9846732 mobile: fix edit notebook sheet does not close after saving edits 2023-04-04 22:04:14 +05:00
ammarahm-ed
f6f1cf0ae7 mobile: fix edit notebook button not working 2023-04-04 22:04:14 +05:00
ammarahm-ed
69615d54db mobile: remember last position of sheet in notebook 2023-04-04 22:04:14 +05:00
ammarahm-ed
a990d0f404 mobile: fix move note sheet not opening 2023-04-04 22:04:14 +05:00
ammarahm-ed
4aeff48e74 mobile: cleanup 2023-04-04 22:04:14 +05:00
ammarahm-ed
78c1a75ec9 mobile: improve opening sheet 2023-04-04 22:04:14 +05:00
ammarahm-ed
9e6664a80e mobile: improve topics-sheet ux 2023-04-04 22:04:14 +05:00
ammarahm-ed
b306a09747 editor: refresh editor on line spacing changed 2023-04-04 17:23:27 +05:00
ammarahm-ed
fab52012ad mobile: fix loading indicator color 2023-04-04 16:26:07 +05:00
ammarahm-ed
fc39c714b6 mobile: update project pods 2023-04-04 16:26:07 +05:00
ammarahm-ed
5fff4bbb0e mobile: fix font sizes 2023-04-04 16:26:07 +05:00
ammarahm-ed
0a29571ad8 mobile: upgrade share extension to support organization 2023-04-04 16:26:07 +05:00
ammarahm-ed
275fa1b3ca mobile: fix loading editor screen on app launch 2023-04-04 16:25:07 +05:00
ammarahm-ed
79889665e5 mobile: cleanup deps 2023-04-04 16:25:07 +05:00
ammarahm-ed
9888ad327c mobile: fix editor hidden when changing orientation
from landscape to
portrait
2023-04-04 16:25:07 +05:00
ammarahm-ed
77ab2d7740 mobile: skip showing loading overlay for relatively smaller
notes
2023-04-04 16:25:07 +05:00
ammarahm-ed
509dc4b541 editor: scroll to top on note change 2023-04-04 16:25:07 +05:00
ammarahm-ed
ff84ee2d42 mobile: improve note saving reliability &
performance
2023-04-04 16:25:07 +05:00
ammarahm-ed
d838c2e8ad editor: update word count immediately after note is loaded
Previously when opening note, the word count on mobile would
be 0, then update to actual word count. This has been fixed so
correct word count shows when opening not without any
delay.
2023-04-04 16:25:07 +05:00
ammarahm-ed
28d718acf6 mobile: show no loading for relatively smaller notes
Previously if a note had smaller content, loading would still take
quite a bit long which seemed unnecessary. Now loading will show
only  if note takes long enough to load which should make the app
feel much more snappier than before when opening notes.
2023-04-04 16:25:07 +05:00
Muhammad Ali
adf546f29c web: disable deleting locked notes (#2244) 2023-04-04 16:24:28 +05:00
Abdulrehman-Jafer
b18cf277f6 core: fix code block overflow in PDF export (#2245)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-04-04 16:19:02 +05:00
ammarahm-ed
8ab26eb852 mobile: fix crash where notebook is not present 2023-03-31 12:56:20 +05:00
ammarahm-ed
aac43274a0 mobile: release 2.4.10 2023-03-30 17:22:00 +05:00
Muhammad Ali
2fb2c4247a web: do not add "none" tool in Configure Toolbar dialog (#2193)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-03-30 17:10:43 +05:00
Abdulrehman-Jafer
cb1d32d177 core: fix txt, pdf & html export line spacing (#2215)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
Co-authored-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2023-03-30 17:09:00 +05:00
ammarahm-ed
75e1985a6f mobile: save value on change 2023-03-30 16:48:55 +05:00
ammarahm-ed
0093b39f64 mobile: fix nav bar does not follow app theme 2023-03-30 16:07:58 +05:00
ammarahm-ed
d19c2ef735 mobile: add mastodon social link 2023-03-30 15:30:55 +05:00
ammarahm-ed
db527cfbf3 mobile: fix splashscreen logo quality 2023-03-30 15:04:08 +05:00
ammarahm-ed
7da8ddb24f mobile: topic title should to single line 2023-03-30 15:02:01 +05:00
ammarahm-ed
5d401ac9ac mobile: use diff sheet size on each platform 2023-03-30 13:54:21 +05:00
ammarahm-ed
9605eafa00 mobile: add support for sorting topics in sheet 2023-03-30 13:54:21 +05:00
Ammar Ahmed
783460c685 Merge pull request #2218 from streetwriters/fix-reminder-date-set
mobile: fix reminder date picker not going in past
2023-03-29 16:54:24 +05:00
ammarahm-ed
af906d8ace mobile: add some null checks 2023-03-29 16:54:04 +05:00
ammarahm-ed
fca26a8793 mobile: fix reminder date picker not going in past 2023-03-29 16:29:40 +05:00
Muhammad Ali
27863def9c web: fix RTL support for outline lists (#2175)
Co-authored-by: Muhammad Ali <alihamuh@gmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-03-24 12:51:00 +05:00
Abdullah Atta
f1d4fd1d5d editor: fix crash when theme.colors is undefined 2023-03-24 12:49:26 +05:00
Abdullah Atta
61b7e43d08 web: do not interrupt importer if notebook is undefined 2023-03-24 11:43:37 +05:00
Abdullah Atta
87ebe1918c web: use debounce instead of inlineDebounce for selection updates in editor 2023-03-24 11:41:03 +05:00
ammarahm-ed
4c5253c267 mobile: release v2.4.9 2023-03-24 02:54:36 +05:00
Abdulrehman-Jafer
f5be8a0c86 web: migrate keyboard.js to typescript (#2160)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-24 00:57:55 +05:00
Abdullah Atta
5392582819 web: fix crash on opening an empty notebook 2023-03-24 00:35:29 +05:00
Abdullah Atta
651ae4cdaa web: bump version to 2.4.6 2023-03-24 00:28:19 +05:00
Abdullah Atta
d87b5c5386 desktop: don't show tray menu icons on flatpak builds 2023-03-24 00:28:19 +05:00
ammarahm-ed
d5331b1997 mobile: release v2.4.8 2023-03-24 00:18:12 +05:00
ammarahm-ed
0a6f8fa18e mobile: improve publish note sheet ui 2023-03-24 00:12:18 +05:00
Abdullah Atta
c1ba16eb80 web: group top level notebook notes 2023-03-23 23:58:17 +05:00
Ammar Ahmed
0cd01b25bd Merge pull request #2187 from streetwriters/fix/publish-note-mobile
Fix publish note with password on mobile
2023-03-23 23:52:50 +05:00
Abdullah Atta
f0c1300ec8 web: hyper optimize move note dialog 2023-03-23 23:48:49 +05:00
ammarahm-ed
70eee2a224 mobile: cleanup logs 2023-03-23 23:48:49 +05:00
ammarahm-ed
42470d1d67 mobile: fix add button color in dark mode 2023-03-23 23:48:49 +05:00
ammarahm-ed
7fc58a3fa5 mobile: make bottom padding optional in sheet 2023-03-23 23:48:49 +05:00
ammarahm-ed
e186037c03 mobile: fix bottom padding on sheet 2023-03-23 23:48:49 +05:00
ammarahm-ed
732a0ffea4 mobile: fix duplicate key warning 2023-03-23 23:48:49 +05:00
ammarahm-ed
89f2e9fde1 mobile: show selected indicator on notebook 2023-03-23 23:48:49 +05:00
Abdullah Atta
c369541a7f web: indicate which notebooks have selected topics 2023-03-23 23:48:49 +05:00
Abdullah Atta
0b7a157ab1 web: enable multiselect mode if note is in > 1 nb/topic 2023-03-23 23:48:49 +05:00
ammarahm-ed
9b86c6ebb7 mobile: fix multiselection bugs 2023-03-23 23:48:49 +05:00
ammarahm-ed
22cd07e414 mobile: improve selection performance 2023-03-23 23:48:49 +05:00
ammarahm-ed
47f919cb61 mobile: fix reset select does not reset selected topics 2023-03-23 23:48:49 +05:00
Abdulrehman-Jafer
5b3c889b7c web: migrate css.js to typescript (#2165)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-23 17:03:23 +05:00
Abdullah Atta
77ac53215c web: remove document as any in page-visibility 2023-03-23 13:55:52 +05:00
Abdulrehman-Jafer
167c7125d2 web: migrate page-visibility.js to typescript (#2166)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-23 13:48:57 +05:00
Abdullah Atta
ba56fd0279 web: don't reset reminders before getting the user 2023-03-23 13:46:54 +05:00
Abdullah Atta
8e795b729a web: read trash cleanup interval from db on startup 2023-03-23 13:46:54 +05:00
Abdullah Atta
a4eb597f35 web: improve notebook vertical pane resizing 2023-03-23 13:46:44 +05:00
Abdullah Atta
0aeade69da web: fix topic search 2023-03-23 13:43:45 +05:00
Abdullah Atta
90a3bd3307 web: fix crash on creating a topic 2023-03-23 13:43:45 +05:00
ammarahm-ed
7a61e3558e mobile: ui cleanup 2023-03-23 13:39:14 +05:00
Abdulrehman-Jafer
dd9c555bff web: migrate parse.js to typescript (#2162)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-23 13:36:31 +05:00
ammarahm-ed
50f84780bb mobile: fix publish note with password not working 2023-03-23 13:23:04 +05:00
Abdulrehman-Jafer
e12372364d web: add support for realtime search (#2171)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-23 12:45:33 +05:00
ammarahm-ed
969611c9e6 mobile: release v2.4.7 2023-03-22 15:05:24 +05:00
ammarahm-ed
06399fe3bd mobile: fix title hidden on navigate sometimes 2023-03-22 15:02:55 +05:00
Abdullah Atta
35c5a90b4e web: update version highlights 2023-03-22 13:47:41 +05:00
Abdullah Atta
3c70cccd14 web: topic is rendered incomplete when opened directly 2023-03-22 13:42:36 +05:00
Abdullah Atta
a3c24bc666 editor: fix image resizing 2023-03-22 13:28:25 +05:00
ammarahm-ed
16b00d21a6 mobile: cleanup 2023-03-22 13:28:25 +05:00
Abdullah Atta
88f90c6240 editor: add padding to placeholder 2023-03-22 13:28:25 +05:00
Abdullah Atta
a5c44dcb3b editor: add src to image on copy 2023-03-22 13:28:25 +05:00
Abdullah Atta
c0a2dd139a web: only load new media attachments on realtime sync 2023-03-22 13:28:25 +05:00
Abdullah Atta
b354a26f0a core: allow selectively downloading media attachments 2023-03-22 13:28:25 +05:00
ammarahm-ed
b3de1205d9 mobile: cleanup 2023-03-22 13:28:25 +05:00
ammarahm-ed
f18e8ae421 mobile: properly handle loading images in editor 2023-03-22 13:28:25 +05:00
ammarahm-ed
a46b863802 core: return only attachment meta if
attachment already exists
2023-03-22 13:28:25 +05:00
ammarahm-ed
2befa03cd2 mobile: conform to new image loading api 2023-03-22 13:28:25 +05:00
Abdullah Atta
763a108abb editor: always use relative height for resizer 2023-03-22 13:28:25 +05:00
Abdullah Atta
30a069c845 editor: always use black icons on image overlay 2023-03-22 13:28:25 +05:00
Abdullah Atta
ffbf5cdc88 editor: fade-in on image load 2023-03-22 13:28:25 +05:00
Abdullah Atta
4318a65a99 editor: use dataurl attr for inserting an image 2023-03-22 13:28:25 +05:00
Abdullah Atta
bc433c56ae core: don't return invalid dataurl if image doesn't exist 2023-03-22 13:28:25 +05:00
Abdullah Atta
e43740c6bc editor: add comments for why we use UTITypes 2023-03-22 13:28:25 +05:00
Abdullah Atta
6123bccf4b editor: only save image height on load 2023-03-22 13:28:25 +05:00
Abdullah Atta
08106c20e8 editor: use dataurl instead of src attr for loading images 2023-03-22 13:28:25 +05:00
Abdullah Atta
eaaecbd5e5 core: hash base64 before saving it as attachment to prevent dups 2023-03-22 13:28:25 +05:00
Abdullah Atta
29608099db web: writeEncrypted -> writeEncryptedBase64 2023-03-22 13:28:25 +05:00
Abdullah Atta
7a773937c2 core: writeEncrypted now accepts only base64 input 2023-03-22 13:28:25 +05:00
Abdullah Atta
fa4a298bfa web: use NodeJS.Platform instead of process.platform 2023-03-22 10:45:03 +05:00
Abdulrehman-Jafer
966bedbb49 web: migrate platform.js to typescript (#2163)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-22 10:42:49 +05:00
Abdulrehman-Jafer
159b6f3ca6 web: migrate dimensions.js to typescript (#2153)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-22 09:21:48 +05:00
Abdulrehman-Jafer
de1b0fc466 web: migrate filename.js to typescript (#2156)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-22 09:07:52 +05:00
Abdulrehman-Jafer
b6a3394dee web: migrate object.js to typescript (#2161)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-22 09:00:29 +05:00
Abdulrehman-Jafer
f5ac51db13 web: migrate random.js to typescript (#2164)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-22 08:59:07 +05:00
Muhammad Ali
df94f31998 web: added state base to default font-family and size 2023-03-22 08:38:21 +05:00
Abdullah Atta
1ec0b65fe8 web: extract attachment extension from mime-type 2023-03-21 09:59:45 +05:00
Abdullah Atta
d9a77ff658 core: migrate filename.ts to javascript 2023-03-21 09:59:45 +05:00
ammarahm-ed
c45118515c editor: add react-scripts patch 2023-03-21 09:59:45 +05:00
ammarahm-ed
f9b8865351 editor: export blob as b64 with correct content type 2023-03-21 09:59:45 +05:00
ammarahm-ed
1e92b3fc92 mobile: resolve attachment hash from src if hash not avail 2023-03-21 09:59:45 +05:00
ammarahm-ed
f34fb858ec mobile: add filename extension before download 2023-03-21 09:59:45 +05:00
ammarahm-ed
1fdede5518 core: add getFileNameWithExtension utility 2023-03-21 09:59:45 +05:00
Abdulrehman-Jafer
a454ec4944 web: migrate color.js to typescript (#2151)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-21 09:37:56 +05:00
Abdullah Atta
5afbf103ac ci: disable arm64 builds for snap 2023-03-21 09:25:15 +05:00
Abdullah Atta
2ef1b76bfb ci: disable deb/rpm builds 2023-03-21 09:03:15 +05:00
Abdullah Atta
f7d06d3b82 desktop: enable arm64 builds 2023-03-21 08:40:03 +05:00
Abdullah Atta
e405468471 web: bump version to 2.4.5 2023-03-21 08:07:11 +05:00
Abdullah Atta
9d2bacdc5f web: replace fixed 7 days with trash cleanup interval 2023-03-21 08:06:34 +05:00
Abdullah Atta
b44d4702be web: update @notesnook-importer/core to v1.7.1 2023-03-20 18:52:05 +05:00
Abdullah Atta
03803dbc23 editor: fix task-list tests 2023-03-20 18:05:27 +05:00
Abdullah Atta
9ea675ded1 editor: fix sub checklists not sorting 2023-03-20 18:05:27 +05:00
Abdullah Atta
c85d623e1c web: update tab/window title for locked & conflicted notes 2023-03-20 18:05:27 +05:00
ammarahm-ed
e6f1320b1d mobile: optimize sheets 2023-03-20 17:08:06 +05:00
Abdullah Atta
80f49e22e5 web: reset tab/window title on new note 2023-03-20 17:05:18 +05:00
Abdullah Atta
4884c27a9a editor: update patches 2023-03-20 16:40:58 +05:00
Ammar Ahmed
4976a4dab4 Merge pull request #2143 from streetwriters/fix-ipad-orientation
mobile: fix ipad orientation window sizing
2023-03-20 16:36:02 +05:00
Ammar Ahmed
5eb0b5faa7 Merge pull request #2145 from streetwriters/fix-gallery-not-shown-reupload
Fix gallery not shown when reupload image
2023-03-20 16:35:49 +05:00
Ammar Ahmed
5e33cb93c6 editor: fix focus last node on tap bottom area on mobile (#2148) 2023-03-20 16:35:23 +05:00
ammarahm-ed
b4220d349f editor: fix loading copied image on ios 2023-03-20 16:34:46 +05:00
Abdullah Atta
d4752685d0 editor: keep marks when creating new list items
we shouldn't reset the user's selected styles like bold/italic when they
create a new list item.
2023-03-20 16:33:14 +05:00
ammarahm-ed
0b06d2a4bd mobile: fix attachment dialog ui 2023-03-20 15:09:52 +05:00
ammarahm-ed
a37d9a193f mobile: fix gallery not shown on reupload image 2023-03-20 15:09:23 +05:00
Abdulrehman-Jafer
b1d465e8bf web: fix monographs and reminders search (#2142)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-20 14:40:51 +05:00
Abdullah Atta
31e17c305f web: show correct sync status when sync is disabled 2023-03-20 14:01:20 +05:00
Abdullah Atta
72db8724ec editor: improve gradient overlay on image 2023-03-20 14:00:50 +05:00
ammarahm-ed
b64cb22186 mobile: fix ipad orientation window sizing 2023-03-20 13:51:16 +05:00
Abdullah Atta
f5ca1220de web: add confirm & progress for subscription cancelling 2023-03-20 12:53:20 +05:00
Abdullah Atta
fb54125ee6 web: simplify subscription cancellation 2023-03-20 12:53:20 +05:00
Abdullah Atta
be957d058f core: add basic subscription handling 2023-03-20 12:53:20 +05:00
Abdullah Atta
54a3e3c927 mobile: remove react-native-actions-sheet patch 2023-03-20 12:47:23 +05:00
ammarahm-ed
aabbf7c1c1 mobile: fix tags not properly shown in sheet 2023-03-20 11:32:49 +05:00
ammarahm-ed
7773ba0804 mobile: finalize topic sheet ui 2023-03-20 11:28:53 +05:00
ammarahm-ed
9f812772fe mobile: fix tag text position 2023-03-20 11:28:53 +05:00
ammarahm-ed
75191afc3e mobile: fix image upload broken on ios 2023-03-20 11:28:53 +05:00
Abdulrehman-Jafer
1c25baccea web: set tab/window title to note title (#2127)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-19 12:24:55 +05:00
Abdullah Atta
b1d4ccc27e editor: fix dropdown needing 2 clicks to open 2023-03-18 12:28:08 +05:00
Abdulrehman-Jafer
0d714847f3 web: copy version on clicking the version item in settings (#2131)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2023-03-18 12:25:20 +05:00
Abdullah Atta
760d4abbd0 web: groups shouldn't be ignored in reminders list 2023-03-18 11:58:30 +05:00
Abdullah Atta
7c1bb33ea9 mobile: update package-lock.json 2023-03-18 11:58:30 +05:00
ammarahm-ed
5ba8ce549e mobile: disable update check during tests 2023-03-18 11:58:30 +05:00
ammarahm-ed
a449c614ee mobile: make all tests pass 2023-03-18 11:58:30 +05:00
Abdullah Atta
12e6688f48 web: nuke Remove from topic menu item 2023-03-18 11:58:30 +05:00
Abdullah Atta
da47ef159b web: fix multiselect for notebook actions on note 2023-03-18 11:58:30 +05:00
Abdullah Atta
efc678d3cd core: removeFromAllNotebooks should unlink notebook relations too 2023-03-18 11:58:30 +05:00
ammarahm-ed
d7a126ab85 mobile: add test ids 2023-03-18 11:58:30 +05:00
ammarahm-ed
bc9c976c95 mobile: fix header title size in topic 2023-03-18 11:58:30 +05:00
ammarahm-ed
26af23771d mobile: fix refresh control colors 2023-03-18 11:58:30 +05:00
ammarahm-ed
590f2d80d3 mobile: add remove from notebook with multi-select 2023-03-18 11:58:30 +05:00
ammarahm-ed
fdeb34ff0a mobile: improve wording of delete notebook dialog 2023-03-18 11:58:30 +05:00
Abdullah Atta
ec76be776c web: make topics optional in importer 2023-03-18 11:58:30 +05:00
Abdullah Atta
4de5691428 web: clarify notebook/topic delete dialog wording 2023-03-18 11:58:30 +05:00
ammarahm-ed
ae5b339251 mobile: change color of sort direction button 2023-03-18 11:58:30 +05:00
ammarahm-ed
a6021943e1 mobile: top level notes 2023-03-18 11:58:30 +05:00
Abdullah Atta
765fcafc38 web: fix all tests & improve notebook/topic deletion 2023-03-18 11:58:30 +05:00
Abdullah Atta
7a53881eb3 web: remove unnecessary console.log 2023-03-18 11:58:30 +05:00
Abdullah Atta
854f22f373 web: improve how reminder bar looks when pane is resized 2023-03-18 11:58:30 +05:00
Abdullah Atta
0566f98323 web: disable horizontal scroll in list container 2023-03-18 11:58:30 +05:00
Abdullah Atta
435a358c4c web: switch to compact ui for tag items 2023-03-18 11:58:30 +05:00
Abdullah Atta
3f55422d45 web: add support for top-level notes in notebooks 2023-03-18 11:58:30 +05:00
Abdullah Atta
9804c1fbb1 core: include notebook notes in total notes count 2023-03-18 11:58:30 +05:00
Abdullah Atta
c3b8c4b630 mobile: update help tips 2023-03-18 11:58:30 +05:00
Abdullah Atta
8c4d8622d8 web: replace svg placeholders with helpful tips 2023-03-18 11:58:30 +05:00
Abdullah Atta
86770b32bb core: add support for linking notes with notebooks
this basically allows notes to exist at the top-level in a notebook
2023-03-18 11:58:30 +05:00
Abdullah Atta
74c86c1e5f editor: table options not visible on old tables 2023-03-17 22:44:29 +05:00
ammarahm-ed
1538b74c63 mobile: make never option premium in trash interval 2023-03-17 22:07:19 +05:00
Abdullah Atta
5934c518e6 web: refactor OptionItem to take objects instead of strings 2023-03-17 22:07:19 +05:00
alihamuh
71dfd0cf9a web: changed config to db for trashClean up interval 2023-03-17 22:07:19 +05:00
ammarahm-ed
984bf1f15f core: remove trash duration fn 2023-03-17 22:07:19 +05:00
ammarahm-ed
a14949b75a mobile: remove pro check 2023-03-17 22:07:19 +05:00
ammarahm-ed
7bbe138d91 mobile: cleanup 2023-03-17 22:07:19 +05:00
ammarahm-ed
65dea70ae5 mobile: add trash interval settings 2023-03-17 22:07:19 +05:00
ammarahm-ed
a07c8b9e11 core: add trash cleanup duration settings 2023-03-17 22:07:19 +05:00
Abdullah Atta
69b82c61f8 web: change wording of trash settings 2023-03-17 22:07:19 +05:00
alihamuh
fb8df74242 web: added trash setting to control delete time 2023-03-17 22:07:19 +05:00
Abdullah Atta
3674594832 core: do a healthcheck before logging out user on session revoke 2023-03-17 17:03:00 +05:00
Ammar Ahmed
1ee24cde07 editor: add support for attachment previews (#2123)
This adds support for basic attachment preview support. Currently only image previews are supported on mobile.

    Fixed image downloading not working on mobile
    Added image preview support on mobile with support for loading full quality images in full screen.
    Added Attachment preview logic in editor, so in future we can support preview for other files such as audio & video
2023-03-17 14:43:53 +05:00
Abdullah Atta
93baed35f4 editor: fix code-block tests to use our own getChangedNodes fn
(cherry picked from commit ff246fb9092ea002b76738f3f7082b5c5f10ea6c)
2023-03-13 12:41:29 +05:00
Abdullah Atta
3e1420d496 editor: get rid of @remirror/core-utils
(cherry picked from commit c74f15c86b2f25c9f572ea74f47243e1b52f9c1f)
2023-03-13 12:41:02 +05:00
Abdullah Atta
ebc4324ef1 web: upgrade @notesnook-importer/core to v1.6.0 2023-03-10 18:24:59 +05:00
Abdullah Atta
6ba04f4731 editor: move cell properties button outside of table settings
this fixes #1410
2023-03-06 16:44:09 +05:00
Abdulrehman-Jafer
7359e23b65 web: fix autocomplete caret color in all inputs (#2073)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-06 15:25:14 +05:00
Abdullah Atta
01387bae94 editor: update code-block test snapshost 2023-03-06 15:15:52 +05:00
Abdullah Atta
df1d61ecdf ci: build editor before running tests 2023-03-06 15:15:52 +05:00
Abdullah Atta
cb644dbea6 editor: disable wrapping for all lines in codeblocks 2023-03-06 15:15:52 +05:00
Abdullah Atta
fc5ea0ad51 editor: add tests for codeblock 2023-03-06 15:15:52 +05:00
Abdullah Atta
8108dbae8a editor: move codeblock language loading into highlighter plugin 2023-03-06 15:15:52 +05:00
Abdullah Atta
b14a9d6eca editor: improve code block detection when pasting 2023-03-06 15:15:52 +05:00
Abdullah Atta
2f4a7f5410 editor: migrate tests to vitest 2023-03-06 15:15:52 +05:00
Abdulrehman-Jafer
dfa718f8ea web: fix autocomplete text color for all inputs (#2057)
Signed-off-by: Abdulrehman-Jafer <abdulrehmanjaferwork01233@gmail.com>
2023-03-06 14:53:35 +05:00
Abdullah Atta
f842700192 web: upgrade @notesnook/importer to v1.5.4 2023-03-03 20:37:33 +05:00
ammarahm-ed
953d0295dc mobile: update release notes 2023-03-02 13:42:25 +05:00
Ammar Ahmed
836f607aa0 mobile: fix reminders not working on android (#2038) 2023-03-02 13:38:58 +05:00
ammarahm-ed
2c6f205ea9 mobile: exclude deps with "android"
keyword on ios
2023-02-28 16:56:16 +05:00
ammarahm-ed
8154dd1830 mobile: release v2.4.5 2023-02-28 16:41:37 +05:00
ammarahm-ed
686674c855 mobile: fix fdroid build check 2023-02-28 16:32:45 +05:00
Abdullah Atta
8e5d931be1 web: bump version to 2.4.4 2023-02-28 16:18:48 +05:00
Abdullah Atta
46509a04b0 editor: all lists should have the same spacing 2023-02-28 16:18:48 +05:00
Ammar Ahmed
d6e189134c fix ci build (#2027) 2023-02-28 15:21:48 +05:00
ammarahm-ed
0a05780e4d mobile: do not apped date to file name
in bulk exports
2023-02-28 15:18:06 +05:00
Abdullah Atta
1671c99614 web: hide backup reminder on download backup 2023-02-28 14:47:52 +05:00
ammarahm-ed
b5c8afe8f7 mobile: handle same filename 2023-02-28 14:09:09 +05:00
Abdullah Atta
7e4ab8e14b web: sanitize filename for single note export 2023-02-28 14:09:09 +05:00
Abdullah Atta
35210ad02b web: remove unnecessary console.log stmt 2023-02-28 14:09:09 +05:00
Abdullah Atta
9f853f0b36 web: uniquify dup filenames when creating zip 2023-02-28 14:09:09 +05:00
Abdullah Atta
559698c946 web: don't replace non-ascii chars in filename sanitizer 2023-02-28 14:09:09 +05:00
ammarahm-ed
50c05efb48 mobile: fix platform check 2023-02-28 14:07:00 +05:00
Abdullah Atta
94b9d3590d web: add resistence to submenu closing
this takes care of accidentally closing sub menus when moving the mouse
over to it.
2023-02-28 13:45:06 +05:00
Abdullah Atta
092016f942 editor: do not build test files 2023-02-28 12:44:11 +05:00
Abdullah Atta
24c899e709 web: ensure that images get downloaded on realtime sync 2023-02-28 12:11:13 +05:00
Abdullah Atta
e53cc648f0 core: do not unsub all once events on publish
this was a stupid bug where publishing any type of event would
unsubscribe all events that had once set to true.
2023-02-28 12:11:13 +05:00
ammarahm-ed
4c541f3995 mobile: load compressed images in editor 2023-02-28 12:11:13 +05:00
ammarahm-ed
f56cbfc142 mobile: fix loading images in realtime sync 2023-02-28 12:11:13 +05:00
Ammar Ahmed
c21c4f1117 mobile: fix color-scheme not change on toggle (#2024)
system theme
2023-02-28 11:47:04 +05:00
Abdullah Atta
abf1f5c61c editor: add tests for backspace handling 2023-02-28 11:17:05 +05:00
Abdullah Atta
1d5b0793d0 editor: move list node types to a separate file to prevent require loop 2023-02-28 11:17:05 +05:00
Abdullah Atta
5ba72fdbd4 editor: adjust sub list indentation 2023-02-28 11:17:05 +05:00
alihamuh
c7d26d2444 editor: improve backspace handling with lists 2023-02-28 11:17:05 +05:00
Muhammad Ali
22c146e350 web: fix context menu crash due to incorrect topic references (#2016) 2023-02-28 10:19:26 +05:00
Ammar Ahmed
1cb0ebd4f8 mobile: fix scroll into view on ios (#2017) 2023-02-27 16:06:06 +05:00
Ammar Ahmed
eff2e475ff mobile: add oss licenses (#2018) 2023-02-27 16:05:47 +05:00
Abdullah Atta
aef475d8fc ci: fix workflow step name 2023-02-27 11:07:06 +05:00
Abdullah Atta
1ed2e55867 ci: add workflow for editor tests 2023-02-27 11:07:06 +05:00
Abdullah Atta
16ffec1f1e editor: set up integration tests 2023-02-27 11:07:06 +05:00
Abdullah Atta
912e97dc3c editor: add button to sort task list 2023-02-27 11:07:06 +05:00
Abdullah Atta
ca2f924417 ci: add config for DCO PR check 2023-02-27 11:05:06 +05:00
Muhammad Ali
468fba8fcf web: hide status bar in tablet mode (#2001) 2023-02-25 11:58:32 +05:00
Abdullah Atta
b948dcee22 editor: optimize clear all completed tasks action 2023-02-25 11:55:46 +05:00
Abdullah Atta
a633f4a3fe editor: task list should update when its items are toggled 2023-02-25 11:53:57 +05:00
alihamuh
aec1d601a3 web: added change default fontstyle feature in the settings 2023-01-18 13:23:39 +05:00
539 changed files with 43554 additions and 94776 deletions

View File

@@ -20,6 +20,7 @@ const SCOPES = [
"logger",
"theme",
"core",
"fs",
"clipper",
"config",
"ci",

View File

@@ -21,4 +21,7 @@ packages/editor/styles/
packages/editor/languages/
# editor mobile
packages/editor-mobile/build.bundle
packages/editor-mobile/build.bundle
# snapshots
tap-snapshots

4
.github/dco.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
allowRemediationCommits:
individual: true
require:
members: false

View File

@@ -50,7 +50,18 @@ jobs:
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Build apks for Github release
run: yarn release:android
@@ -91,15 +102,4 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk

View File

@@ -215,17 +215,17 @@ jobs:
- name: Build snap
if: inputs.publish-snap
run: |
npx electron-builder --linux snap -p never
npx electron-builder --linux snap:x64 -p never
working-directory: ./apps/web/desktop
- name: Build AppImage deb and rpm
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
npx electron-builder --linux AppImage deb rpm -p always
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
else
npx electron-builder --linux AppImage deb rpm -p never
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/web/desktop

57
.github/workflows/editor.tests.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Test @notesnook/editor
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "packages/editor/**"
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
pull_request:
types:
- "ready_for_review"
- "opened"
- "synchronize"
- "reopened"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
packages/crypto-worker/package-lock.json
packages/editor-mobile/package-lock.json
packages/editor/package-lock.json
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Build editor
run: npx nx build @notesnook/editor
- name: Run all @notesnook/editor tests
run: npx nx test @notesnook/editor

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ dist
nx-cloud.env
.idea
.eslintcache
.env.local
.env.local
.nyc_output

View File

@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
Ugh! Bugs!
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
@@ -30,7 +30,7 @@ Before you open a new feature request, please make sure it's not a duplicate. **
### Helping out in the issue tracker
There are always new issues getting opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
New issues are always opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
In addition to this, you can help out in the following ways:
@@ -62,7 +62,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
2. Run `npm run bootstrap` in the repository root.
3. If youve fixed a bug or added code that should be tested, add tests!
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`npm run test`).
5. Format your code with prettier (`npm run prettier`).
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
@@ -131,11 +131,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnbs Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
## Git Branch Organization
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We dont use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
1. All tests must be passing at all times
2. There should be as few breaking changes as possible

View File

@@ -10,11 +10,11 @@
## Overview
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
## Developer guide

View File

@@ -19,9 +19,9 @@
## Build instructions
> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
### Setting up development environment
### Setting up the development environment
Requirements:
@@ -57,7 +57,7 @@ npm install
### Running the app on Android
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
```bash
npm run start:android
@@ -78,11 +78,11 @@ npm run start:ios
## Developer guide
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
### The tech stack
We try to keep the stack as lean as possible
We try to keep the stack as lean as possible:
1. React Native
2. Typescript/Javascript
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible
The app codebase is distributed over two primary directories. `native/` and `app/`.
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
- `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
- `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
- `hooks/`: Hooks for different app logic
- `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `screens`: Navigator screens.
- `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
- `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
- `utils`: General purpose stuff such as constant values, utility functions etc.
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
- `utils`: General purpose stuff such as constant values, utility functions, etc.
There are several other folders at the root:
@@ -115,11 +115,11 @@ There are several other folders at the root:
### Running the tests
When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
### Android
To run the tests on android, you will need to create an emulator device on your system:
To run the tests on Android, you will need to create an emulator device on your system:
```
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
Once you have created an emulator device, build the android apks
Once you have created an emulator device, build the Android apks:
```
npm run build:android
```
Finally run the tests
Finally, run the tests:
```
npm run test:android
@@ -141,9 +141,9 @@ npm run test:android
### iOS
To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
```
brew tap wix/brew
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
npm run build:ios
```
Finally run the tests:
Finally, run the tests:
```
npm run test:ios

View File

@@ -31,14 +31,6 @@ import { TipManager } from "./services/tip-manager";
import { useUserStore } from "./stores/use-user-store";
import { View } from "react-native";
import { useState } from "react";
import NetInfo from "@react-native-community/netinfo";
NetInfo.configure({
reachabilityUrl: "https://notesnook.com",
reachabilityTest: (response) => {
if (!response) return false;
return response?.status >= 200 && response?.status < 300;
}
});
SettingsService.init();
SettingsService.checkOrientation();

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -22,10 +22,17 @@ import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { generateSecureRandom } from "react-native-securerandom";
import Sodium from "@ammarahmed/react-native-sodium";
import { MMKV } from "./mmkv";
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
},
android: {}
});
@@ -39,12 +46,27 @@ export async function deriveCryptoKey(name, data) {
credentials.key,
KEYSTORE_CONFIG
);
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
return credentials.key;
} catch (e) {
console.error(e);
}
}
async function upgradeIOSKeychain(username, password) {
if (Platform.OS !== "ios") return;
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
await Keychain.setInternetCredentials(
"notesnook",
username,
password,
KEYSTORE_CONFIG
);
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
}
}
export async function getCryptoKey(_name) {
try {
if (await Keychain.hasInternetCredentials("notesnook")) {
@@ -52,6 +74,9 @@ export async function getCryptoKey(_name) {
"notesnook",
KEYSTORE_CONFIG
);
// upgrades ios keychain to use accessGroups
// so we have access to keychain in share extension.
await upgradeIOSKeychain(credentials.username, credentials.password);
return credentials.password;
} else {
return null;

View File

@@ -68,14 +68,3 @@ db.host(
ISSUES_HOST: "https://issues.streetwriters.co"
}
);
export async function loadDatabase() {
// if (!DB) {
// let module = await import(/* webpackChunkName: "notes-core" */ 'notes-core/api/index');
// DB = module.default;
// }
// db = new DB(Storage, Platform.OS === 'ios' ? EventSource : AndroidEventSource, filesystem);
// if (DOMParser) {
// await DOMParser.prepare();
// }
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import {
decrypt,
deriveCryptoKey,

View File

@@ -0,0 +1,50 @@
/*
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 { Dimensions, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "react-native-blob-util";
/**
* Scale down & compress images to screen width
* for loading in editor.
* @returns
*/
export async function compressToBase64(path, type) {
const { width, scale } = Dimensions.get("window");
const response = await ImageResizer.createResizedImage(
path,
width * scale,
9999,
type,
80,
0,
undefined,
true,
{
mode: "contain",
onlyScaleDown: true
}
);
const base64 = await RNFetchBlob.fs.readFile(
Platform.OS === "ios" ? response.uri?.replace("file://", "") : response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
return base64;
}

View File

@@ -17,19 +17,171 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/utils/filename";
import React from "react";
import { Platform } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir } from "./utils";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
export default async function downloadAttachment(hash, global = true) {
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export function downloadAllAttachments() {
const attachments = db.attachments.all;
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
if (!attachments || !attachments.length) return;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder))
await RNFetchBlob.fs.unlink(zipSourceFolder);
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = attachments[i];
const hash = attachment.metadata.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
});
} catch (e) {
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Fail,
reason: e
});
console.log("Error downloading attachment", e);
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
console.log("Error zipping attachments", e);
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined
}
) {
let attachment = db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
@@ -37,16 +189,23 @@ export default async function downloadAttachment(hash, global = true) {
}
let folder = {};
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
if (!options.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
}
try {
await db.fs.downloadFile(
console.log(
"starting download attachment",
attachment.metadata.hash,
options.groupId
);
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (
@@ -54,6 +213,11 @@ export default async function downloadAttachment(hash, global = true) {
)
return;
let filename = getFileNameWithExtension(
attachment.metadata.filename,
attachment.metadata.type
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
@@ -63,46 +227,51 @@ export default async function downloadAttachment(hash, global = true) {
hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType,
mime: attachment.metadata.type,
fileName: attachment.metadata.filename,
uri: folder.uri,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize
};
let fileUri = await Sodium.decryptFile(key, info, "file");
ToastEvent.show({
heading: "Download successful",
message: attachment.metadata.filename + " downloaded",
type: "success"
});
let fileUri = await Sodium.decryptFile(
key,
info,
options.cache ? "cache" : "file"
);
if (attachment.dateUploaded) {
if (!options.silent) {
ToastEvent.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
});
}
if (
attachment.dateUploaded &&
!isImage(attachment.metadata?.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios") {
fileUri = folder.uri + `/${attachment.metadata.filename}`;
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
}
if (!options.silent) {
presentSheet({
title: "File downloaded",
paragraph: `${filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.metadata.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
console.log("saved file uri: ", fileUri);
presentSheet({
title: "File downloaded",
paragraph: `${attachment.metadata.filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.metadata.hash,
component: (
<ShareComponent
uri={fileUri}
name={attachment.metadata.filename}
padding={12}
/>
)
});
return fileUri;
} catch (e) {
console.log("download attachment error: ", e);
@@ -112,5 +281,8 @@ export default async function downloadAttachment(hash, global = true) {
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.metadata.hash);
if (options.throwError) {
throw e;
}
}
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "@notesnook/core/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";

View File

@@ -23,14 +23,16 @@ import {
deleteFile,
exists,
readEncrypted,
writeEncrypted
writeEncryptedBase64,
hashBase64
} from "./io";
import { uploadFile } from "./upload";
import { cancelable } from "./utils";
export default {
readEncrypted,
writeEncrypted,
writeEncryptedBase64,
hashBase64,
uploadFile: cancelable(uploadFile),
downloadFile: cancelable(downloadFile),
deleteFile,

View File

@@ -19,25 +19,54 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Platform } from "react-native";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function readEncrypted(filename, key, cipherData) {
let path = `${cacheDir}/${filename}`;
try {
let exists = await RNFetchBlob.fs.exists(path);
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists =
(await RNFetchBlob.fs.exists(path)) ||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
if (!exists) {
return false;
}
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
let output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename
hash: filename,
appGroupId: IOS_APPGROUPID
},
cipherData.outputType === "base64" ? "base64" : "text"
cipherData.outputType === "base64"
? isPng || isJpeg
? "cache"
: "base64"
: "text"
);
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
const dCachePath = `${cacheDir}/${output}`;
output = await compressToBase64(
`file://${dCachePath}`,
isPng ? "PNG" : "JPEG"
);
}
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
@@ -46,17 +75,26 @@ export async function readEncrypted(filename, key, cipherData) {
}
}
export async function writeEncrypted(filename, { data, type, key }) {
console.log("file input: ", { type, key });
export async function hashBase64(data) {
const hash = await Sodium.hashFile({
type: "base64",
data,
uri: ""
});
return {
hash: hash,
type: "xxh64"
};
}
export async function writeEncryptedBase64({ data, key }) {
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
console.log(filepath);
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
return {
...output,

View File

@@ -17,10 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -35,9 +38,19 @@ export async function uploadFile(filename, data, cancelToken) {
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
const uploadUrl = await res.text();
if (!uploadUrl) throw new Error("Unable to resolve upload url");
let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
}
let request = RNFetchBlob.config({
IOSBackgroundTask: true
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
"PUT",
@@ -45,7 +58,7 @@ export async function uploadFile(filename, data, cancelToken) {
{
"content-type": ""
},
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
RNFetchBlob.wrap(uploadFilePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
@@ -63,7 +76,10 @@ export async function uploadFile(filename, data, cancelToken) {
if (result) {
let attachment = db.attachments.attachment(filename);
if (!attachment) return result;
if (!attachment.metadata.type.startsWith("image/")) {
if (
!isImage(attachment.metadata.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
}

View File

@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import RNFetchBlob from "rn-fetch-blob";
import * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
@@ -62,3 +64,22 @@ export function cancelable(operation) {
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
console.log(e, r);
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -148,7 +148,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
<Button
key={item.title}
title={item.title}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
type="gray"
onPress={() => onPress(item)}
width={null}

View File

@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
right: 0
}}
iconSize={24}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
style={{
borderRadius: 100,
paddingVertical: 0,

View File

@@ -102,15 +102,21 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastEvent.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
}
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
setAttachments([...db.attachments.all]);
setLoading({
name: null
@@ -209,7 +215,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
>
<Paragraph
size={SIZE.xs + 1}
size={SIZE.xs}
style={{
marginRight: 10
}}
@@ -221,7 +227,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
style={{
marginRight: 10
}}
size={SIZE.xs + 1}
size={SIZE.xs}
color={colors.icon}
>
{formatBytes(attachment.length)}
@@ -232,7 +238,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
style={{
marginRight: 10
}}
size={SIZE.xs + 1}
size={SIZE.xs}
color={colors.icon}
>
{attachment.noteIds.length} note
@@ -248,7 +254,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
context: "local"
});
}}
size={SIZE.xs + 1}
size={SIZE.xs}
color={colors.icon}
>
{attachment.metadata.hash}
@@ -304,7 +310,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
key={item.id}
>
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
</PressableButton>
))}
</>

View File

@@ -22,7 +22,6 @@ import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { formatBytes } from "../../utils";
import { SIZE } from "../../utils/size";
@@ -37,20 +36,26 @@ function getFileExtension(filename) {
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
export const AttachmentItem = ({
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading
}) => {
const colors = useThemeStore((state) => state.colors);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const encryptionProgress = useAttachmentStore(
(state) => state.encryptionProgress
);
const onPress = () => {
if (!pressable) return;
Actions.present(attachment, setAttachments, attachment.metadata.hash);
};
return (
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -113,20 +118,22 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
{attachment.metadata.filename}
</Paragraph>
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
</View>
</View>
{currentProgress || encryptionProgress || encryption ? (
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption) return;
if (encryption || !pressable) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
@@ -139,13 +146,7 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={
encryptionProgress
? encryptionProgress
: currentProgress?.value
? currentProgress?.value / 100
: 0
}
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
showsText
textStyle={{
fontSize: 10

View File

@@ -0,0 +1,273 @@
/*
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, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { presentSheet } from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeStore } from "../../stores/use-theme-store";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const colors = useThemeStore((state) => state.colors);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
value: 0,
statusText: "Download started... Please wait"
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef();
const onDownload = async () => {
update({
disableClosing: true
});
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
attachments,
(progress, statusText) => setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update({
disableClosing: false
});
};
const cancel = async () => {
update({
disableClosing: false
});
canceled.current = true;
console.log(groupId.current, "canceling groupId downloads");
await db.fs.cancel(groupId.current);
setDownloading(false);
groupId.current = null;
};
const successResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 1) results.push(db.attachments.attachment(key));
}
return results;
};
const failedResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 0) results.push(db.attachments.attachment(key));
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
}
return (
<View
style={{
alignItems: "center",
width: "100%",
paddingVertical: 12,
paddingHorizontal: 12
}}
>
<Heading>
{downloading
? "Downloading attachments"
: result?.size
? "Downloaded attachments"
: "Download attachments"}
</Heading>
{downloading ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{progress.statusText}
</Paragraph>
) : result?.size ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{getResultText()}
</Paragraph>
) : (
<Paragraph
style={{
textAlign: "center"
}}
>
Are you sure you want to download all attachments
{isNote ? " of this note?" : "?"}
</Paragraph>
)}
{downloading ? (
<View
style={{
width: 200,
marginTop: 10
}}
>
<ProgressBarComponent
height={5}
width={null}
animated={true}
useNativeDriver
progress={progress.value ? progress.value / attachments.length : 0}
unfilledColor={colors.nav}
color={colors.accent}
borderWidth={0}
/>
</View>
) : null}
<FlatList
style={{
maxHeight: 300,
width: "100%",
minHeight: 60,
backgroundColor: colors.nav,
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments : []}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 60
}}
>
<Paragraph color={colors.icon}>No downloads in progress.</Paragraph>
</View>
}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<AttachmentItem
attachment={item}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
/>
);
}}
/>
{result?.size ? (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={close}
type="accent"
title="Done"
/>
) : !downloading ? (
<View
style={{
flexDirection: "row",
width: "100%",
marginTop: 20
}}
>
<Button
style={{
flex: 1,
borderRadius: 100,
marginRight: 5
}}
onPress={close}
type="grayBg"
title="No"
/>
<Button
style={{
flex: 1,
borderRadius: 100,
marginLeft: 5
}}
onPress={onDownload}
type="accent"
title="Yes"
/>
</View>
) : (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={cancel}
type="error"
title="Cancel"
/>
)}
</View>
);
};
DownloadAttachments.present = (context, attachments, isNote) => {
presentSheet({
context: context,
component: (ref, close, update) => (
<DownloadAttachments
close={close}
attachments={attachments}
isNote={isNote}
update={update}
/>
)
});
};
export default DownloadAttachments;

View File

@@ -17,82 +17,61 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import React, { useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { presentSheet } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import {
eCloseAttachmentDialog,
eOpenAttachmentsDialog
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import DialogHeader from "../dialog/dialog-header";
import { Toast } from "../toast";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import SheetWrapper from "../ui/sheet";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
export const AttachmentDialog = () => {
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/utils/filename";
export const AttachmentDialog = ({ note }) => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const actionSheetRef = useRef();
const [attachments, setAttachments] = useState([]);
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent(eOpenAttachmentsDialog, open);
eSubscribeEvent(eCloseAttachmentDialog, close);
return () => {
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
eUnSubscribeEvent(eCloseAttachmentDialog, close);
};
}, [visible]);
const open = (data) => {
if (data?.id) {
setNote(data);
let _attachments = db.attachments.ofNote(data.id, "all");
setAttachments(_attachments);
} else {
setAttachments([...db.attachments.all]);
}
setVisible(true);
};
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
actionSheetRef.current?.hide();
setVisible(false);
};
const [currentFilter, setCurrentFilter] = useState("all");
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments([...db.attachments.all]);
setAttachments([...attachments]);
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
db.attachments.all,
attachments,
attachmentSearchValue.current
);
if (results.length === 0) return;
@@ -104,107 +83,239 @@ export const AttachmentDialog = () => {
<AttachmentItem setAttachments={setAttachments} attachment={item} />
);
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onClose={async () => {
setVisible(false);
const onCheck = async () => {
setLoading(true);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
await db.attachments.markAsFailed(attachment.id, null);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
setLoading(false);
};
const attachmentTypes = [
{
title: "All",
filterBy: "all"
},
{
title: "Images",
filterBy: "images"
},
{
title: "Documents",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
}
];
const filterAttachments = (type) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
switch (type) {
case "all":
return attachments;
case "images":
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
case "video":
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
case "audio":
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
case "documents":
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
}
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
}}
>
<Toast context="local" />
<SheetProvider context="attachments-list" />
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<DialogHeader
title={note ? "Attachments" : "Manage attachments"}
paragraph="Tap on an attachment to view properties"
button={{
title: "Check all",
type: "grayAccent",
loading: loading,
onPress: async () => {
setLoading(true);
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(
attachment.metadata.hash
);
if (result.failed) {
db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
db.attachments.markAsFailed(attachment.id, null);
}
setAttachments([...db.attachments.all]);
}
setLoading(false);
}
}}
/>
<Seperator />
{!note ? (
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
) : null}
<Heading>Attachments</Heading>
<FlatList
nestedScrollEnabled
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon name="attachment" size={60} color={colors.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
data={attachments}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
<Paragraph
color={colors.icon}
size={SIZE.xs}
<View
style={{
textAlign: "center",
marginTop: 10
flexDirection: "row"
}}
>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{" "}All attachments are end-to-end encrypted.
</Paragraph>
{loading ? (
<ActivityIndicator
style={{
height: 40,
width: 40,
marginRight: 10
}}
size={SIZE.lg}
/>
) : (
<IconButton
name="check-all"
customStyle={{
height: 40,
width: 40,
marginRight: 10
}}
color={colors.pri}
size={SIZE.lg}
onPress={onCheck}
/>
)}
<IconButton
name="download"
customStyle={{
height: 40,
width: 40
}}
color={colors.pri}
onPress={() => {
DownloadAttachments.present(
"attachments-list",
attachments,
!!note
);
}}
size={SIZE.lg}
/>
</View>
</View>
</SheetWrapper>
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
<FlatList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={10}
initialNumToRender={10}
windowSize={5}
stickyHeaderIndices={[0]}
ListHeaderComponent={
<ScrollView
style={{
width: "100%",
height: 50,
flexDirection: "row",
backgroundColor: colors.bg
}}
contentContainerStyle={{
minWidth: "100%"
}}
horizontal
>
{attachmentTypes.map((item) => (
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
style={{
borderRadius: 0,
borderBottomWidth: 1,
flexGrow: 1,
borderBottomColor:
currentFilter !== item.filterBy
? "transparent"
: colors.accent
}}
onPress={() => {
setCurrentFilter(item.filterBy);
setAttachments(filterAttachments(item.filterBy));
}}
/>
))}
</ScrollView>
}
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon name="attachment" size={60} color={colors.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
data={attachments}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 10
}}
>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{" "}All attachments are end-to-end encrypted.
</Paragraph>
</View>
);
};
AttachmentDialog.present = (note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
import { View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
@@ -75,7 +75,7 @@ const AuthModal = () => {
return !visible ? null : (
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
statusBarTranslucent={true}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
@@ -101,7 +101,7 @@ const AuthModal = () => {
<View
style={{
position: "absolute",
top: Platform.OS === "ios" ? insets.top : 0,
top: insets.top,
zIndex: 999,
flexDirection: "row",
alignItems: "center",
@@ -126,11 +126,11 @@ const AuthModal = () => {
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title="Skip for now"
title="Skip"
onPress={() => {
hideAuth();
}}
iconSize={20}
iconSize={16}
type="gray"
iconPosition="right"
icon="chevron-right"

View File

@@ -18,9 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { TouchableOpacity, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
@@ -30,13 +29,12 @@ import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SVG } from "./background";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login";
import { useSettingStore } from "../../stores/use-setting-store";
const LoginSteps = {
emailAuth: 1,
@@ -64,6 +62,7 @@ export const Login = ({ changeMode }) => {
await sleep(500);
Progress.present();
});
const deviceMode = useSettingStore((state) => state.deviceMode);
useEffect(() => {
async () => {
@@ -82,62 +81,70 @@ export const Login = ({ changeMode }) => {
<>
<ForgotPassword />
<SheetProvider context="two_factor_verify" />
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
minHeight: "100%"
}}
>
<View
style={{
height: 250,
overflow: "hidden"
}}
>
<SvgView
src={SVG(colors.night ? colors.icon : "black")}
height={700}
/>
</View>
<View
style={{
width: "100%",
justifyContent: "center",
alignSelf: "center",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15
flex: 0.45,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.nav,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%"
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.nav,
borderRadius: 2
}}
/>
</View>
<Heading
style={{
textAlign: "center"
marginBottom: 25,
marginTop: 10
}}
size={30}
color={colors.heading}
extraBold
size={SIZE.xxl}
>
Welcome back!
Login to your {"\n"}account
</Heading>
<Paragraph
style={{
textDecorationLine: "underline",
textAlign: "center",
marginTop: 5
}}
onPress={() => {
if (loading) return;
changeMode(1);
}}
size={SIZE.md}
>
{"Don't have an account? Sign up"}
</Paragraph>
</View>
<View
style={{
width: DDS.isTab
@@ -147,10 +154,9 @@ export const Login = ({ changeMode }) => {
: focused
? "100%"
: "99.9%",
padding: 12,
backgroundColor: colors.bg,
flexGrow: 1,
alignSelf: "center"
alignSelf: "center",
paddingHorizontal: 20
}}
>
<Input
@@ -217,28 +223,22 @@ export const Login = ({ changeMode }) => {
<View
style={{
marginTop: 25,
alignSelf: "center"
marginTop: 25
}}
>
<Button
style={{
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
style={{
width: 250,
borderRadius: 100
}}
fontSize={SIZE.md}
type="accent"
title={
loading
? null
: step === LoginSteps.emailAuth
? "Login"
: "Continue"
}
title={!loading ? "Continue" : null}
/>
{step === LoginSteps.passwordAuth && (
@@ -261,9 +261,34 @@ export const Login = ({ changeMode }) => {
type="errorShade"
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
Don't have an account?{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.accent }}
>
Sign up
</Paragraph>
</Paragraph>
</TouchableOpacity>
) : null}
</View>
</View>
</Animated.View>
</View>
</>
);
};

View File

@@ -44,8 +44,10 @@ import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
function getObfuscatedEmail(email) {
if (!email) return null;
return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) {
if (!email) return "";
const [username, provider] = email.split("@");
if (username.length === 1) return `****@${provider}`;
return email.replace(/(.{1})(.*)(?=@)/, function (gp1, gp2, gp3) {
for (let i = 0; i < gp3.length; i++) {
gp2 += "*";
}

View File

@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { Dimensions, View } from "react-native";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import { TouchableOpacity, View } from "react-native";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
@@ -32,11 +31,10 @@ import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SVG } from "./background";
import { hideAuth } from "./common";
import { useSettingStore } from "../../stores/use-setting-store";
export const Signup = ({ changeMode, trial }) => {
const colors = useThemeStore((state) => state.colors);
@@ -50,6 +48,7 @@ export const Signup = ({ changeMode, trial }) => {
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
const deviceMode = useSettingStore((state) => state.deviceMode);
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
@@ -96,69 +95,77 @@ export const Signup = ({ changeMode, trial }) => {
return (
<>
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
minHeight: "100%"
}}
>
<View
style={{
height: 250,
overflow: "hidden"
flex: 0.35,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.nav,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%"
}}
>
<SvgView
src={SVG(colors.night ? colors.icon : "black")}
height={700}
/>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.nav,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={SIZE.xxl}
>
Create your {"\n"}account
</Heading>
</View>
<View
style={{
width: "100%",
justifyContent: "center",
alignSelf: "center",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: Dimensions.get("window").height < 700 ? -75 : 15
}}
>
<Heading
style={{
textAlign: "center"
}}
size={30}
color={colors.heading}
>
Create your account
</Heading>
<Paragraph
style={{
textDecorationLine: "underline",
textAlign: "center"
}}
onPress={() => {
if (loading) return;
changeMode(0);
}}
size={SIZE.md}
>
Already have an account? Log in
</Paragraph>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
padding: 12,
paddingHorizontal: 20,
backgroundColor: colors.bg,
flexGrow: 1,
alignSelf: "center"
alignSelf: "center",
flex: 0.6
}}
>
<Input
@@ -217,37 +224,13 @@ export const Signup = ({ changeMode, trial }) => {
validationType="confirmPassword"
customValidator={() => password.current}
placeholder="Confirm password"
marginBottom={5}
marginBottom={12}
onSubmit={signup}
/>
<View
style={{
marginTop: 25,
alignSelf: "center"
}}
>
<Button
style={{
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
signup();
}}
type="accent"
title={loading ? null : "Agree and continue"}
/>
</View>
<Paragraph
style={{
textAlign: "center",
position: "absolute",
bottom: 0,
alignSelf: "center",
marginBottom: 20
marginBottom: 25
}}
size={SIZE.xs}
color={colors.icon}
@@ -263,7 +246,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.accent}
>
terms of service{" "}
Terms of Service{" "}
</Paragraph>
and{" "}
<Paragraph
@@ -276,11 +259,46 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.accent}
>
privacy policy.
</Paragraph>
Privacy Policy.
</Paragraph>{" "}
You also agree to recieve marketing emails from us which you can
opt-out of from app settings.
</Paragraph>
<Button
title={!loading ? "Continue" : null}
type="accent"
loading={loading}
onPress={signup}
fontSize={SIZE.md}
style={{
marginRight: 12,
width: 250,
borderRadius: 100
}}
/>
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(0);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
Already have an account?{" "}
<Paragraph size={SIZE.xs + 1} style={{ color: colors.accent }}>
Login
</Paragraph>
</Paragraph>
</TouchableOpacity>
</View>
</Animated.View>
</View>
</>
);
};

View File

@@ -21,7 +21,6 @@ import React from "react";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { AnnouncementDialog } from "../announcements";
import { AttachmentDialog } from "../attachments";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
@@ -34,13 +33,10 @@ import MergeConflicts from "../merge-conflicts";
import PremiumDialog from "../premium";
import { Expiring } from "../premium/expiring";
import SheetProvider from "../sheet-provider";
import { AddNotebookSheet } from "../sheets/add-notebook";
import AddToNotebookSheet from "../sheets/add-to";
import ManageTagsSheet from "../sheets/manage-tags";
import PublishNoteSheet from "../sheets/publish-note";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => {
const colors = useThemeStore((state) => state.colors);
@@ -51,7 +47,6 @@ const DialogProvider = () => {
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<AddNotebookSheet colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -61,15 +56,12 @@ const DialogProvider = () => {
<RestoreDataSheet />
<ResultDialog />
<VaultDialog colors={colors} />
<AddToNotebookSheet colors={colors} />
<RateAppSheet />
<ImagePreview />
<PublishNoteSheet />
<ManageTagsSheet />
<AttachmentDialog />
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
</>
);
};

View File

@@ -35,6 +35,7 @@ import BaseDialog from "./base-dialog";
import DialogButtons from "./dialog-buttons";
import DialogHeader from "./dialog-header";
import { useCallback } from "react";
import { Button } from "../ui/button";
export const Dialog = ({ context = "global" }) => {
const colors = useThemeStore((state) => state.colors);
@@ -54,7 +55,11 @@ export const Dialog = ({ context = "global" }) => {
input: false,
inputPlaceholder: "Enter some text",
defaultValue: "",
disableBackdropClosing: false
disableBackdropClosing: false,
check: {
info: "Check",
type: "transparent"
}
});
useEffect(() => {
@@ -87,6 +92,7 @@ export const Dialog = ({ context = "global" }) => {
if (data.context !== context) return;
setDialogInfo(data);
setVisible(true);
setInputValue(data.defaultValue);
},
[context]
);
@@ -137,6 +143,9 @@ export const Dialog = ({ context = "global" }) => {
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
style={{
minHeight: 0
}}
/>
<Seperator half />
@@ -163,6 +172,28 @@ export const Dialog = ({ context = "global" }) => {
</View>
) : null}
{dialogInfo.check ? (
<>
<Button
onPress={() => {
setInputValue(!inputValue);
}}
icon={
inputValue
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
style={{
justifyContent: "flex-start"
}}
height={35}
width="100%"
title={dialogInfo.check.info}
type={inputValue ? dialogInfo.check.type : "gray"}
/>
</>
) : null}
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={dialogInfo.positivePress && onPressPositive}

View File

@@ -22,6 +22,7 @@ import { View } from "react-native";
import { useMenuStore } from "../../../stores/use-menu-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
@@ -30,6 +31,7 @@ import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddTopicDialog,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { sleep } from "../../../utils/time";
@@ -40,6 +42,7 @@ import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { Toast } from "../../toast";
import { useRelationStore } from "../../../stores/use-relation-store";
export class AddTopicDialog extends React.Component {
constructor(props) {
@@ -59,14 +62,12 @@ export class AddTopicDialog extends React.Component {
addNewTopic = async () => {
try {
this.setState({ loading: true });
if (!this.title || this.title?.trim() === "") {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
this.setState({ loading: false });
return;
}
@@ -78,10 +79,13 @@ export class AddTopicDialog extends React.Component {
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
}
this.setState({ loading: false });
this.close();
Navigation.queueRoutesForUpdate("Notebooks", "Notebook", "TopicNotes");
useMenuStore.getState().setMenuPins();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
} catch (e) {
console.error(e);
}
@@ -177,7 +181,6 @@ export class AddTopicDialog extends React.Component {
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
loading={this.state.loading}
/>
</DialogContainer>
<Toast context="local" />

View File

@@ -0,0 +1,324 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import {
addOrientationListener,
removeOrientationListener
} from "react-native-orientation";
import Pdf from "react-native-pdf";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { cacheDir } from "../../../common/filesystem/utils";
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import { presentDialog } from "../../dialog/functions";
import SheetProvider from "../../sheet-provider";
import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { sleep } from "../../../utils/time";
import { MMKV } from "../../../common/database/mmkv";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
const attachmentSnapshotsKey = "___attachmentsnapshots";
const usePDFSnapshot = (attachment) => {
const snapshots = useRef(MMKV.getMap(attachmentSnapshotsKey) || {});
const snapshot = useRef(snapshots[attachment?.id]);
function saveSnapshot(ss) {
if (!attachment) return;
snapshots.current[attachment.id] = ss;
MMKV.setMap(attachmentSnapshotsKey, snapshots.current);
snapshot.current = snapshots.current[attachment.id];
}
return [snapshot, saveSnapshot];
};
const PDFPreview = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [pdfSource, setPDFSource] = useState();
const [loading, setLoading] = useState(false);
const [width, setWidth] = useState(WIN_WIDTH);
const insets = useGlobalSafeAreaInsets();
const [numPages, setNumPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const inputRef = useRef();
const pdfRef = useRef();
const [attachment, setAttachment] = useState(null);
const [password, setPassword] = useState("");
const [progress] = useAttachmentProgress(attachment);
const [snapshot, saveSnapshot] = usePDFSnapshot(attachment);
const snapshotValue = useRef(snapshot.current);
useEffect(() => {
eSubscribeEvent("PDFPreview", open);
return () => {
eUnSubscribeEvent("PDFPreview", open);
};
}, []);
const onOrientationChange = (o) => {
if (o.includes("LANDSCAPE")) {
setWidth(WIN_HEIGHT);
} else {
setWidth(WIN_WIDTH);
}
};
useEffect(() => {
addOrientationListener(onOrientationChange);
return () => {
removeOrientationListener(onOrientationChange);
};
}, []);
const open = async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.metadata.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
setPDFSource(null);
setVisible(false);
setPassword("");
};
const onError = async (error) => {
if (error?.message === "Password required or incorrect password.") {
await sleep(300);
presentDialog({
context: attachment?.metadata?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
title: "Decrypt",
paragraph: "Please input password to view pdf.",
positivePress: (value) => {
setTimeout(() => {
setPassword(value);
});
},
onClose: () => {
close();
}
});
}
};
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.metadata?.hash} />
<Dialog context={attachment?.metadata?.hash} />
<View
style={{
width: "100%",
height: "100%",
backgroundColor: "black"
}}
>
{loading ? (
<Animated.View
exiting={FadeOut}
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
indeterminate={!progress}
color={colors.accent}
borderColor="transparent"
progress={parseInt(progress?.value || "100") / 100}
/>
<Paragraph
style={{
marginTop: 10
}}
color={colors.light}
>
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
</Paragraph>
</Animated.View>
) : (
<>
<View
style={{
width: "100%",
height: 50,
marginTop: insets.top,
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 12,
paddingLeft: 6
}}
>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="arrow-left"
onPress={close}
customStyle={{
marginRight: 12
}}
size={SIZE.xxl}
/>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 12
}}
>
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
style={{
color: colors.pri,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.nav,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "OpenSans-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
}}
blurOnSubmit
/>
<Paragraph color={colors.light}>/{numPages}</Paragraph>
</View>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="download"
onPress={() => {
downloadAttachment(attachment.metadata.hash, false);
}}
/>
</View>
</View>
{pdfSource ? (
<Animated.View
style={{
flex: 1
}}
entering={FadeIn}
>
<Pdf
source={{
uri: pdfSource
}}
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {
console.log(`Link pressed: ${uri}`);
}}
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
}}
/>
</Animated.View>
) : null}
</>
)}
</View>
</BaseDialog>
)
);
};
export default PDFPreview;

View File

@@ -135,7 +135,6 @@ const ResultDialog = () => {
paddingHorizontal: 12
}}
onPress={close}
height={50}
fontSize={SIZE.md + 2}
/>
</View>

View File

@@ -86,7 +86,7 @@ export const ProFeatures = ({ count = 6 }) => {
await sleep(300);
eSendEvent(eOpenPremiumDialog);
}}
size={SIZE.xs + 1}
size={SIZE.xs}
style={{
textDecorationLine: "underline",
color: colors.icon

View File

@@ -207,13 +207,7 @@ export class VaultDialog extends Component {
return;
}
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"TopicNotes",
"TaggedNotes",
"ColoredNotes"
);
Navigation.queueRoutesForUpdate();
this.password = null;
this.confirmPassword = null;

View File

@@ -30,7 +30,6 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { eScrollEvent } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import Tag from "../ui/tag";
@@ -64,13 +63,15 @@ export const Title = () => {
}
if (data.y > 150) {
if (!hide) return;
titleState[currentScreen.id] = false;
setHide(false);
} else {
if (hide) return;
titleState[currentScreen.id] = true;
setHide(true);
}
},
[currentScreen.name, hide]
[currentScreen.id, currentScreen.name, hide]
);
useEffect(() => {
@@ -85,10 +86,6 @@ export const Title = () => {
}
}, [currentScreen.id, currentScreen.name]);
useEffect(() => {
titleState[currentScreen.id] = hide;
}, [currentScreen.id, hide]);
useEffect(() => {
eSubscribeEvent(eScrollEvent, onScroll);
return () => {
@@ -112,25 +109,16 @@ export const Title = () => {
{!hide && !isHidden ? (
<Heading
onPress={navigateToNotebook}
numberOfLines={isTopic ? 2 : 1}
size={isTopic ? SIZE.md + 2 : SIZE.xl}
numberOfLines={1}
size={SIZE.xl}
style={{
flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0
}}
color={currentScreen.color || colors.heading}
>
{isTopic ? (
<Paragraph numberOfLines={1} size={SIZE.xs + 1}>
{notebook?.title}
{"\n"}
</Paragraph>
) : null}
{isTag ? (
<Heading
size={isTopic ? SIZE.md + 2 : SIZE.xl}
color={colors.accent}
>
<Heading size={SIZE.xl} color={colors.accent}>
#
</Heading>
) : null}

View File

@@ -18,18 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { Platform, View } from "react-native";
import ImageViewer from "react-native-image-zoom-viewer";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { cacheDir } from "../../common/filesystem/utils";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import Sodium from "@ammarahmed/react-native-sodium";
import dataurl from "@notesnook/core/utils/dataurl";
const ImagePreview = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [image, setImage] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
@@ -39,9 +47,29 @@ const ImagePreview = () => {
};
}, []);
const open = (image) => {
setImage(image);
const open = async (image) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
let hash = image.hash;
if (!hash && dataurl.toObject(image.src)) {
const data = dataurl.toObject(image.src);
if (!data) return;
hash = await Sodium.hashFile({
data: data.data,
type: "base64",
uri: ""
});
}
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
setImage("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
@@ -59,44 +87,60 @@ const ImagePreview = () => {
backgroundColor: "black"
}}
>
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
renderHeader={() => (
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 80,
marginTop: 0,
paddingHorizontal: 12,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
paddingTop: 30
}}
>
<IconButton
name="close"
color="white"
onPress={() => {
close();
{loading ? (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
indeterminate
color={colors.accent}
borderColor="transparent"
/>
</View>
) : (
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
renderHeader={() => (
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 80,
marginTop: 0,
paddingHorizontal: 12,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
paddingTop: Platform.OS === "android" ? 30 : 0
}}
/>
</View>
)}
imageUrls={[
{
url: image
}
]}
/>
>
<IconButton
name="close"
color="white"
onPress={() => {
close();
}}
/>
</View>
)}
imageUrls={[
{
url: image
}
]}
/>
)}
</View>
</BaseDialog>
)

File diff suppressed because one or more lines are too long

View File

@@ -17,12 +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 notifee from "@notifee/react-native";
import React, { useCallback, useEffect, useRef } from "react";
import { Platform, View } from "react-native";
import RNBootSplash from "react-native-bootsplash";
import { checkVersion } from "react-native-check-version";
import Config from "react-native-config";
import { enabled } from "react-native-privacy-snapshot";
import { DatabaseLogger, db, loadDatabase } from "../../common/database";
import { DatabaseLogger, db } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import { eSendEvent, presentSheet } from "../../services/event-manager";
@@ -35,10 +37,11 @@ import { useNoteStore } from "../../stores/use-notes-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { AndroidModule } from "../../utils";
import { eOpenAnnouncementDialog } from "../../utils/events";
import { getGithubVersion } from "../../utils/github-version";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { SVG } from "../auth/background";
import Migrate from "../sheets/migrate";
import NewFeature from "../sheets/new-feature/index";
import { Update } from "../sheets/update";
@@ -46,14 +49,9 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import Config from "react-native-config";
import { getGithubVersion } from "../../utils/github-version";
import notifee from "@notifee/react-native";
const Launcher = React.memo(
function Launcher() {
@@ -70,7 +68,8 @@ const Launcher = React.memo(
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const dbInitCompleted = useRef(false);
const verifying = useRef(false);
const loadNotes = useCallback(async () => {
if (verifyUser) {
return;
@@ -81,28 +80,25 @@ const Launcher = React.memo(
initialize();
setImmediate(() => {
setLoading(false);
if (!dbInitCompleted.current) {
setImmediate(() => doAppLoadActions());
}
});
});
});
}, [doAppLoadActions, setLoading, verifyUser]);
}, [setLoading, verifyUser]);
const init = useCallback(async () => {
if (!dbInitCompleted.current) {
await RNBootSplash.hide({ fade: true });
await loadDatabase();
if (!db.isInitialized) {
RNBootSplash.hide({ fade: true });
DatabaseLogger.info("Initializing database");
await db.init();
dbInitCompleted.current = true;
}
if (db.migrations.required() && !verifyUser) {
presentSheet({
component: <Migrate />,
onClose: async () => {
await db.init();
if (!db.isInitialized) {
await db.init();
}
loadNotes();
},
disableClosing: true
@@ -121,9 +117,6 @@ const Launcher = React.memo(
if (!loading) {
doAppLoadActions();
}
return () => {
dbInitCompleted.current = false;
};
}, [doAppLoadActions, loading]);
const doAppLoadActions = useCallback(async () => {
@@ -137,7 +130,6 @@ const Launcher = React.memo(
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await checkNeedsBackup()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
if (introCompleted) {
@@ -151,7 +143,7 @@ const Launcher = React.memo(
}, [introCompleted]);
const checkAppUpdateAvailable = async () => {
if (__DEV__) return;
if (__DEV__ || Config.isTesting === "true") return;
try {
const version =
Config.GITHUB_RELEASE === "true"
@@ -180,25 +172,13 @@ const Launcher = React.memo(
return false;
};
const checkNeedsBackup = async () => {
return false;
// let { nextBackupRequestTime, reminder } = SettingsService.get();
// if (reminder === 'off' || !reminder) {
// if (nextBackupRequestTime < Date.now()) {
// presentSheet({
// title: 'Backup & restore',
// paragraph: 'Please enable automatic backups to keep your data safe',
// component: <SettingsBackupAndRestore isSheet={true} />
// });
// return true;
// }
// }
// return false;
};
const onUnlockBiometrics = useCallback(async () => {
if (!(await BiometricService.isBiometryAvailable())) return;
if (Platform.OS === "android") {
const activityName = await AndroidModule.getActivityName();
if (activityName !== "MainActivity") return;
}
let verified = await BiometricService.validateUser(
"Unlock to access your notes",
""
@@ -207,6 +187,9 @@ const Launcher = React.memo(
setVerifyUser(false);
enabled(false);
password.current = null;
setTimeout(() => {
verifying.current = false;
}, 1);
}
}, [setVerifyUser]);
@@ -215,7 +198,10 @@ const Launcher = React.memo(
}, [init, verifyUser]);
useEffect(() => {
if (verifying.current || useUserStore.getState().shouldBlockVerifyUser)
return;
if (verifyUser && appState === "active") {
verifying.current = true;
onUnlockBiometrics();
}
}, [appState, onUnlockBiometrics, verifyUser]);
@@ -241,18 +227,10 @@ const Launcher = React.memo(
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999
zIndex: 999,
justifyContent: "center"
}}
>
<View
style={{
height: 250,
overflow: "hidden"
}}
>
<SvgView src={SVG(colors.night ? "white" : "black")} height={700} />
</View>
<View
style={{
flex: 1,
@@ -288,14 +266,13 @@ const Launcher = React.memo(
textAlign: "center"
}}
>
Unlock to access your notes
Unlock your notes
</Heading>
<Paragraph
style={{
alignSelf: "center",
textAlign: "center",
fontSize: SIZE.md,
maxWidth: "90%"
}}
>
@@ -306,8 +283,7 @@ const Launcher = React.memo(
style={{
width: "100%",
padding: 12,
backgroundColor: colors.bg,
flexGrow: 1
backgroundColor: colors.bg
}}
>
{user ? (
@@ -347,14 +323,9 @@ const Launcher = React.memo(
<Button
title="Unlock with Biometrics"
width={250}
height={45}
style={{
borderRadius: 100
}}
onPress={onUnlockBiometrics}
icon={"fingerprint"}
type={user ? "grayAccent" : "accent"}
fontSize={SIZE.md}
/>
</View>
</View>

View File

@@ -24,8 +24,10 @@ 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 Notebook from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useThemeStore } from "../../../stores/use-theme-store";
@@ -39,10 +41,6 @@ import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const navigateToTopic = (topic) => {
TopicNotes.navigate(topic, true);
};
function navigateToTag(item) {
const tag = db.tags.tag(item.id);
if (!tag) return;
@@ -55,30 +53,48 @@ const showActionSheet = (item) => {
function getNotebook(item) {
const isTrash = item.type === "trash";
if (isTrash || !item.notebooks || item.notebooks.length < 1) return [];
const currentId = useNavigationStore.getState().currentScreen.id;
if (isTrash) return [];
const items = [];
const notebooks = db.relations.to(item, "notebook") || [];
return item.notebooks.reduce(function (prev, curr) {
if (prev && prev.length > 0) return prev;
const topicId = curr.topics[0];
const notebook = db.notebooks?.notebook(curr.id)?.data;
if (!notebook) return [];
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) return [];
for (let notebook of notebooks) {
if (items.length > 1) break;
if (notebook.id === currentId) continue;
items.push(notebook);
}
return [
{
title: `${notebook?.title} ${topic?.title}`,
notebook: notebook,
topic: topic
if (item.notebooks) {
for (let nb of item.notebooks) {
if (items.length > 1) break;
const notebook = db.notebooks?.notebook(nb.id)?.data;
if (!notebook) continue;
for (let topicId of nb.topics) {
if (items.length > 1) break;
if (topicId === currentId) continue;
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) continue;
items.push(topic);
}
];
}, []);
}
}
return items;
}
function getTags(item) {
const noteTags = item.tags?.slice(0, 3) || [];
const tags = [];
for (const tagName of noteTags) {
const tag = db.tags.tag(tagName);
if (!tag) continue;
tags.push(tag);
}
return tags;
}
const NoteItem = ({
item,
isTrash,
tags,
dateBy = "dateCreated",
noOpen = false
}) => {
@@ -88,11 +104,13 @@ const NoteItem = ({
);
const compactMode = notesListMode === "compact";
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
const notebooks = React.useMemo(() => getNotebook(item), [item]);
const _update = useRelationStore((state) => state.updater);
// eslint-disable-next-line react-hooks/exhaustive-deps
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
const reminders = db.relations.from(item, "reminder");
const reminder = getUpcomingReminder(reminders);
const _update = useRelationStore((state) => state.updater);
const noteColor = COLORS_NOTE[item.color?.toLowerCase()];
const tags = getTags(item);
return (
<>
<View
@@ -112,12 +130,17 @@ const NoteItem = ({
flexWrap: "wrap"
}}
>
{notebooks?.map((_item) => (
{notebooks?.map((item) => (
<Button
title={_item.title}
key={_item}
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
}
tooltipText={item.title}
key={item.id}
height={25}
icon="book-outline"
icon={item.type === "topic" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
@@ -132,7 +155,13 @@ const NoteItem = ({
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => navigateToTopic(_item.topic)}
onPress={() => {
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
Notebook.navigate(item);
}
}}
/>
))}

View File

@@ -100,7 +100,7 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
};
export const NoteWrapper = React.memo(
function NoteWrapper({ item, index, tags, dateBy, isSheet }) {
function NoteWrapper({ item, index, dateBy, isSheet }) {
const isTrash = item.type === "trash";
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
@@ -113,7 +113,7 @@ export const NoteWrapper = React.memo(
isSheet={isSheet}
item={item}
>
<NoteItem item={item} dateBy={dateBy} tags={tags} isTrash={isTrash} />
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
</SelectionWrapper>
);
},
@@ -125,10 +125,6 @@ export const NoteWrapper = React.memo(
return false;
}
if (JSON.stringify(prev.tags) !== JSON.stringify(next.tags)) {
return false;
}
if (prev.item !== next.item) {
return false;
}

View File

@@ -31,6 +31,7 @@ import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "../../../utils/time";
const showActionSheet = (item) => {
Properties.present(item);
@@ -163,7 +164,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{new Date(item[dateBy]).toDateString().substring(4)}
{getFormattedDate(item[dateBy], "date")}
</Paragraph>
)}
<Paragraph

View File

@@ -64,16 +64,7 @@ export const openNotebookTopic = (item) => {
negativeText: "Delete",
positivePress: async () => {
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate(
"Tags",
"Notes",
"Notebooks",
"Favorites",
"Trash",
"TaggedNotes",
"ColoredNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Restore successful",

View File

@@ -108,7 +108,7 @@ const ReminderItem = React.memo(
color={colors.errorText}
/>
<Paragraph
size={SIZE.xs + 1}
size={SIZE.xs}
color={colors.icon}
style={{ marginLeft: 5 }}
>
@@ -133,7 +133,7 @@ const ReminderItem = React.memo(
>
<Icon name="reload" size={SIZE.md} color={colors.accent} />
<Paragraph
size={SIZE.xs + 1}
size={SIZE.xs}
color={colors.icon}
style={{ marginLeft: 5 }}
>
@@ -147,7 +147,7 @@ const ReminderItem = React.memo(
<ReminderTime
reminder={item}
checkIsActive={false}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
style={{
justifyContent: "flex-start",
borderWidth: 0,

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 React, { useRef } from "react";
import React, { useEffect, useRef } from "react";
import { RefreshControl, View } from "react-native";
import { FlashList } from "@shopify/flash-list";
import Animated, { FadeInDown } from "react-native-reanimated";
@@ -57,24 +57,10 @@ const RenderItem = ({ item, index, type, ...restArgs }) => {
const dateBy =
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
const totalNotes = getTotalNotes(item);
const tags =
item.tags
?.slice(0, 3)
?.map((item) => {
let tag = db.tags.tag(item);
if (!tag) return null;
return {
title: tag.title,
id: tag.id,
alias: tag.alias
};
})
.filter((t) => t !== null) || [];
return (
<Item
item={item}
tags={tags}
dateBy={dateBy}
index={index}
type={type}
@@ -103,7 +89,9 @@ const List = ({
ListHeader,
warning,
isSheet = false,
onMomentumScrollEnd
onMomentumScrollEnd,
handlers,
ScrollComponent
}) => {
const colors = useThemeStore((state) => state.colors);
const scrollRef = useRef();
@@ -151,14 +139,21 @@ const List = ({
[screen]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
screen
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const _keyExtractor = (item) => item.id || item.title;
const ListView = ScrollComponent ? ScrollComponent : FlashList;
return (
<>
<Animated.View
@@ -167,7 +162,8 @@ const List = ({
}}
entering={type === "search" ? undefined : FadeInDown}
>
<FlashList
<ListView
{...handlers}
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
@@ -222,12 +218,14 @@ const List = ({
}
/>
</Animated.View>
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Notes" ? "home" : type}
scrollRef={scrollRef}
/>
{listData ? (
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Notes" ? "home" : type}
scrollRef={scrollRef}
/>
) : null}
</>
);
};

View File

@@ -37,7 +37,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { dHeight } from "../../utils";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep, timeConverter } from "../../utils/time";
import { getFormattedDate, sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import DialogButtons from "../dialog/dialog-buttons";
import DialogContainer from "../dialog/dialog-container";
@@ -85,13 +85,7 @@ const MergeConflicts = () => {
}
});
}
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
if (editorController.current?.note?.id === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
@@ -172,7 +166,7 @@ const MergeConflicts = () => {
{isCurrent ? "(This Device)" : "(Incoming)"}
</Text>
{"\n"}
{timeConverter(contentToKeep?.dateEdited)}
{getFormattedDate(contentToKeep?.dateEdited)}
</Paragraph>
</View>

View File

@@ -26,7 +26,7 @@ import { presentSheet } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import { timeConverter, timeSince } from "../../utils/time";
import { getFormattedDate, timeSince } from "../../utils/time";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { PressableButton } from "../ui/pressable";
@@ -63,15 +63,15 @@ export default function NoteHistory({ note, fwdRef }) {
}, []);
const getDate = (start, end) => {
let _start = timeConverter(start);
let _end = timeConverter(end + 60000);
if (_start === _end) return _start;
let final = _end.lastIndexOf(",");
let part = _end.slice(0, final + 1);
if (_start.includes(part)) {
return _start + " —" + _end.replace(part, "");
}
return _start + " — " + _end;
let _start_date = getFormattedDate(start, "date");
let _end_date = getFormattedDate(end + 60000, "date");
let _start_time = getFormattedDate(start, "time");
let _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_end_time}`;
};
const renderItem = useCallback(

View File

@@ -44,16 +44,7 @@ export default function NotePreview({ session, content, note }) {
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate(
"Tags",
"Notes",
"Notebooks",
"Favorites",
"Trash",
"TaggedNotes",
"ColoredNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Restore successful",
@@ -73,13 +64,7 @@ export default function NotePreview({ session, content, note }) {
}
eSendEvent(eCloseSheet, "note_history");
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
ToastEvent.show({
heading: "Note restored successfully",

View File

@@ -18,13 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { ScrollView } from "react-native";
import { FeatureBlock } from "./feature";
import { ScrollView } from "react-native-actions-sheet";
export const CompactFeatures = ({
vertical,
features = [],
maxHeight = 500,
maxHeight = 600,
scrollRef
}) => {
let data = vertical
@@ -80,14 +80,11 @@ export const CompactFeatures = ({
return (
<ScrollView
horizontal={!vertical}
nestedScrollEnabled
onMomentumScrollEnd={() => {
scrollRef?.current?.handleChildScrollEnd();
}}
showsHorizontalScrollIndicator={false}
style={{
width: "100%",
maxHeight: maxHeight
maxHeight: maxHeight,
paddingHorizontal: 12
}}
>
{data.map((item) => (

View File

@@ -143,7 +143,7 @@ export const Expiring = () => {
await sleep(300);
eSendEvent(eOpenPremiumDialog, promo);
}}
size={SIZE.xs + 1}
size={SIZE.xs}
style={{
textDecorationLine: "underline",
color: colors.icon,

View File

@@ -41,18 +41,19 @@ export const FeatureBlock = ({
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
marginBottom: 10
marginBottom: 10,
backgroundColor: colors.nav,
borderRadius: 10,
paddingVertical: 12
}}
>
<Icon color={colors.accent} name="check" size={SIZE.lg} />
<Paragraph
style={{
flexWrap: "wrap",
marginLeft: 5,
flexShrink: 1
}}
size={SIZE.md}
size={SIZE.sm}
>
{content}
</Paragraph>

View File

@@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
import { editorState } from "../../screens/editor/tiptap/utils";
import useKeyboard from "../../hooks/use-keyboard";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
@@ -31,6 +31,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { getElevation } from "../../utils";
import {
eCloseActionSheet,
eCloseSheet,
eOpenPremiumDialog,
eShowGetPremium
} from "../../utils/events";
@@ -39,12 +40,12 @@ import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
export const PremiumToast = ({ context = "global", offset = 0 }) => {
const colors = useThemeStore((state) => state.colors);
const [msg, setMsg] = useState(null);
const timer = useRef();
const keyboard = useKeyboard();
const open = useCallback(
(event) => {
@@ -72,7 +73,6 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
useEffect(() => {
eSubscribeEvent(eShowGetPremium, open);
return () => {
clearTimeout(timer.current);
eUnSubscribeEvent(eShowGetPremium, open);
};
}, [open]);
@@ -80,9 +80,7 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
const onPress = async () => {
open(null);
eSendEvent(eCloseActionSheet);
if (editorState().isFocused) {
//tiny.call(EditorWebView, tiny.blur);
}
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenPremiumDialog);
};
@@ -102,9 +100,13 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
flexDirection: "row",
alignSelf: "center",
justifyContent: "space-between",
top: offset,
top: offset + keyboard.keyboardHeight,
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
}}
onTouchEnd={() => {
setMsg(null);
clearTimeout(timer.current);
}}
>
<View
style={{

View File

@@ -232,8 +232,7 @@ export const PricingPlans = ({
width={250}
style={{
paddingHorizontal: 12,
marginBottom: 15,
borderRadius: 100
marginBottom: 15
}}
/>
</>
@@ -398,8 +397,7 @@ export const PricingPlans = ({
style={{
paddingHorizontal: 12,
marginTop: 30,
marginBottom: 10,
borderRadius: 100
marginBottom: 10
}}
/>
{Platform.OS !== "ios" &&

View File

@@ -43,13 +43,7 @@ export const ColorTags = ({ item }) => {
let _note = db.notes.note(note.id).data;
setNote({ ..._note });
setColorNotes();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
@@ -89,6 +83,7 @@ export const ColorTags = ({ item }) => {
flexWrap: "wrap",
flexGrow: isTablet ? undefined : 1,
paddingHorizontal: 12,
paddingRight: 0,
alignItems: "center",
justifyContent: isTablet ? "center" : "space-between"
}}

View File

@@ -21,7 +21,7 @@ import React from "react";
import { View } from "react-native";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { timeConverter } from "../../utils/time";
import { getFormattedDate } from "../../utils/time";
import Paragraph from "../ui/typography/paragraph";
export const DateMeta = ({ item }) => {
const colors = useThemeStore((state) => state.colors);
@@ -62,11 +62,11 @@ export const DateMeta = ({ item }) => {
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
<Paragraph size={SIZE.xs} color={colors.icon}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{timeConverter(item[key])}
<Paragraph size={SIZE.xs} color={colors.icon}>
{getFormattedDate(item[key], "date-time")}
</Paragraph>
</View>
);

View File

@@ -16,10 +16,9 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Platform, View } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { presentSheet } from "../../services/event-manager";
@@ -37,7 +36,6 @@ import { Items } from "./items";
import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { Tags, TagStrip } from "./tags";
const Line = ({ top = 6, bottom = 6 }) => {
const colors = useThemeStore((state) => state.colors);
return (
@@ -53,20 +51,11 @@ const Line = ({ top = 6, bottom = 6 }) => {
);
};
export const Properties = ({
close = () => {},
item,
buttons = [],
getRef
}) => {
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
const colors = useThemeStore((state) => state.colors);
const alias = item.alias || item.title;
const isColor = !!COLORS_NOTE[item.title];
const onScrollEnd = () => {
getRef().current?.handleChildScrollEnd();
};
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
@@ -75,89 +64,92 @@ export const Properties = ({
);
}
return (
<ScrollView
nestedScrollEnabled
onMomentumScrollEnd={onScrollEnd}
<FlatList
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
style={{
backgroundColor: colors.bg,
paddingHorizontal: 0,
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%"
}}
>
<View
style={{
paddingHorizontal: 12,
marginTop: 5,
zIndex: 10
}}
>
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.accent}>
#
data={[0]}
keyExtractor={() => "properties-scroll-item"}
renderItem={() => (
<View>
<View
style={{
paddingHorizontal: 12,
marginTop: 5,
zIndex: 10
}}
>
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.accent}>
#
</Heading>
) : null}
{alias}
</Heading>
) : null}
{alias}
</Heading>
{item.type === "note" ? (
<TagStrip close={close} item={item} />
) : null}
{item.type === "note" ? <TagStrip close={close} item={item} /> : null}
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
style={{
justifyContent: "flex-start",
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0
}}
fontSize={SIZE.xs}
/>
) : null}
</View>
<Line top={12} />
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
<DateMeta item={item} />
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<View
style={{
justifyContent: "flex-start",
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
<Items
item={item}
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
fontSize={SIZE.xs + 1}
/>
) : null}
</View>
<Line top={12} />
<Synced item={item} close={close} />
<DevMode item={item} />
<DateMeta item={item} />
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<View
style={{
paddingHorizontal: 12
}}
>
{item.notebooks ? <Notebooks note={item} close={close} /> : null}
</View>
<Items
item={item}
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
/>
<Synced item={item} close={close} />
<DevMode item={item} />
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
</ScrollView>
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
</View>
)}
/>
);
};
@@ -187,6 +179,7 @@ Properties.present = (item, buttons = [], isSheet) => {
"lock-unlock",
"trash",
"remove-from-topic",
"remove-from-notebook",
"history",
"read-only",
"reminders",
@@ -198,13 +191,25 @@ Properties.present = (item, buttons = [], isSheet) => {
break;
case "notebook":
props[0] = db.notebooks.notebook(item.id).data;
props.push(["edit-notebook", "pin", "add-shortcut", "trash"]);
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook"
]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push(["move-notes", "edit-topic", "add-shortcut", "trash"]);
props.push([
"move-notes",
"edit-topic",
"add-shortcut",
"trash",
"default-topic"
]);
break;
case "tag":
props[0] = db.tags.tag(item.id);
@@ -224,7 +229,7 @@ Properties.present = (item, buttons = [], isSheet) => {
close={() => {
close();
}}
getRef={() => ref}
actionSheetRef={ref}
item={props[0]}
buttons={props[1]}
/>

View File

@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import { FlatList, ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useActions } from "../../hooks/use-actions";
import { DDS } from "../../services/device-detection";
@@ -85,8 +84,9 @@ export const Items = ({ item, buttons, close }) => {
</View>
);
const renderColumnItem = ({ item }) => (
const renderColumnItem = (item) => (
<Button
key={item.name + item.title}
buttonType={{
text: item.on
? colors.accent
@@ -108,18 +108,19 @@ export const Items = ({ item, buttons, close }) => {
/>
);
const renderTopBarItem = ({ item }) => {
const renderTopBarItem = (item, index) => {
const isLast = index === topBarItems.length;
return (
<PressableButton
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
activeOpacity={1}
customStyle={{
alignItems: "center",
width: topBarItemWidth,
marginBottom: 10,
marginRight: 10,
backgroundColor: "transparent"
alignSelf: "flex-start",
marginRight: isLast ? 0 : 10,
paddingHorizontal: 0,
width: topBarItemWidth
}}
>
<PressableButton
@@ -149,11 +150,7 @@ export const Items = ({ item, buttons, close }) => {
/>
</PressableButton>
<Paragraph
size={SIZE.xxs + 1}
style={{ textAlign: "center" }}
textBreakStrategy="simple"
>
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
{item.title}
</Paragraph>
</PressableButton>
@@ -169,7 +166,7 @@ export const Items = ({ item, buttons, close }) => {
"lock-unlock",
"publish"
];
const bottomBarItems = data.filter(
const topBarItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) > -1
);
@@ -178,21 +175,19 @@ export const Items = ({ item, buttons, close }) => {
);
const topBarItemWidth =
(width - (bottomBarItems.length * 10 + 14)) / bottomBarItems.length;
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
return item.type === "note" ? (
<>
<FlatList
data={bottomBarItems}
keyExtractor={(item) => item.title}
<ScrollView
horizontal
disableVirtualization={true}
style={{
paddingHorizontal: 12,
paddingTop: 12
paddingVertical: 12
}}
renderItem={renderTopBarItem}
/>
>
{topBarItems.map(renderTopBarItem)}
</ScrollView>
<FlatList
data={bottomGridItems}
@@ -202,10 +197,8 @@ export const Items = ({ item, buttons, close }) => {
disableVirtualization={true}
style={{
marginTop: item.type !== "note" ? 10 : 0,
paddingTop: 10
}}
columnWrapperStyle={{
justifyContent: "flex-start"
paddingTop: 10,
marginLeft: 6
}}
contentContainerStyle={{
alignSelf: "center",
@@ -216,11 +209,6 @@ export const Items = ({ item, buttons, close }) => {
/>
</>
) : (
<FlatList
data={data}
keyExtractor={(item) => item.title}
renderItem={renderColumnItem}
disableVirtualization={true}
/>
<View data={data}>{data.map(renderColumnItem)}</View>
);
};

View File

@@ -23,20 +23,33 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import Notebook from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useNotebookStore } from "../../stores/use-notebook-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) {
const colors = useThemeStore((state) => state.colors);
const notebooks = useNotebookStore((state) => state.notebooks);
function getNotebooks(item) {
if (!item.notebooks || item.notebooks.length < 1) return [];
let filteredNotebooks = [];
const relations = db.relations.to(note, "notebook");
filteredNotebooks.push(
...relations.map((notebook) => ({
...notebook,
topics: []
}))
);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
for (let notebookReference of item.notebooks) {
let notebook = {
...(notebooks.find((item) => item.id === notebookReference.id) || {})
@@ -45,7 +58,14 @@ export default function Notebooks({ note, close, full }) {
notebook.topics = notebook.topics.filter((topic) => {
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
});
filteredNotebooks.push(notebook);
const index = filteredNotebooks.findIndex(
(item) => item.id === notebook.id
);
if (index > -1) {
filteredNotebooks[index].topics = notebook.topics;
} else {
filteredNotebooks.push(notebook);
}
}
}
return filteredNotebooks;
@@ -63,7 +83,6 @@ export default function Notebooks({ note, close, full }) {
if (!item) return;
TopicNotes.navigate(item, true);
};
const renderItem = (item) => (
<View
key={item.id}
@@ -78,7 +97,8 @@ export default function Notebooks({ note, close, full }) {
borderWidth: full ? 0 : 1,
borderColor: colors.nav,
borderRadius: 10,
backgroundColor: full ? "transparent" : colors.nav
backgroundColor: full ? "transparent" : colors.nav,
minHeight: 42
}}
>
<Icon
@@ -97,6 +117,7 @@ export default function Notebooks({ note, close, full }) {
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
>
@@ -119,6 +140,7 @@ export default function Notebooks({ note, close, full }) {
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
eSendEvent(eClearEditor);
close();
}}
onLongPress={async () => {
@@ -130,13 +152,7 @@ export default function Notebooks({ note, close, full }) {
note
);
useNotebookStore.getState().setNotebooks();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
ToastEvent.show({
heading: "Note removed from topic",
context: "local",
@@ -146,7 +162,7 @@ export default function Notebooks({ note, close, full }) {
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
icon="bookmark-outline"
style={{
marginRight: 5,
@@ -160,7 +176,7 @@ export default function Notebooks({ note, close, full }) {
</View>
);
return !note.notebooks || note.notebooks.length === 0 ? null : (
return noteNotebooks.length === 0 ? null : (
<View
style={{
width: "100%",
@@ -175,7 +191,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`See all linked notebooks`}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
style={{
alignSelf: "flex-end",
marginRight: 12,

View File

@@ -91,10 +91,10 @@ export const Synced = ({ item, close }) => {
console.error(e);
}
}}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
title="Learn more"
height={30}
type="transparent"
type="grayAccent"
/>
</View>
) : null;

View File

@@ -21,11 +21,10 @@ import React from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { TaggedNotes } from "../../screens/notes/tagged";
import { eSendEvent } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { eOpenTagsDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
export const Tags = ({ item, close }) => {
@@ -41,14 +40,14 @@ export const Tags = ({ item, close }) => {
flexWrap: "wrap",
alignItems: "center",
paddingHorizontal: 12,
alignSelf: "center"
alignSelf: "center",
justifyContent: "space-between",
width: "100%"
}}
>
<Button
onPress={async () => {
close();
await sleep(300);
eSendEvent(eOpenTagsDialog, item);
ManageTagsSheet.present([item]);
}}
buttonType={{
text: colors.accent
@@ -58,7 +57,7 @@ export const Tags = ({ item, close }) => {
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
style={{
marginRight: 5,
borderRadius: 100,
@@ -79,8 +78,8 @@ export const TagStrip = ({ item, close }) => {
alignItems: "center"
}}
>
{item.tags.map((item) =>
item ? <TagItem key={item} tag={item} close={close} /> : null
{item.tags.map((tag) =>
tag ? <TagItem key={tag} tag={tag} close={close} /> : null
)}
</View>
) : null;
@@ -102,14 +101,13 @@ const TagItem = ({ tag, close }) => {
marginTop: 0,
backgroundColor: "transparent"
};
return (
<Button
onPress={onPress}
title={"#" + tag}
type="grayBg"
height={20}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
style={style}
textStyle={{
textDecorationLine: "underline"

View File

@@ -39,7 +39,7 @@ export const Topics = ({ item, close }) => {
height={30}
onPress={() => open(topic)}
icon="bookmark-outline"
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
style={{
marginRight: 5,
paddingHorizontal: 8,

View File

@@ -21,21 +21,22 @@ import React, { useCallback, useEffect } from "react";
import { BackHandler, Platform, View } from "react-native";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import { ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { eOpenMoveNoteDialog } from "../../utils/events";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import ManageTagsSheet from "../sheets/manage-tags";
export const SelectionHeader = React.memo(() => {
const colors = useThemeStore((state) => state.colors);
@@ -64,13 +65,7 @@ export const SelectionHeader = React.memo(() => {
selectedItemsList.forEach((item) => {
db.notes.note(item.id).favorite();
});
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
clearSelection();
}
};
@@ -82,16 +77,7 @@ export const SelectionHeader = React.memo(() => {
noteIds.push(item.id);
});
await db.trash.restore(...noteIds);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Trash",
"Notebooks",
"Tags"
);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastEvent.show({
@@ -118,16 +104,7 @@ export const SelectionHeader = React.memo(() => {
noteIds.push(item.id);
});
await db.trash.delete(...noteIds);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Trash",
"Notebooks",
"Tags"
);
Navigation.queueRoutesForUpdate();
clearSelection();
}
},
@@ -201,7 +178,7 @@ export const SelectionHeader = React.memo(() => {
}}
>
<Heading size={SIZE.md} color={colors.accent}>
{selectedItemsList.length + " Selected"}
{selectedItemsList.length}
</Heading>
</View>
</View>
@@ -233,11 +210,25 @@ export const SelectionHeader = React.memo(() => {
screen === "Notebook" ||
screen === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.pri}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
//setSelectionMode(false);
await sleep(100);
eSendEvent(eOpenMoveNoteDialog);
MoveNoteSheet.present();
}}
customStyle={{
marginLeft: 10
@@ -248,6 +239,7 @@ export const SelectionHeader = React.memo(() => {
name="plus"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
@@ -264,36 +256,40 @@ export const SelectionHeader = React.memo(() => {
</>
)}
{screen === "TopicNotes" ? (
{screen === "TopicNotes" || screen === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
const currentTopic =
const currentScreen =
useNavigationStore.getState().currentScreen;
await db.notes.removeFromNotebook(
{
id: currentTopic.notebookId,
topic: currentTopic.id
},
...selectedItemsList.map((item) => item.id)
);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
if (screen === "Notebook") {
for (const item of selectedItemsList) {
await db.relations.unlink(
{ type: "notebook", id: currentScreen.id },
item
);
}
} else {
await db.notes.removeFromNotebook(
{
id: currentScreen.notebookId,
topic: currentScreen.id
},
...selectedItemsList.map((item) => item.id)
);
}
Navigation.queueRoutesForUpdate();
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from topic"
tooltipText={`Remove from ${
screen === "Notebook" ? "notebook" : "topic"
}`}
tooltipPosition={4}
testID="select-minus"
color={colors.pri}

View File

@@ -36,7 +36,7 @@ import Paragraph from "../ui/typography/paragraph";
const SheetProvider = ({ context = "global" }) => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [dialogData, setDialogData] = useState(null);
const [data, setData] = useState(null);
const actionSheetRef = useRef();
const editor = useRef({
refocus: false
@@ -52,42 +52,35 @@ const SheetProvider = ({ context = "global" }) => {
}, [close, open, visible]);
const open = useCallback(
async (data) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
if (visible || dialogData) {
setDialogData(null);
setVisible(false);
await sleep(0);
}
setDialogData(data);
async (payload) => {
if (!payload.context) payload.context = "global";
if (payload.context !== context) return;
setData(payload);
setVisible(true);
if (data.editor) {
if (payload.editor) {
editor.current.refocus = false;
if (editorState().keyboardState) {
// tiny.call(EditorWebView, tiny.cacheRange + tiny.blur);
editor.current.refocus = true;
}
}
},
[context, dialogData, visible]
[context]
);
useEffect(() => {
(async () => {
if (visible && dialogData) {
if (dialogData.editor) await sleep(100);
if (visible && data) {
if (data.editor) await sleep(100);
actionSheetRef.current?.setModalVisible(true);
return;
} else {
if (editor.current?.refocus) {
editorState().isFocused = true;
// tiny.call(EditorWebView, tiny.restoreRange + tiny.clearRange);
editor.current.refocus = false;
}
}
})();
}, [visible, dialogData]);
}, [visible, data]);
const close = useCallback(
(ctx) => {
@@ -98,34 +91,31 @@ const SheetProvider = ({ context = "global" }) => {
[context]
);
return !visible || !dialogData ? null : (
return !visible || !data ? null : (
<SheetWrapper
fwdRef={actionSheetRef}
gestureEnabled={!dialogData?.progress && !dialogData?.disableClosing}
closeOnTouchBackdrop={
!dialogData?.progress && !dialogData?.disableClosing
}
gestureEnabled={!data?.progress && !data?.disableClosing}
closeOnTouchBackdrop={!data?.progress && !data?.disableClosing}
onClose={() => {
dialogData.onClose && dialogData.onClose();
data.onClose && data.onClose();
setVisible(false);
setDialogData(null);
setData(null);
}}
bottomPadding={!data.noBottomPadding}
enableGesturesInScrollView={data.enableGesturesInScrollView}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginBottom:
!dialogData.progress &&
!dialogData.icon &&
!dialogData.title &&
!dialogData.paragraph
!data.progress && !data.icon && !data.title && !data.paragraph
? 0
: 10,
paddingHorizontal: 12
}}
>
{dialogData?.progress ? (
{data?.progress ? (
<ActivityIndicator
style={{
marginTop: 15
@@ -135,47 +125,48 @@ const SheetProvider = ({ context = "global" }) => {
/>
) : null}
{dialogData?.icon ? (
{data?.icon ? (
<Icon
color={colors[dialogData.iconColor] || colors.accent}
name={dialogData.icon}
color={colors[data.iconColor] || colors.accent}
name={data.icon}
size={50}
/>
) : null}
{dialogData?.title ? <Heading> {dialogData?.title}</Heading> : null}
{data?.title ? <Heading> {data?.title}</Heading> : null}
{dialogData?.paragraph ? (
{data?.paragraph ? (
<Paragraph style={{ textAlign: "center" }}>
{dialogData?.paragraph}
{data?.paragraph}
</Paragraph>
) : null}
</View>
{typeof dialogData.component === "function"
? dialogData.component(
{typeof data.component === "function"
? data.component(
actionSheetRef,
() => close(context),
(data) => {
if (!data) return;
setDialogData((prevData) => {
setData((prevData) => {
return {
...prevData,
...data
};
});
}
},
colors
)
: dialogData.component}
: data.component}
<View
style={{
paddingHorizontal: 12,
marginBottom: dialogData.valueArray ? 12 : 0
marginBottom: data.valueArray ? 12 : 0
}}
>
{dialogData.valueArray &&
dialogData.valueArray.map((v) => (
{data.valueArray &&
data.valueArray.map((v) => (
<Button
title={v}
type="gray"
@@ -197,32 +188,30 @@ const SheetProvider = ({ context = "global" }) => {
paddingHorizontal: 12
}}
>
{dialogData?.action ? (
{data?.action ? (
<Button
onPress={dialogData.action}
key={dialogData.actionText}
title={dialogData.actionText}
accentColor={dialogData.iconColor || "accent"}
onPress={data.action}
key={data.actionText}
title={data.actionText}
accentColor={data.iconColor || "accent"}
accentText="light"
type="accent"
height={45}
height={40}
width={250}
style={{
borderRadius: 100
marginBottom: 25
}}
fontSize={SIZE.md}
/>
) : null}
{dialogData?.actionsArray &&
dialogData?.actionsArray.map((item) => (
{data?.actionsArray &&
data?.actionsArray.map((item) => (
<Button
onPress={item.action}
key={item.accentText}
title={item.actionText}
icon={item.icon && item.icon}
type={item.type || "accent"}
height={50}
style={{
marginBottom: 10
}}
@@ -231,7 +220,7 @@ const SheetProvider = ({ context = "global" }) => {
/>
))}
{dialogData?.learnMore ? (
{data?.learnMore ? (
<Paragraph
style={{
alignSelf: "center",
@@ -239,7 +228,7 @@ const SheetProvider = ({ context = "global" }) => {
textDecorationLine: "underline"
}}
size={SIZE.xs}
onPress={dialogData.learnMorePress}
onPress={data.learnMorePress}
color={colors.icon}
>
<Icon
@@ -247,7 +236,7 @@ const SheetProvider = ({ context = "global" }) => {
name="information-outline"
size={SIZE.xs}
/>{" "}
{dialogData.learnMore}
{data.learnMore}
</Paragraph>
) : null}
</View>

View File

@@ -25,41 +25,34 @@ import {
TouchableOpacity,
View
} from "react-native";
import { FlatList } from "react-native-gesture-handler";
import { FlatList } from "react-native-actions-sheet";
import { notesnook } from "../../../../e2e/test.ids";
import { useMenuStore } from "../../../stores/use-menu-store";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddNotebookDialog,
eOpenAddNotebookDialog
} from "../../../utils/events";
import { ph, pv, SIZE } from "../../../utils/size";
import { DDS } from "../../../services/device-detection";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { IconButton } from "../../ui/icon-button";
import { Button } from "../../ui/button";
import DialogHeader from "../../dialog/dialog-header";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import { MoveNotes } from "../move-notes/movenote";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { Toast } from "../../toast";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
let refs = [];
export class AddNotebookSheet extends React.Component {
constructor(props) {
super(props);
refs = [];
this.state = {
notebook: null,
visible: false,
topics: [],
notebook: props.notebook,
topics:
props.notebook?.topics?.map((item) => {
return item.title;
}) || [],
description: null,
titleFocused: false,
descFocused: false,
@@ -68,13 +61,14 @@ export class AddNotebookSheet extends React.Component {
editTopic: false,
loading: false
};
this.title = null;
this.description = null;
this.title = props.notebook?.title;
this.description = props.notebook?.description;
this.listRef;
this.prevItem = null;
this.prevIndex = null;
this.currentSelectedInput = null;
this.id = null;
this.id = props.notebook?.id;
this.backPressCount = 0;
this.currentInputValue = null;
this.titleRef;
@@ -83,57 +77,22 @@ export class AddNotebookSheet extends React.Component {
this.hiddenInput = createRef();
this.topicInputRef = createRef();
this.addingTopic = false;
this.actionSheetRef = createRef();
}
componentDidMount() {
eSubscribeEvent(eOpenAddNotebookDialog, this.open);
eSubscribeEvent(eCloseAddNotebookDialog, this.close);
this.actionSheetRef = props.actionSheetRef;
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddNotebookDialog, this.open);
eUnSubscribeEvent(eCloseAddNotebookDialog, this.close);
refs = [];
}
open = (notebook) => {
refs = [];
if (notebook) {
let topicsList = [];
notebook.topics.forEach((item) => {
topicsList.push(item.title);
});
this.id = notebook.id;
this.title = notebook.title;
this.description = notebook.description;
this.setState({
topics: [...topicsList],
visible: true,
notebook: notebook
});
} else {
this.setState({
visible: true,
notebook: null
});
}
sleep(100).then(() => {
this.actionSheetRef.current?.show();
componentDidMount() {
sleep(300).then(() => {
!this.state.notebook && this.titleRef?.focus();
});
};
}
close = () => {
this.actionSheetRef.current?.hide();
refs = [];
this.prevIndex = null;
this.prevItem = null;
this.currentSelectedInput = null;
this.title = null;
this.description = null;
this.currentInputValue = null;
this.id = null;
this.props.close(true);
};
onDelete = (index) => {
@@ -223,6 +182,7 @@ export class AddNotebookSheet extends React.Component {
});
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
this.close();
} else {
newNotebookId = await db.notebooks.add({
title: this.title,
@@ -232,23 +192,9 @@ export class AddNotebookSheet extends React.Component {
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate(
"Notes",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
this.setState({
loading: false
});
this.close();
await sleep(300);
if (!notebook) {
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
}
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
};
onSubmit = (forward = true) => {
@@ -265,7 +211,7 @@ export class AddNotebookSheet extends React.Component {
topics: prevTopics
});
setTimeout(() => {
this.listRef.scrollToEnd({ animated: true });
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
this.currentInputValue = null;
} else {
@@ -289,7 +235,7 @@ export class AddNotebookSheet extends React.Component {
if (forward) {
setTimeout(() => {
this.listRef.scrollToEnd({ animated: true });
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
}
}
@@ -299,183 +245,165 @@ export class AddNotebookSheet extends React.Component {
willFocus && this.topicInputRef.current?.focus();
};
renderTopicItem = ({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={this.props.colors}
/>
);
render() {
const { colors } = this.props;
const { topics, visible, topicInputFocused, notebook } = this.state;
if (!visible) return null;
const { topics, topicInputFocused, notebook } = this.state;
return (
<SheetWrapper
onOpen={async () => {
this.topicsToDelete = [];
await sleep(300);
!this.state.notebook && this.titleRef?.focus();
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
fwdRef={this.actionSheetRef}
onClose={() => {
this.close();
this.setState({
visible: false,
topics: [],
descFocused: false,
titleFocused: false,
editTopic: false,
notebook: null
});
}}
statusBarTranslucent={false}
onRequestClose={this.close}
>
<TextInput
ref={this.hiddenInput}
style={{
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<View
style={{
maxHeight: DDS.isTab ? "90%" : "96%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<TextInput
ref={this.hiddenInput}
style={{
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<DialogHeader
title={
notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"
}
paragraph={
notebook && notebook.dateCreated
? "You are editing " + this.title + " notebook."
: "Notebooks are the best way to organize your notes."
}
/>
<Seperator half />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused ? colors.accent : colors.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
onMomentumScrollEnd={() => {
this.actionSheetRef.current?.handleChildScrollEnd();
}}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={<View style={{ height: 50 }} />}
renderItem={({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={colors}
/>
)}
/>
<Seperator />
<Heading size={SIZE.lg}>
{notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"}
</Heading>
<Button
width="100%"
height={50}
fontSize={SIZE.md}
title={
notebook && notebook.dateCreated
? "Save changes"
: "Create notebook"
}
title="Save"
type="accent"
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24
}}
onPress={this.addNewNotebook}
/>
{/*
{Platform.OS === 'ios' && (
<View
style={{
height: 40
}}
/>
)} */}
</View>
<Toast context="local" />
</SheetWrapper>
<Seperator />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused ? colors.accent : colors.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={
topics.length === 0 ? null : <View style={{ height: 50 }} />
}
renderItem={this.renderTopicItem}
/>
</View>
);
}
}
AddNotebookSheet.present = (notebook) => {
presentSheet({
component: (ref, close, _update, colors) => (
<AddNotebookSheet
actionSheetRef={ref}
notebook={notebook}
close={close}
colors={colors}
/>
)
});
};
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
const topicRef = (ref) => (refs[index] = ref);

View File

@@ -20,13 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createContext, useContext } from "react";
export const SelectionContext = createContext({
enabled: false,
selected: [],
toggleSelection: (item) => null,
deselect: (item) => null,
select: (item) => null,
isSelected: (item) => null,
setMultiSelect: () => null,
deselectAll: () => null
});
export const SelectionProvider = SelectionContext.Provider;

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import { FlatList } from "react-native-gesture-handler";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { ListHeaderInputItem } from "./list-header-item.js";
@@ -27,6 +27,7 @@ export const FilteredList = ({
itemType,
onAddItem,
hasHeaderSearch,
listRef,
...restProps
}) => {
const [filtered, setFiltered] = useState(data);
@@ -56,6 +57,7 @@ export const FilteredList = ({
<FlatList
{...restProps}
data={filtered}
ref={listRef}
ListHeaderComponent={
hasHeaderSearch ? (
<ListHeaderInputItem
@@ -69,7 +71,6 @@ export const FilteredList = ({
}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
nestedScrollEnabled
/>
);
};

View File

@@ -17,90 +17,45 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {
createRef,
useCallback,
useEffect,
useMemo,
useState
} from "react";
import React, { useCallback, useEffect, useMemo } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
eSubscribeEvent,
eUnSubscribeEvent,
eSendEvent,
presentSheet,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { eOpenMoveNoteDialog } from "../../../utils/events";
import { eOnTopicSheetUpdate } from "../../../utils/events";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { useRelationStore } from "../../../stores/use-relation-store";
const actionSheetRef = createRef();
const AddToNotebookSheet = () => {
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
function open(note) {
setNote(note);
setVisible(true);
actionSheetRef.current?.setModalVisible(true);
}
useEffect(() => {
eSubscribeEvent(eOpenMoveNoteDialog, open);
return () => {
eUnSubscribeEvent(eOpenMoveNoteDialog, open);
};
}, []);
const _onClose = () => {
setVisible(false);
setNote(null);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
};
return !visible ? null : (
<SheetWrapper fwdRef={actionSheetRef} onClose={_onClose}>
<MoveNoteComponent note={note} />
</SheetWrapper>
);
};
export default AddToNotebookSheet;
const MoveNoteComponent = ({ note }) => {
const MoveNoteSheet = ({ note, actionSheetRef }) => {
const colors = useThemeStore((state) => state.colors);
const [multiSelect, setMultiSelect] = useState(false);
const notebooks = useNotebookStore((state) =>
state.notebooks.filter((n) => n?.type === "notebook")
);
const [edited, setEdited] = useState(false);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const [itemState, setItemState] = useState({});
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const onAddNotebook = async (title) => {
if (!title || title.trim().length === 0) {
@@ -159,13 +114,13 @@ const MoveNoteComponent = ({ note }) => {
(item) => {
switch (item.type) {
case "notebook": {
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
}
const notes = db.relations.from(item, "note");
if (notes.length === 0) return 0;
let count = 0;
selectedItemsList.forEach((item) =>
noteIds.indexOf(item.id) > -1 ? count++ : undefined
notes.findIndex((note) => note.id === item.id) > -1
? count++
: undefined
);
return count;
}
@@ -184,42 +139,55 @@ const MoveNoteComponent = ({ note }) => {
useEffect(() => {
resetItemState();
return () => {
useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetItemState = useCallback(
(state) => {
setItemState(() => {
const itemState = {};
const notebooks = db.notebooks.all;
for (let notebook of notebooks) {
itemState[notebook.id] = state
const itemState = {};
const notebooks = db.notebooks.all;
let count = 0;
for (let notebook of notebooks) {
itemState[notebook.id] = state
? state
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[notebook.id] === "selected") {
count++;
contextValue.select(notebook);
} else {
contextValue.deselect(notebook);
}
for (let topic of notebook.topics) {
itemState[topic.id] = state
? state
: areAllSelectedItemsInAllTopics(notebook, selectedItemsList) &&
getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
getSelectedNotesCountInItem(topic, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[notebook.id] === "selected") {
contextValue.select(notebook);
}
for (let topic of notebook.topics) {
itemState[topic.id] = state
? state
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
getSelectedNotesCountInItem(topic, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[topic.id] === "selected") {
contextValue.select(topic);
}
if (itemState[topic.id] === "selected") {
count++;
contextValue.select(topic);
} else {
contextValue.deselect(topic);
}
}
return itemState;
});
}
if (count > 1) {
useItemSelectionStore.getState().setMultiSelect(true);
} else {
useItemSelectionStore.getState().setMultiSelect(false);
}
useItemSelectionStore.getState().setItemState(itemState);
},
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
);
@@ -231,11 +199,11 @@ const MoveNoteComponent = ({ note }) => {
}
};
function areAllSelectedItemsInAllTopics(notebook, items) {
function areAllSelectedItemsInNotebook(notebook, items) {
const notes = db.relations.from(notebook, "note");
if (notes.length === 0) return false;
return items.every((item) => {
return notebook.topics.every((topic) => {
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
});
return notes.find((note) => note.id === item.id);
});
}
@@ -246,51 +214,26 @@ const MoveNoteComponent = ({ note }) => {
}
const updateItemState = useCallback(function (item, state) {
setItemState((itemState) => {
const mergeState = {
[item.id]: state
};
const notebooks = db.notebooks.all;
const notebook =
item.type === "notebook"
? item
: notebooks.find((n) => n.id === item.notebookId);
const intermediate = notebook.topics.some((topic) => {
return topic.id === item.id
? state === "selected"
: itemState[topic.id] === "selected";
});
if (intermediate) mergeState[notebook.id] = "intermediate";
const selected = notebook.topics.every((topic) => {
return topic.id === item.id
? state === "selected"
: itemState[topic.id] === "selected";
});
if (selected) mergeState[notebook.id] = "selected";
if (!selected && !intermediate) mergeState[notebook.id] = "deselected";
return {
...itemState,
...mergeState
};
const itemState = useItemSelectionStore.getState().itemState;
const mergeState = {
[item.id]: state
};
useItemSelectionStore.getState().setItemState({
...itemState,
...mergeState
});
}, []);
const contextValue = useMemo(
() => ({
enabled: multiSelect,
toggleSelection: (item) => {
setItemState((itemState) => {
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
return itemState;
});
const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
},
setMultiSelect: setMultiSelect,
deselect: (item) => {
updateItemState(item, "deselected");
},
@@ -301,7 +244,7 @@ const MoveNoteComponent = ({ note }) => {
resetItemState(state);
}
}),
[multiSelect, resetItemState, updateItemState]
[resetItemState, updateItemState]
);
const getItemFromId = (id) => {
@@ -314,39 +257,47 @@ const MoveNoteComponent = ({ note }) => {
};
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
const itemState = useItemSelectionStore.getState().itemState;
for (const id in itemState) {
const item = getItemFromId(id);
if (item.type === "notebook") continue;
const noteIds = selectedItemsList.map((n) => n.id);
if (itemState[id] === "selected") {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
}
} else if (itemState[id] === "deselected") {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
}
}
}
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
setNotebooks();
eSendEvent(eOnTopicSheetUpdate);
SearchService.updateAndSearch();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
};
@@ -397,34 +348,32 @@ const MoveNoteComponent = ({ note }) => {
/>
</View>
{multiSelect ? (
<View
<View
style={{
paddingHorizontal: 12
}}
>
<Button
title="Reset selection"
height={30}
style={{
paddingHorizontal: 12
alignSelf: "flex-start",
paddingHorizontal: 0,
width: "100%",
marginTop: 6
}}
>
<Button
title="Reset selection"
height={30}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0
}}
onPress={() => {
resetItemState();
setMultiSelect(false);
}}
/>
</View>
) : null}
type="grayAccent"
onPress={() => {
resetItemState();
}}
/>
</View>
<SelectionProvider value={contextValue}>
<FilteredList
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
style={{
paddingHorizontal: 12
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85
}}
ListEmptyComponent={
notebooks.length > 0 ? null : (
@@ -450,12 +399,8 @@ const MoveNoteComponent = ({ note }) => {
item={item}
key={item.id}
index={index}
intermediate={itemState[item.id] === "intermediate"}
removed={
itemState[item.id] === "deselected" &&
getSelectedNotesCountInItem(item) > 0
}
isSelected={itemState[item.id] === "selected"}
hasNotes={getSelectedNotesCountInItem(item) > 0}
sheetRef={actionSheetRef}
infoText={
<>
{item.topics.length === 1
@@ -465,25 +410,18 @@ const MoveNoteComponent = ({ note }) => {
}
getListItems={getItemsForItem}
getSublistItemProps={(topic) => ({
selected: itemState[topic.id] === "selected",
intermediate: itemState[topic.id] === "intermediate",
isSelected: itemState[topic.id] === "selected",
removed:
itemState[topic.id] === "deselected" &&
getSelectedNotesCountInItem(topic) > 0,
hasNotes: getSelectedNotesCountInItem(topic) > 0,
style: {
marginBottom: 0,
height: 40
},
onPress: (item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
updateItemState(
notebooks.find((n) => n.id === item.notebookId),
"intermediate"
);
} else {
contextValue.deselect(item);
}
@@ -508,22 +446,35 @@ const MoveNoteComponent = ({ note }) => {
onAddSublistItem={(item) => {
openAddTopicDialog(item);
}}
onPress={(item) => {
const itemState = useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
}}
/>
)}
itemType="notebook"
onAddItem={async (title) => {
return await onAddNotebook(title);
}}
ListFooterComponent={
<View
style={{
height: 200
}}
/>
}
ListFooterComponent={<View style={{ height: 20 }} />}
/>
</SelectionProvider>
</View>
</>
);
};
MoveNoteSheet.present = (note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true
});
};
export default MoveNoteSheet;

View File

@@ -17,8 +17,10 @@ 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, { useState } from "react";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
@@ -27,13 +29,78 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useSelectionContext } from "./context";
import { FilteredList } from "./filtered-list";
import { useItemSelectionStore } from "./store";
const _ListItem = ({
const SelectionIndicator = ({
item,
hasNotes,
selectItem,
onPress,
onChange
}) => {
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const isSelected = itemState === "selected";
const isIntermediate = itemState === "intermediate";
const isRemoved = !isSelected && hasNotes;
const colors = useThemeStore((state) => state.colors);
useEffect(() => {
onChange?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemState]);
return (
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
isRemoved
? colors.red
: isIntermediate || isSelected
? colors.accent
: colors.icon
}
onPress={() => {
if (multiSelect) return selectItem();
onPress?.(item);
}}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
testID={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
);
};
export const ListItem = ({
item,
index,
icon,
infoText,
intermediate,
hasSubList,
onPress,
onScrollEnd,
@@ -43,29 +110,49 @@ const _ListItem = ({
sublistItemType,
onAddItem,
getSublistItemProps,
removed,
isSelected,
hasHeaderSearch,
onAddSublistItem
onAddSublistItem,
hasNotes,
onChange,
sheetRef
}) => {
const { enabled, toggleSelection, setMultiSelect, select, deselect } =
useSelectionContext();
const { toggleSelection } = useSelectionContext();
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
const colors = useThemeStore((state) => state.colors);
const [expanded, setExpanded] = useState(false);
function selectItem() {
const currentState = isSelected;
toggleSelection(item);
if (item.type === "notebook") {
item.topics.forEach((item) => {
if (currentState) {
deselect(item);
} else {
select(item);
}
});
}
}
const getSelectedNotesCountInNotebookTopics = (item) => {
if (item.type === "topic") return;
let count = 0;
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
count++;
}
}
useSelectionStore.getState().selectedItemsList.forEach((item) => {
if (noteIds.indexOf(item.id) > -1) {
count++;
}
});
return count;
};
useEffect(() => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
}, [item]);
const onChangeSubItem = () => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
};
return (
<View
style={{
@@ -77,12 +164,12 @@ const _ListItem = ({
<PressableButton
onPress={() => {
if (hasSubList) return setExpanded(!expanded);
if (enabled) return selectItem();
if (multiSelect) return selectItem();
onPress?.(item);
}}
type={type}
onLongPress={() => {
setMultiSelect(true);
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
customStyle={{
@@ -107,46 +194,12 @@ const _ListItem = ({
alignItems: "center"
}}
>
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
removed
? colors.red
: intermediate || isSelected
? colors.accent
: colors.icon
}
onPress={() => {
if (item.type === "notebook") {
setMultiSelect(true);
}
selectItem();
if (enabled) return;
onPress?.(item);
}}
testID={
removed
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: intermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
removed
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: intermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
<SelectionIndicator
hasNotes={hasNotes}
onPress={onPress}
item={item}
onChange={onChange}
selectItem={selectItem}
/>
<View>
{hasSubList && expanded ? (
@@ -165,14 +218,27 @@ const _ListItem = ({
<View
style={{
flexDirection: "row"
flexDirection: "row",
alignItems: "center"
}}
>
{showSelectedIndicator ? (
<View
style={{
backgroundColor: colors.accent,
width: 7,
height: 7,
borderRadius: 100,
marginRight: 12
}}
/>
) : null}
{onAddSublistItem ? (
<IconButton
name={"plus"}
testID="add-item-icon"
color={colors}
color={colors.pri}
size={SIZE.xl}
onPress={() => {
onAddSublistItem(item);
@@ -205,7 +271,7 @@ const _ListItem = ({
style={{
width: "95%",
alignSelf: "flex-end",
maxHeight: 500
maxHeight: 250
}}
itemType={sublistItemType}
hasHeaderSearch={hasHeaderSearch}
@@ -214,6 +280,7 @@ const _ListItem = ({
item={item}
{...getSublistItemProps(item)}
index={index}
onChange={onChangeSubItem}
onScrollEnd={onScrollEnd}
/>
)}
@@ -223,11 +290,3 @@ const _ListItem = ({
</View>
);
};
export const ListItem = React.memo(_ListItem, (prev, next) => {
if (prev.selected === undefined) return false;
if (prev.isSelected !== next.isSelected) return false;
if (prev.selected !== next.selected) return false;
if (prev.intermediate !== next.intermediate) return false;
return true;
});

View File

@@ -0,0 +1,42 @@
/*
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 create, { State } from "zustand";
type SelectionItemState = Record<
string,
"intermediate" | "selected" | "deselected"
>;
export interface SelectionStore extends State {
itemState: SelectionItemState;
setItemState: (state: SelectionItemState) => void;
multiSelect: boolean;
setMultiSelect: (multiSelect: boolean) => void;
}
export const useItemSelectionStore = create<SelectionStore>((set) => ({
itemState: {},
setItemState: (itemState) => {
set({
itemState
});
},
multiSelect: false,
setMultiSelect: (multiSelect) => set({ multiSelect })
}));

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { RefObject, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { ActionSheetRef } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import {
eSendEvent,
@@ -31,7 +31,7 @@ import { Button } from "../../ui/button";
import Input from "../../ui/input";
type ChangeEmailProps = {
actionSheetRef: RefObject<ActionSheet>;
actionSheetRef: RefObject<ActionSheetRef>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
};

View File

@@ -42,6 +42,7 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { requestInAppReview } from "../../../services/app-review";
const ExportNotesSheet = ({ notes, update }) => {
const colors = useThemeStore((state) => state.colors);
@@ -72,6 +73,7 @@ const ExportNotesSheet = ({ notes, update }) => {
update({ disableClosing: false });
setComplete(true);
setExporting(false);
requestInAppReview();
};
const actions = [
@@ -277,7 +279,6 @@ const ExportNotesSheet = ({ notes, update }) => {
});
});
}}
height={50}
/>
<Button
title="Share"
@@ -301,7 +302,6 @@ const ExportNotesSheet = ({ notes, update }) => {
}).catch(console.log);
}
}}
height={50}
/>
<Button
title="Export in another format"
@@ -317,7 +317,6 @@ const ExportNotesSheet = ({ notes, update }) => {
setResult(null);
setExporting(false);
}}
height={50}
/>
</>
)}

View File

@@ -48,7 +48,6 @@ export const ShareComponent = ({ uri, name, padding }) => {
});
});
}}
height={50}
/>
<Button
title="Share"
@@ -65,7 +64,6 @@ export const ShareComponent = ({ uri, name, padding }) => {
shareFile: true
}).catch(console.log);
}}
height={50}
/>
</View>
);

View File

@@ -196,7 +196,6 @@ For example:
onPress={onPress}
title={loading ? null : "Submit"}
loading={loading}
height={50}
width="100%"
type="accent"
/>

View File

@@ -17,51 +17,33 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { ScrollView, View } from "react-native";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useTagStore } from "../../../stores/use-tag-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { eCloseTagsDialog, eOpenTagsDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import SheetWrapper from "../../ui/sheet";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useCallback } from "react";
const ManageTagsSheet = () => {
import { IconButton } from "../../ui/icon-button";
const ManageTagsSheet = (props) => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const [notes, setNotes] = useState(props.notes || []);
const allTags = useTagStore((state) => state.tags);
const [tags, setTags] = useState([]);
const [query, setQuery] = useState(null);
const inputRef = useRef();
const actionSheetRef = useRef();
const [focus, setFocus] = useState(false);
useEffect(() => {
eSubscribeEvent(eOpenTagsDialog, open);
eSubscribeEvent(eCloseTagsDialog, close);
return () => {
eUnSubscribeEvent(eOpenTagsDialog, open);
eUnSubscribeEvent(eCloseTagsDialog, close);
};
}, [open]);
useEffect(() => {
if (visible) {
sortTags();
}
}, [allTags, note, query, sortTags, visible]);
sortTags();
}, [allTags, notes, query, sortTags]);
const sortTags = useCallback(() => {
let _tags = [...allTags];
@@ -70,13 +52,14 @@ const ManageTagsSheet = () => {
if (query) {
_tags = _tags.filter((t) => t.title.startsWith(query));
}
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
if (!note || !note.tags) {
if (!tagsMerged || !tagsMerged.length) {
setTags(_tags);
return;
}
let noteTags = [];
for (let tag of note.tags) {
for (let tag of tagsMerged) {
let index = _tags.findIndex((t) => t.title === tag);
if (index !== -1) {
noteTags.push(_tags[index]);
@@ -86,28 +69,11 @@ const ManageTagsSheet = () => {
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
let combinedTags = [...noteTags, ..._tags];
setTags(combinedTags);
}, [allTags, note, query]);
const open = useCallback(
(item) => {
setNote(item);
useTagStore.getState().setTags();
sortTags();
setVisible(true);
},
[sortTags]
);
}, [allTags, notes, query]);
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
setQuery(null);
actionSheetRef.current?.hide();
};
useTagStore.getState().setTags();
}, []);
const onSubmit = async () => {
let _query = query;
@@ -121,15 +87,23 @@ const ManageTagsSheet = () => {
}
let tag = _query;
setNote({ ...note, tags: note.tags ? [...note.tags, tag] : [tag] });
setNotes(
notes.map((note) => ({
...note,
tags: note.tags ? [...note.tags, tag] : [tag]
}))
);
setQuery(null);
inputRef.current?.setNativeProps({
text: ""
});
try {
await db.notes.note(note.id).tag(tag);
for (let note of notes) {
await db.notes.note(note.id).tag(tag);
}
useTagStore.getState().setTags();
setNote(db.notes.note(note.id).data);
setNotes(notes.map((note) => db.notes.note(note.id).data));
} catch (e) {
ToastEvent.show({
heading: "Cannot add tag",
@@ -139,143 +113,131 @@ const ManageTagsSheet = () => {
});
}
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
Navigation.queueRoutesForUpdate();
};
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onOpen={async () => {
await sleep(300);
inputRef.current?.focus();
}}
onClose={async () => {
setQuery(null);
setVisible(false);
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
minHeight: focus ? "100%" : "60%"
}}
>
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
minHeight: "60%"
<Input
button={{
icon: "magnify",
color: colors.accent,
size: SIZE.lg
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(db.tags.sanitize(v));
}}
onFocusInput={() => {
setFocus(true);
}}
onBlurInput={() => {
setFocus(false);
}}
onSubmit={onSubmit}
placeholder="Search or add a tag"
/>
<ScrollView
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
<Input
button={{
icon: "magnify",
color: colors.accent,
size: SIZE.lg
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(db.tags.sanitize(v));
}}
onSubmit={onSubmit}
height={50}
placeholder="Search or add a tag"
/>
{query && query !== tags[0]?.title ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="accent"
>
<Heading size={SIZE.sm} color={colors.light}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.light} size={SIZE.lg} />
</PressableButton>
) : null}
{!allTags || allTags.length === 0 ? (
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.icon}>
#
</Heading>
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
You do not have any tags.
</Paragraph>
</View>
) : null}
<ScrollView
nestedScrollEnabled
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
>
{query && query !== tags[0]?.title ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="accent"
>
<Heading size={SIZE.sm} color={colors.light}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.light} size={SIZE.lg} />
</PressableButton>
) : null}
{!allTags || allTags.length === 0 ? (
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.icon}>
#
</Heading>
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
You do not have any tags.
</Paragraph>
</View>
) : null}
{tags.map((item) => (
<TagItem
key={item.title}
tag={item}
note={note}
setNote={setNote}
/>
))}
</ScrollView>
</View>
</SheetWrapper>
{tags.map((item) => (
<TagItem
key={item.title}
tag={item}
notes={notes}
setNotes={setNotes}
/>
))}
</ScrollView>
</View>
);
};
ManageTagsSheet.present = (notes) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({ tag, note, setNote }) => {
const TagItem = ({ tag, notes, setNotes }) => {
const colors = useThemeStore((state) => state.colors);
const someNotesTagged = notes.some(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const allNotesTagged = notes.every(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const onPress = async () => {
let prevNote = { ...note };
try {
if (prevNote.tags.indexOf(tag.title) !== -1) {
await db.notes
.note(note.id)
.untag(prevNote.tags[prevNote.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
for (let note of notes) {
try {
if (someNotesTagged) {
await db.notes
.note(note.id)
.untag(note.tags[note.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
} catch (e) {
console.error(e);
}
useTagStore.getState().setTags();
setNote(db.notes.note(note.id).data);
} catch (e) {
console.error(e);
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setTimeout(() => {
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
}, 1);
};
@@ -284,39 +246,36 @@ const TagItem = ({ tag, note, setNote }) => {
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
justifyContent: "flex-start",
height: 40
}}
onPress={onPress}
type={
note && note.tags.findIndex((t) => t === tag.title) !== -1
? "shade"
: "grayBg"
}
type="gray"
>
<Heading
size={SIZE.sm}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.pri
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={someNotesTagged || allNotesTagged ? colors.accent : colors.icon}
testID={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
>
{"#" + tag.title}
</Heading>
<Icon
name={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? "minus"
: "plus"
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.accent
}
size={SIZE.lg}
/>
<Paragraph size={SIZE.sm}>{"#" + tag.title}</Paragraph>
</PressableButton>
);
};

View File

@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { FlashList } from "@shopify/flash-list";
import { NotebookType, NoteType, TopicType } from "app/utils/types";
import React, { RefObject, useState } from "react";
import { Platform, useWindowDimensions, View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import { db } from "../../../common/database";
import {
eSendEvent,
@@ -58,7 +58,7 @@ export const MoveNotes = ({
}: {
notebook: NotebookType;
selectedTopic?: TopicType;
fwdRef: RefObject<ActionSheet>;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const colors = useThemeStore((state) => state.colors);
const [currentNotebook, setCurrentNotebook] = useState(notebook);
@@ -122,15 +122,7 @@ export const MoveNotes = ({
db.notebooks?.notebook(currentNotebook.id).data as NotebookType
);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebook",
"Notebooks"
);
Navigation.queueRoutesForUpdate();
return true;
};
@@ -166,11 +158,7 @@ export const MoveNotes = ({
{item.title}
</Paragraph>
{item.type == "note" && item.headline ? (
<Paragraph
numberOfLines={1}
color={colors.icon}
size={SIZE.xs + 1}
>
<Paragraph numberOfLines={1} color={colors.icon} size={SIZE.xs}>
{item.headline}
</Paragraph>
) : null}
@@ -277,10 +265,6 @@ export const MoveNotes = ({
)}
<FlashList
nestedScrollEnabled
onMomentumScrollEnd={() => {
fwdRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
<View
style={{
@@ -322,15 +306,7 @@ export const MoveNotes = ({
},
...selectedNoteIds
);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebook",
"Notebooks"
);
Navigation.queueRoutesForUpdate();
SearchService.updateAndSearch();
eSendEvent(eCloseSheet);
}}
@@ -345,7 +321,7 @@ export const MoveNotes = ({
MoveNotes.present = (notebook: NotebookType, topic: TopicType) => {
presentSheet({
component: (ref: RefObject<ActionSheet>) => (
component: (ref: RefObject<ActionSheetRef>) => (
<MoveNotes fwdRef={ref} notebook={notebook} selectedTopic={topic} />
)
});

View File

@@ -93,7 +93,13 @@ NewFeature.present = () => {
});
return;
}
if (version && version === getVersion()) return false;
if (!version || version === getVersion()) {
SettingsService.set({
version: getVersion()
});
return false;
}
SettingsService.set({
version: getVersion()
});

View File

@@ -18,22 +18,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import { presentSheet, ToastEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import {
eClosePublishNoteDialog,
eOpenPublishNoteDialog
} from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions";
import { SIZE } from "../../../utils/size";
import DialogHeader from "../../dialog/dialog-header";
@@ -41,76 +33,41 @@ import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { requestInAppReview } from "../../../services/app-review";
let passwordValue = null;
const PublishNoteSheet = () => {
const PublishNoteSheet = ({ note: item, update }) => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const actionSheetRef = useRef();
const loading = useAttachmentStore((state) => state.loading);
const [selfDestruct, setSelfDestruct] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [note, setNote] = useState(null);
const [note, setNote] = useState(item);
const [publishing, setPublishing] = useState(false);
const publishUrl =
note &&
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
const isPublished = note && db?.monographs.isPublished(note?.id);
const pwdInput = useRef();
useEffect(() => {
eSubscribeEvent(eOpenPublishNoteDialog, open);
eSubscribeEvent(eClosePublishNoteDialog, close);
return () => {
eUnSubscribeEvent(eOpenPublishNoteDialog, open);
eUnSubscribeEvent(eClosePublishNoteDialog, close);
};
}, []);
const open = (item) => {
if (!item) return;
setNote(item);
setPublishing(false);
setSelfDestruct(false);
setIsLocked(false);
setVisible(true);
passwordValue = null;
};
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
passwordValue = null;
actionSheetRef.current?.hide();
};
const passwordValue = useRef();
const publishNote = async () => {
if (publishing) return;
setPublishing(true);
setPublishLoading(true);
try {
if (note?.id) {
if (isLocked && !passwordValue) return;
await db.monographs.publish(note.id, {
selfDestruct: selfDestruct,
password: isLocked && passwordValue
password: isLocked && passwordValue.current
});
setNote(db.notes.note(note.id)?.data);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
requestInAppReview();
} catch (e) {
ToastEvent.show({
heading: "Could not publish note",
@@ -120,23 +77,22 @@ const PublishNoteSheet = () => {
});
}
setPublishing(false);
setPublishLoading(false);
};
const setPublishLoading = (value) => {
setPublishing(value);
};
const deletePublishedNote = async () => {
if (publishing) return;
setPublishing(true);
setPublishLoading(true);
try {
if (note?.id) {
await db.monographs.unpublish(note.id);
setNote(db.notes.note(note.id)?.data);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
} catch (e) {
ToastEvent.show({
@@ -147,267 +103,281 @@ const PublishNoteSheet = () => {
});
}
actionSheetRef.current?.hide();
setPublishing(false);
setPublishLoading(false);
};
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
closeOnTouchBackdrop={!publishing}
gestureEnabled={!publishing}
onClose={async () => {
passwordValue = null;
setVisible(false);
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
}}
>
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
}}
>
<DialogHeader
title={note?.title}
paragraph={`Anyone with the link${
isLocked ? " and password" : ""
} of the published note can view it.`}
/>
<DialogHeader
title={note?.title}
paragraph={`Anyone with the link${
isLocked ? " and password" : ""
} of the published note can view it.`}
/>
{publishing ? (
<View
{publishing ? (
<View
style={{
justifyContent: "center",
alignContent: "center",
height: 150,
width: "100%"
}}
>
<ActivityIndicator size={25} color={colors.accent} />
<Paragraph
style={{
justifyContent: "center",
alignContent: "center",
height: 150,
width: "100%"
textAlign: "center"
}}
>
<ActivityIndicator size={25} color={colors.accent} />
<Paragraph
Please wait...
{loading && loading.current && loading.total
? `\nDownloading attachments (${
loading?.current / loading?.total
})`
: ""}
</Paragraph>
</View>
) : (
<>
{isPublished && (
<View
style={{
textAlign: "center"
flexDirection: "row",
alignItems: "center",
marginTop: 10,
backgroundColor: colors.nav,
padding: 12,
borderRadius: 5
}}
>
Please wait...
{loading && loading.current && loading.total
? `\nDownloading attachments (${
loading?.current / loading?.total
})`
: ""}
</Paragraph>
</View>
) : (
<>
{isPublished && (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginTop: 15,
backgroundColor: colors.nav,
padding: 12,
borderRadius: 5
width: "100%",
flexShrink: 1
}}
>
<View
<Heading size={SIZE.md}>Published at:</Heading>
<Paragraph size={SIZE.sm} numberOfLines={1}>
{publishUrl}
</Paragraph>
<Paragraph
onPress={async () => {
try {
await openLinkInBrowser(publishUrl, colors.accent);
} catch (e) {
console.error(e);
}
}}
size={SIZE.xs}
style={{
width: "100%",
flexShrink: 1
marginTop: 5,
color: colors.pri
}}
>
<Heading size={SIZE.sm}>Published at:</Heading>
<Paragraph size={SIZE.xs} numberOfLines={1}>
{publishUrl}
</Paragraph>
<Paragraph
onPress={async () => {
try {
await openLinkInBrowser(publishUrl, colors.accent);
} catch (e) {
console.error(e);
}
}}
size={SIZE.xs}
style={{
marginTop: 5,
color: colors.pri
}}
>
<Icon color={colors.accent} name="open-in-new" /> Open in
browser
</Paragraph>
</View>
<IconButton
onPress={() => {
Clipboard.setString(publishUrl);
ToastEvent.show({
heading: "Note publish url copied",
type: "success",
context: "local"
});
}}
color={colors.accent}
size={SIZE.lg}
name="content-copy"
/>
<Icon color={colors.accent} name="open-in-new" /> Open in
browser
</Paragraph>
</View>
)}
<Seperator />
<TouchableOpacity
<IconButton
onPress={() => {
Clipboard.setString(publishUrl);
ToastEvent.show({
heading: "Note publish url copied",
type: "success",
context: "local"
});
}}
color={colors.accent}
size={SIZE.lg}
name="content-copy"
/>
</View>
)}
<TouchableOpacity
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
activeOpacity={0.9}
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 10,
backgroundColor: colors.nav,
paddingVertical: 12,
borderRadius: 5,
marginTop: 10
}}
>
<IconButton
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
activeOpacity={0.9}
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 10
}}
>
<IconButton
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
color={isLocked ? colors.accent : colors.icon}
size={SIZE.lg}
name={
isLocked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.md}>Password protection</Heading>
<Paragraph>
Published note can only be viewed by someone with the
password.
</Paragraph>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
activeOpacity={0.9}
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<IconButton
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
color={selfDestruct ? colors.accent : colors.icon}
size={SIZE.lg}
name={
selfDestruct
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.md}>Self destruct</Heading>
<Paragraph>
Published note link will be automatically deleted once it is
viewed by someone.
</Paragraph>
</View>
</TouchableOpacity>
color={isLocked ? colors.accent : colors.icon}
size={SIZE.xl}
name={
isLocked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
alignSelf: "center",
marginTop: 10
flexShrink: 1
}}
>
{isLocked ? (
<>
<Input
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue}
placeholder="Enter Password"
/>
<Seperator half />
</>
) : null}
<Heading size={SIZE.md}>Password protection</Heading>
<Paragraph>
Published note can only be viewed by someone with the password.
</Paragraph>
</View>
</TouchableOpacity>
<Button
onPress={publishNote}
fontSize={SIZE.md}
width="100%"
style={{
marginTop: 10
}}
height={50}
type="accent"
title={isPublished ? "Update published note" : "Publish note"}
/>
<TouchableOpacity
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
activeOpacity={0.9}
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.nav,
paddingVertical: 12,
borderRadius: 5
}}
>
<IconButton
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
color={selfDestruct ? colors.accent : colors.icon}
size={SIZE.xl}
name={
selfDestruct
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.md}>Self destruct</Heading>
<Paragraph>
Published note link will be automatically deleted once it is
viewed by someone.
</Paragraph>
</View>
</TouchableOpacity>
<View
style={{
width: "100%",
alignSelf: "center",
marginTop: 10
}}
>
{isLocked ? (
<>
<Input
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue.current = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue.current}
placeholder="Enter Password"
/>
<Seperator half />
</>
) : null}
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "center"
}}
>
{isPublished && (
<>
<Seperator half />
<Button
onPress={deletePublishedNote}
fontSize={SIZE.md}
width="100%"
height={50}
type="error"
title="Unpublish note"
title="Unpublish"
style={{
width: "49%"
}}
/>
</>
)}
<Seperator half />
<Button
onPress={publishNote}
fontSize={SIZE.md}
style={{
width: isPublished ? "49%" : 250,
borderRadius: isPublished ? 5 : 100
}}
type="accent"
title={isPublished ? "Update" : "Publish"}
/>
</View>
</>
)}
</View>
</>
)}
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 5,
textDecorationLine: "underline"
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://docs.notesnook.com/monographs/",
colors.accent
);
} catch (e) {
console.error(e);
}
}}
>
Learn more about Notesnook Monograph
</Paragraph>
</View>
</SheetWrapper>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 10,
textDecorationLine: "underline"
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://docs.notesnook.com/monographs/",
colors.accent
);
} catch (e) {
console.error(e);
}
}}
>
Learn more about Notesnook Monograph
</Paragraph>
</View>
);
};
PublishNoteSheet.present = (note) => {
presentSheet({
component: (ref, close, update) => (
<PublishNoteSheet
actionSheetRef={ref}
close={close}
update={update}
note={note}
/>
)
});
};
export default PublishNoteSheet;

View File

@@ -96,7 +96,6 @@ const RateAppSheet = () => {
onPress={rateApp}
fontSize={SIZE.md}
width="100%"
height={50}
type="accent"
title="Rate now (It takes only a second)"
/>
@@ -121,14 +120,12 @@ const RateAppSheet = () => {
fontSize={SIZE.md}
type="error"
width="48%"
height={50}
title="Never"
/>
<Button
onPress={onClose}
fontSize={SIZE.md}
width="48%"
height={50}
type="grayBg"
title="Later"
/>

View File

@@ -43,8 +43,7 @@ import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
let RNFetchBlob;
import RNFetchBlob from "react-native-blob-util";
class RecoveryKeySheet extends React.Component {
constructor(props) {
@@ -115,7 +114,6 @@ class RecoveryKeySheet extends React.Component {
this.svg.current?.toDataURL(async (data) => {
try {
let path;
RNFetchBlob = (await import("rn-fetch-blob")).default;
let fileName = "nn_" + this.user.email + "_recovery_key_qrcode";
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".png";
@@ -151,7 +149,6 @@ class RecoveryKeySheet extends React.Component {
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".txt";
RNFetchBlob = (await import("rn-fetch-blob")).default;
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
fileName,
@@ -298,7 +295,6 @@ class RecoveryKeySheet extends React.Component {
width="100%"
type="grayAccent"
fontSize={SIZE.md}
height={50}
/>
<Seperator />
<Button
@@ -308,7 +304,6 @@ class RecoveryKeySheet extends React.Component {
type="grayAccent"
fontSize={SIZE.md}
icon="qrcode"
height={50}
/>
<Seperator />
<Button
@@ -318,7 +313,6 @@ class RecoveryKeySheet extends React.Component {
type="grayAccent"
icon="text"
fontSize={SIZE.md}
height={50}
/>
<Seperator />
@@ -329,7 +323,6 @@ class RecoveryKeySheet extends React.Component {
type="grayAccent"
icon="cloud"
fontSize={SIZE.md}
height={50}
/>
<Seperator />
@@ -349,7 +342,6 @@ class RecoveryKeySheet extends React.Component {
<Button
title="I have saved the key."
width="100%"
height={50}
type="error"
fontSize={SIZE.md}
onPress={this.close}

View File

@@ -18,12 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
PresentSheetOptions,
presentSheet
presentSheet,
PresentSheetOptions
} from "../../../services/event-manager";
import { Reminder } from "../../../services/notifications";
import { useRelationStore } from "../../../stores/use-relation-store";
@@ -37,7 +38,7 @@ import { PressableButtonProps } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
type RelationsListProps = {
actionSheetRef: RefObject<ActionSheet>;
actionSheetRef: RefObject<ActionSheetRef>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
item: { id: string; type: string };
@@ -62,8 +63,6 @@ const IconsByType = {
export const RelationsList = ({
actionSheetRef,
close,
update,
item,
referenceType,
relationType,
@@ -75,7 +74,6 @@ export const RelationsList = ({
const [items, setItems] = useState<Reminder[]>([]);
const colors = useThemeStore((state) => state.colors);
const hasNoRelations = !items || items.length === 0;
useEffect(() => {
setItems(
db.relations?.[relationType]?.(
@@ -123,6 +121,7 @@ export const RelationsList = ({
) : (
<List
listData={items}
ScrollComponent={FlashList}
loading={false}
type={referenceType}
headerProps={null}

View File

@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import dayjs from "dayjs";
import React, { RefObject } from "react";
import { ScrollView, View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
@@ -36,7 +36,7 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheet>;
actionSheetRef: RefObject<ActionSheetRef>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
reminder?: Reminder;
@@ -119,7 +119,7 @@ export default function ReminderNotify({
marginTop: 10
}}
>
<Paragraph size={SIZE.xs + 1}>Remind me in:</Paragraph>
<Paragraph size={SIZE.xs}>Remind me in:</Paragraph>
{QuickActions.map((item) => {
return (
<Button
@@ -127,7 +127,7 @@ export default function ReminderNotify({
key={item.title}
title={item.title}
height={30}
fontSize={SIZE.xs + 1}
fontSize={SIZE.xs}
style={{ marginLeft: 10, borderRadius: 100 }}
onPress={() => onSnooze(item.time)}
/>
@@ -150,7 +150,7 @@ export default function ReminderNotify({
<Paragraph
style={{
color: colors.icon,
fontSize: SIZE.xs + 1,
fontSize: SIZE.xs,
marginBottom: 10
}}
>

View File

@@ -17,19 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { RefObject, useRef, useState } from "react";
import {
Platform,
ScrollView,
TextInput,
useWindowDimensions,
View
} from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { Platform, TextInput, View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
presentSheet,
PresentSheetOptions,
ToastEvent
ToastEvent,
presentSheet
} from "../../../services/event-manager";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
@@ -39,18 +33,20 @@ import Input from "../../ui/input";
import dayjs from "dayjs";
import DatePicker from "react-native-date-picker";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import Navigation from "../../../services/navigation";
import Notifications, { Reminder } from "../../../services/notifications";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useRelationStore } from "../../../stores/use-relation-store";
import { ReminderTime } from "../../ui/reminder-time";
import Paragraph from "../../ui/typography/paragraph";
import { NoteType } from "../../../utils/types";
import { Dialog } from "../../dialog";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheet>;
actionSheetRef: RefObject<ActionSheetRef>;
close?: (ctx?: string) => void;
update?: (options: PresentSheetOptions) => void;
reminder?: Reminder;
@@ -117,13 +113,15 @@ export default function ReminderSheet({
>(reminder?.priority || SettingsService.get().reminderNotificationMode);
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [repeatFrequency, setRepeatFrequency] = useState(1);
const title = useRef<string | undefined>(reminder?.title);
const details = useRef<string | undefined>(reminder?.description);
const titleRef = useRef<TextInput>(null);
const { height } = useWindowDimensions();
const referencedItem = reference
? (db.notes?.note(reference.id)?.data as NoteType)
: null;
const title = useRef<string | undefined>(
reminder?.title || referencedItem?.title
);
const details = useRef<string | undefined>(reminder?.description);
const titleRef = useRef<TextInput>(null);
const timer = useRef<NodeJS.Timeout>();
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -134,9 +132,10 @@ export default function ReminderSheet({
};
const handleConfirm = (date: Date) => {
hideDatePicker();
setDate(date);
console.log(date);
timer.current = setTimeout(() => {
hideDatePicker();
setDate(date);
}, 50);
};
function nth(n: number) {
return (
@@ -201,7 +200,8 @@ export default function ReminderSheet({
if (!_reminder) {
ToastEvent.show({
heading: "Failed to add a new reminder"
heading: "Failed to add a new reminder",
context: "local"
});
}
if (reference) {
@@ -211,57 +211,67 @@ export default function ReminderSheet({
});
}
Notifications.scheduleNotification(_reminder as Reminder);
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"Notes",
"NotesPage",
"Reminders",
"Favorites",
"TopicNotes"
);
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
close?.();
} catch (e) {
ToastEvent.error(e as Error);
ToastEvent.error(e as Error, undefined, "local");
}
}
return (
<View
style={{
paddingHorizontal: 12
paddingHorizontal: 12,
maxHeight: DDS.isTab ? "90%" : "99.99%"
}}
>
<Dialog context="local"/>
<ScrollView
onScrollEndDrag={() => actionSheetRef.current?.handleChildScrollEnd()}
<View
style={{
maxHeight: height * 0.85
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>Set reminder</Heading>
<Button
title="Save"
type="accent"
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24
}}
onPress={saveReminder}
/>
</View>
<Dialog context="local" />
<ScrollView>
<Input
fwdRef={titleRef}
defaultValue={reminder?.title || referencedItem?.title}
placeholder="Remind me of..."
onChangeText={(text) => (title.current = text)}
containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
wrapperStyle={{
marginTop: 10
}}
/>
<Input
defaultValue={reminder ? reminder?.description : referencedItem?.headline}
placeholder="Add a quick note"
defaultValue={
reminder ? reminder?.description : referencedItem?.headline
}
placeholder="Add a short note"
onChangeText={(text) => (details.current = text)}
containerStyle={{
borderWidth: 0,
borderBottomWidth: 1,
maxHeight: 80,
marginTop: 10
maxHeight: 80
}}
multiline
textAlignVertical="top"
inputStyle={{
height: 80
minHeight: 80,
paddingVertical: 12
}}
height={80}
wrapperStyle={{
@@ -299,6 +309,16 @@ export default function ReminderSheet({
mode as keyof typeof ReminderModes
] as Reminder["mode"]
);
if (mode === "Repeat") {
setSelectedDays((days) => {
if (days.length > 0) return days;
if (days.indexOf(date.getDay()) > -1) {
return days;
}
days.push(date.getDay());
return [...days];
});
}
}}
/>
))}
@@ -440,11 +460,13 @@ export default function ReminderSheet({
<DatePicker
date={date}
maximumDate={dayjs(date).add(3, "months").toDate()}
onDateChange={handleConfirm}
textColor={colors.night ? "#ffffff" : "#000000"}
fadeToColor={colors.bg}
theme={colors.night ? "dark" : "light"}
is24hourSource="locale"
androidVariant="nativeAndroid"
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
/>
@@ -527,7 +549,7 @@ export default function ReminderSheet({
}}
>
<>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
<Paragraph size={SIZE.xs} color={colors.icon}>
{recurringMode === RecurringModes.Daily
? "Repeats daily " + `at ${dayjs(date).format("hh:mm A")}.`
: selectedDays.length === 7 &&
@@ -562,15 +584,6 @@ export default function ReminderSheet({
}}
/>
</ScrollView>
<Button
style={{
width: "100%"
}}
title="Save"
type="accent"
fontSize={SIZE.md}
onPress={saveReminder}
/>
</View>
);
}
@@ -582,6 +595,8 @@ ReminderSheet.present = (
) => {
presentSheet({
context: isSheet ? "local" : undefined,
enableGesturesInScrollView: true,
noBottomPadding: true,
component: (ref, close, update) => (
<ReminderSheet
actionSheetRef={ref}

View File

@@ -18,24 +18,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { EVENTS } from "@notesnook/core/common";
import React, { createRef, useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import RNFetchBlob from "react-native-blob-util";
import DocumentPicker from "react-native-document-picker";
import { FlatList } from "react-native-gesture-handler";
import * as ScopedStorage from "react-native-scoped-storage";
import { db } from "../../../common/database";
import storage from "../../../common/database/storage";
import {
ToastEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
eUnSubscribeEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { initialize } from "../../../stores";
import { useThemeStore } from "../../../stores/use-theme-store";
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep, timeConverter } from "../../../utils/time";
import { getFormattedDate } from "../../../utils/time";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
@@ -44,12 +45,18 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
const actionSheetRef = createRef();
let RNFetchBlob;
const RestoreDataSheet = () => {
const [visible, setVisible] = useState(false);
const [restoring, setRestoring] = useState(false);
const sheet = useRef();
useEffect(() => {
const open = async () => {
setVisible(true);
setTimeout(() => {
sheet.current?.show();
}, 1);
};
eSubscribeEvent(eOpenRestoreDialog, open);
eSubscribeEvent(eCloseRestoreDialog, close);
return () => {
@@ -58,21 +65,15 @@ const RestoreDataSheet = () => {
};
}, [close]);
const open = async () => {
setVisible(true);
await sleep(30);
actionSheetRef.current?.setModalVisible(true);
};
const close = useCallback(() => {
if (restoring) {
showIsWorking();
return;
}
actionSheetRef.current?.setModalVisible(false);
sheet.current?.hide();
setTimeout(() => {
setVisible(false);
}, 300);
}, 150);
}, [restoring]);
const showIsWorking = () => {
@@ -86,15 +87,19 @@ const RestoreDataSheet = () => {
return !visible ? null : (
<SheetWrapper
fwdRef={actionSheetRef}
fwdRef={sheet}
gestureEnabled={!restoring}
closeOnTouchBackdrop={!restoring}
onClose={close}
onClose={() => {
setVisible(false);
close();
}}
>
<RestoreDataComponent
close={close}
restoring={restoring}
setRestoring={setRestoring}
actionSheetRef={sheet}
/>
<Toast context="local" />
</SheetWrapper>
@@ -109,7 +114,6 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
const [loading, setLoading] = useState(true);
const [backupDirectoryAndroid, setBackupDirectoryAndroid] = useState(false);
const [progress, setProgress] = useState();
useEffect(() => {
const subscription = db.eventManager.subscribe(
EVENTS.migrationProgress,
@@ -123,7 +127,9 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}, []);
useEffect(() => {
checkBackups();
setTimeout(() => {
checkBackups();
}, 300);
}, []);
const restore = async (item) => {
@@ -208,7 +214,6 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
return;
}
} else {
RNFetchBlob = (await import("rn-fetch-blob")).default;
let path = await storage.checkAndCreateDir("/backups/");
files = await RNFetchBlob.fs.lstat(path);
}
@@ -246,7 +251,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}}
>
<Paragraph size={SIZE.sm} style={{ width: "100%", maxWidth: "100%" }}>
{timeConverter(item?.lastModified * 1)}
{getFormattedDate(item?.lastModified * 1)}
</Paragraph>
<Paragraph size={SIZE.xs}>
{(item.filename || item.name).replace(".nnbackup", "")}
@@ -362,10 +367,6 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
</View>
<Seperator half />
<FlatList
nestedScrollEnabled
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
!restoring ? (
loading ? (

View File

@@ -25,23 +25,22 @@ import Navigation from "../../../services/navigation";
import { useThemeStore } from "../../../stores/use-theme-store";
import { GROUP, SORT } from "../../../utils/constants";
import { refreshNotesPage } from "../../../utils/events";
import layoutmanager from "../../../utils/layout-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
const Sort = ({ type, screen }) => {
const colors = useThemeStore((state) => state.colors);
const isTopicSheet = screen === "TopicSheet";
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(type)
);
const updateGroupOptions = async (_groupOptions) => {
await db.settings.setGroupOptions(type, _groupOptions);
layoutmanager.withSpringAnimation(600);
setGroupOptions(_groupOptions);
setTimeout(() => {
Navigation.queueRoutesForUpdate(screen);
if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen);
eSendEvent("groupOptionsUpdate");
eSendEvent(refreshNotesPage);
}, 1);
@@ -52,6 +51,9 @@ const Sort = ({ type, screen }) => {
...groupOptions,
sortDirection: groupOptions.sortDirection === "asc" ? "desc" : "asc"
};
if (type === "topics") {
_groupOptions.groupBy = "none";
}
await updateGroupOptions(_groupOptions);
};
@@ -100,7 +102,7 @@ const Sort = ({ type, screen }) => {
height={25}
iconPosition="right"
fontSize={SIZE.sm - 1}
type="grayBg"
type="transparent"
buttonType={{
text: colors.accent
}}
@@ -119,7 +121,7 @@ const Sort = ({ type, screen }) => {
flexDirection: "row",
justifyContent: "flex-start",
flexWrap: "wrap",
borderBottomWidth: 1,
borderBottomWidth: isTopicSheet ? 0 : 1,
borderBottomColor: colors.nav,
marginBottom: 12,
paddingHorizontal: 12,
@@ -166,6 +168,9 @@ const Sort = ({ type, screen }) => {
...groupOptions,
sortBy: type === "trash" ? "dateDeleted" : item
};
if (type === "topics") {
_groupOptions.groupBy = "none";
}
await updateGroupOptions(_groupOptions);
}}
iconSize={SIZE.md}
@@ -175,64 +180,70 @@ const Sort = ({ type, screen }) => {
)}
</View>
<Heading
style={{
marginLeft: 12
}}
size={SIZE.lg}
>
Group by
</Heading>
<Seperator />
<View
style={{
borderRadius: 0,
flexDirection: "row",
flexWrap: "wrap",
paddingHorizontal: 12
}}
>
{Object.keys(GROUP).map((item) => (
<Button
key={item}
testID={"btn-" + item}
type={groupOptions.groupBy === GROUP[item] ? "grayBg" : "gray"}
buttonType={{
text:
groupOptions.groupBy === GROUP[item]
? colors.accent
: colors.icon
}}
onPress={async () => {
let _groupOptions = {
...groupOptions,
groupBy: GROUP[item]
};
if (item === "abc") {
_groupOptions.sortBy = "title";
_groupOptions.sortDirection = "asc";
} else {
if (groupOptions.sortBy === "title") {
_groupOptions.sortBy = "dateEdited";
_groupOptions.sortDirection = "desc";
}
}
updateGroupOptions(_groupOptions);
}}
height={40}
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
title={item.slice(0, 1).toUpperCase() + item.slice(1, item.length)}
{isTopicSheet ? null : (
<>
<Heading
style={{
paddingHorizontal: 8,
marginBottom: 10,
marginRight: 10
marginLeft: 12
}}
/>
))}
</View>
size={SIZE.lg}
>
Group by
</Heading>
<Seperator />
<View
style={{
borderRadius: 0,
flexDirection: "row",
flexWrap: "wrap",
paddingHorizontal: 12
}}
>
{Object.keys(GROUP).map((item) => (
<Button
key={item}
testID={"btn-" + item}
type={groupOptions.groupBy === GROUP[item] ? "grayBg" : "gray"}
buttonType={{
text:
groupOptions.groupBy === GROUP[item]
? colors.accent
: colors.icon
}}
onPress={async () => {
let _groupOptions = {
...groupOptions,
groupBy: GROUP[item]
};
if (item === "abc") {
_groupOptions.sortBy = "title";
_groupOptions.sortDirection = "asc";
} else {
if (groupOptions.sortBy === "title") {
_groupOptions.sortBy = "dateEdited";
_groupOptions.sortDirection = "desc";
}
}
updateGroupOptions(_groupOptions);
}}
height={40}
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
title={
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
}
style={{
paddingHorizontal: 8,
marginBottom: 10,
marginRight: 10
}}
/>
))}
</View>
</>
)}
</View>
);
};

View File

@@ -0,0 +1,555 @@
/*
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 qclone from "qclone";
import React, {
createContext,
RefObject,
useCallback,
useContext,
useEffect,
useRef,
useState
} from "react";
import { RefreshControl, View } from "react-native";
import ActionSheet, {
ActionSheetRef,
FlatList
} from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { IconButton } from "../../../components/ui/icon-button";
import { PressableButton } from "../../../components/ui/pressable";
import Paragraph from "../../../components/ui/typography/paragraph";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet
} from "../../../services/event-manager";
import useNavigationStore, {
NotebookScreenParams
} from "../../../stores/use-navigation-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import {
eOnNewTopicAdded,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { normalize, SIZE } from "../../../utils/size";
import { GroupHeader, NotebookType, TopicType } from "../../../utils/types";
import { groupArray } from "@notesnook/core/utils/grouping";
import Config from "react-native-config";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { MMKV } from "../../../common/database/mmkv";
import { openEditor } from "../../../screens/notes/common";
import { getTotalNotes, history } from "../../../utils";
import { deleteItems } from "../../../utils/functions";
import { presentDialog } from "../../dialog/functions";
import { Properties } from "../../properties";
import Sort from "../sort";
type ConfigItem = { id: string; type: string };
class TopicSheetConfig {
static storageKey: "$$sp";
static makeId(item: ConfigItem) {
return `${TopicSheetConfig.storageKey}:${item.type}:${item.id}`;
}
static get(item: ConfigItem) {
return MMKV.getInt(TopicSheetConfig.makeId(item)) || 0;
}
static set(item: ConfigItem, index = 0) {
MMKV.setInt(TopicSheetConfig.makeId(item), index);
}
}
export const TopicsSheet = () => {
const [collapsed, setCollapsed] = useState(false);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const canShow =
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes";
const [notebook, setNotebook] = useState(
canShow
? db.notebooks?.notebook(
currentScreen?.notebookId || currentScreen?.id || ""
)?.data
: null
);
const [selection, setSelection] = useState<TopicType[]>([]);
const [enabled, setEnabled] = useState(false);
const colors = useThemeStore((state) => state.colors);
const ref = useRef<ActionSheetRef>(null);
const isTopic = currentScreen.name === "TopicNotes";
const [topics, setTopics] = useState(
notebook
? qclone(
groupArray(notebook.topics, db.settings?.getGroupOptions("topics"))
)
: []
);
const [groupOptions, setGroupOptions] = useState(
db.settings?.getGroupOptions("topics")
);
const onRequestUpdate = React.useCallback(
(data?: NotebookScreenParams) => {
if (!canShow) return;
if (!data) data = { item: notebook } as NotebookScreenParams;
const _notebook = db.notebooks?.notebook(data.item?.id)
?.data as NotebookType;
if (_notebook) {
setNotebook(_notebook);
setTopics(
qclone(
groupArray(_notebook.topics, db.settings?.getGroupOptions("topics"))
)
);
}
},
[canShow, notebook]
);
const onUpdate = useCallback(() => {
setGroupOptions({ ...(db.settings?.getGroupOptions("topics") as any) });
onRequestUpdate();
}, [onRequestUpdate]);
useEffect(() => {
eSubscribeEvent("groupOptionsUpdate", onUpdate);
return () => {
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
};
}, [onUpdate]);
useEffect(() => {
const onTopicUpdate = () => {
setTimeout(() => {
onRequestUpdate();
}, 1);
};
eSubscribeEvent(eOnTopicSheetUpdate, onTopicUpdate);
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
return () => {
eUnSubscribeEvent(eOnTopicSheetUpdate, onRequestUpdate);
eUnSubscribeEvent(eOnNewTopicAdded, onTopicUpdate);
};
}, [onRequestUpdate]);
const PLACEHOLDER_DATA = {
heading: "Topics",
paragraph: "You have not added any topics yet.",
button: "Add first topic",
action: () => {
eSendEvent(eOpenAddTopicDialog, { notebookId: notebook.id });
},
loading: "Loading notebook topics"
};
const renderTopic = ({
item,
index
}: {
item: TopicType | GroupHeader;
index: number;
}) =>
(item as GroupHeader).type === "header" ? null : (
<TopicItem sheetRef={ref} item={item as TopicType} index={index} />
);
const selectionContext = {
selection: selection,
enabled,
setEnabled,
toggleSelection: (item: TopicType) => {
setSelection((state) => {
const selection = [...state];
const index = selection.findIndex(
(selected) => selected.id === item.id
);
if (index > -1) {
selection.splice(index, 1);
if (selection.length === 0) {
setEnabled(false);
}
return selection;
}
selection.push(item);
return selection;
});
}
};
useEffect(() => {
if (canShow) {
setTimeout(() => {
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
const notebook = db.notebooks?.notebook(id as string)?.data;
const snapPoint = isTopic
? 0
: TopicSheetConfig.get({
type: isTopic ? "topic" : "notebook",
id: currentScreen.id as string
});
if (ref.current?.isOpen()) {
ref.current?.snapToIndex(snapPoint);
} else {
ref.current?.show(snapPoint);
}
if (notebook) {
onRequestUpdate({
item: notebook
} as any);
}
}, 300);
} else {
ref.current?.hide();
}
}, [
canShow,
currentScreen?.id,
currentScreen.name,
currentScreen?.notebookId,
onRequestUpdate,
isTopic
]);
return (
<ActionSheet
ref={ref}
isModal={false}
containerStyle={{
maxHeight: 300,
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: colors.bg,
borderWidth: 1,
borderColor: colors.nav,
borderBottomWidth: 0
}}
openAnimationConfig={{
friction: 10
}}
onSnapIndexChange={(index) => {
setCollapsed(index === 0);
TopicSheetConfig.set(
{
type: isTopic ? "topic" : "notebook",
id: currentScreen.id as string
},
index
);
}}
closable={!canShow}
elevation={10}
indicatorStyle={{
width: 100,
backgroundColor: colors.nav
}}
keyboardHandlerEnabled={false}
snapPoints={Config.isTesting === "true" ? [100] : [25, 100]}
initialSnapIndex={1}
backgroundInteractionEnabled
gestureEnabled
>
<View
style={{
position: "absolute",
right: 12,
marginTop: -80
}}
>
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={"accent"}
accentText="light"
onPress={openEditor}
customStyle={{
borderRadius: 100
}}
>
<View
style={{
alignItems: "center",
justifyContent: "center",
height: normalize(60),
width: normalize(60)
}}
>
<Icon name="plus" color="white" size={SIZE.xxl} />
</View>
</PressableButton>
</View>
<View
style={{
maxHeight: 300,
height: 300,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 12,
alignItems: "center"
}}
>
<Paragraph size={SIZE.xs} color={colors.icon}>
TOPICS
</Paragraph>
<View
style={{
flexDirection: "row"
}}
>
{enabled ? (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={async () => {
//@ts-ignore
history.selectedItemsList = selection;
presentDialog({
title: `Delete ${
selection.length > 1 ? "topics" : "topics"
}`,
paragraph: `Are you sure you want to delete ${
selection.length > 1 ? "these topics?" : "this topic?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
await deleteItems();
history.selectedItemsList = [];
setEnabled(false);
setSelection([]);
},
positiveType: "errorShade"
});
return;
}}
color={colors.pri}
tooltipText="Move to trash"
tooltipPosition={1}
name="delete"
size={22}
/>
) : (
<>
<IconButton
name={
groupOptions?.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
onPress={() => {
presentSheet({
component: <Sort screen="TopicSheet" type="topics" />
});
}}
testID="group-topic-button"
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
<IconButton
name="plus"
onPress={PLACEHOLDER_DATA.action}
testID="add-topic-button"
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
<IconButton
name={collapsed ? "chevron-up" : "chevron-down"}
onPress={() => {
if (ref.current?.currentSnapIndex() !== 0) {
setCollapsed(true);
ref.current?.snapToIndex(0);
} else {
setCollapsed(false);
ref.current?.snapToIndex(1);
}
}}
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
</>
)}
</View>
</View>
<SelectionContext.Provider value={selectionContext}>
<FlatList
data={topics}
style={{
width: "100%"
}}
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
onRequestUpdate();
}}
colors={[colors.accent]}
progressBackgroundColor={colors.bg}
/>
}
keyExtractor={(item) => (item as TopicType).id}
renderItem={renderTopic}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Paragraph color={colors.icon}>No topics</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</SelectionContext.Provider>
</View>
</ActionSheet>
);
};
const SelectionContext = createContext<{
selection: TopicType[];
enabled: boolean;
setEnabled: (value: boolean) => void;
toggleSelection: (item: TopicType) => void;
}>({
selection: [],
enabled: false,
setEnabled: (_value: boolean) => {},
toggleSelection: (_item: TopicType) => {}
});
const useSelection = () => useContext(SelectionContext);
const TopicItem = ({
item,
index,
sheetRef
}: {
item: TopicType;
index: number;
sheetRef: RefObject<ActionSheetRef>;
}) => {
const screen = useNavigationStore((state) => state.currentScreen);
const colors = useThemeStore((state) => state.colors);
const selection = useSelection();
const isSelected =
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
const isFocused = screen.id === item.id;
const notesCount = getTotalNotes(item);
return (
<PressableButton
type={isSelected || isFocused ? "grayBg" : "transparent"}
onLongPress={() => {
if (selection.enabled) return;
selection.setEnabled(true);
selection.toggleSelection(item);
}}
testID={`topic-sheet-item-${index}`}
onPress={() => {
if (selection.enabled) {
selection.toggleSelection(item);
return;
}
TopicNotes.navigate(item, true);
}}
customStyle={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 12,
borderRadius: 0
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{selection.enabled ? (
<IconButton
size={SIZE.lg}
color={isSelected ? colors.accent : colors.icon}
name={
isSelected
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
) : null}
<Paragraph size={SIZE.sm}>
{item.title}{" "}
{notesCount ? (
<Paragraph size={SIZE.xs} color={colors.icon}>
{notesCount}
</Paragraph>
) : null}
</Paragraph>
</View>
<IconButton
name="dots-horizontal"
customStyle={{
width: 40,
height: 40
}}
testID={notesnook.ids.notebook.menu}
onPress={() => {
Properties.present(item);
}}
left={0}
right={0}
bottom={0}
top={0}
color={colors.pri}
size={SIZE.xl}
/>
</PressableButton>
);
};

View File

@@ -19,10 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import { Linking, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { checkVersion } from "react-native-check-version";
import Config from "react-native-config";
import deviceInfoModule from "react-native-device-info";
import { ScrollView } from "react-native-gesture-handler";
import { useThemeStore } from "../../../stores/use-theme-store";
import { STORE_LINK } from "../../../utils/constants";
import { SIZE } from "../../../utils/size";
@@ -148,9 +148,6 @@ export const Update = ({ version: appVersion, fwdRef }) => {
<Seperator />
<ScrollView
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
fwdRef?.current?.handleChildScrollEnd();
}}
style={{
width: "100%"
}}

View File

@@ -164,7 +164,6 @@ export const PinItem = React.memo(
}}
fontSize={SIZE.md}
width="95%"
height={50}
customStyle={{
marginBottom: 30
}}

View File

@@ -302,6 +302,7 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = value;
})
.onEnd((event) => {
if (locked.value || forcedLock.value) return;
if (currentTab.value === 2 && Platform.OS === "android") return;
const velocityX =
event.velocityX < 0 ? event.velocityX * -1 : event.velocityX;

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
@@ -34,24 +34,25 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
let toastMessages = [];
export const Toast = ({ context = "global" }) => {
const colors = useThemeStore((state) => state.colors);
const [keyboard, setKeyboard] = useState(false);
const [data, setData] = useState({});
const insets = useGlobalSafeAreaInsets();
const hideTimeout = useRef();
const [visible, setVisible] = useState(false);
const toastMessages = useRef([]);
const showToastFunc = useCallback(
const show = useCallback(
async (data) => {
if (!data) return;
if (data.context !== context) return;
if (toastMessages.findIndex((m) => m.message === data.message) >= 0) {
if (
toastMessages.current.findIndex((m) => m.message === data.message) >= 0
) {
return;
}
toastMessages.push(data);
if (toastMessages?.length > 1) return;
toastMessages.current.push(data);
if (toastMessages.current?.length > 1) return;
setData(data);
setVisible(true);
@@ -59,16 +60,16 @@ export const Toast = ({ context = "global" }) => {
clearTimeout(hideTimeout.current);
}
hideTimeout.current = setTimeout(() => {
hideToastFunc();
hide();
}, data.duration);
},
[context, hideToastFunc]
[context, hide]
);
const showNext = useCallback(
const next = useCallback(
(data) => {
if (!data) {
hideToastFunc();
hide();
return;
}
setData(data);
@@ -76,27 +77,28 @@ export const Toast = ({ context = "global" }) => {
clearTimeout(hideTimeout.current);
}
hideTimeout.current = setTimeout(() => {
hideToastFunc();
hide();
}, data?.duration);
},
[hideToastFunc]
[hide]
);
const hideToastFunc = useCallback(() => {
const hide = useCallback(() => {
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
let msg = toastMessages.length > 1 ? toastMessages.shift() : null;
let msg =
toastMessages.current.length > 1 ? toastMessages.current.shift() : null;
if (msg) {
setVisible(false);
showNext(msg);
next(msg);
setTimeout(() => {
setVisible(true);
}, 300);
} else {
setVisible(false);
toastMessages.shift();
toastMessages.current.shift();
setTimeout(() => {
setData({});
if (hideTimeout.current) {
@@ -104,44 +106,22 @@ export const Toast = ({ context = "global" }) => {
}
}, 100);
}
}, [showNext]);
const _onKeyboardShow = () => {
setKeyboard(true);
};
const _onKeyboardHide = () => {
setKeyboard(false);
};
}, [next]);
useEffect(() => {
toastMessages = [];
let sub1 = Keyboard.addListener("keyboardDidShow", _onKeyboardShow);
let sub2 = Keyboard.addListener("keyboardDidHide", _onKeyboardHide);
eSubscribeEvent(eShowToast, showToastFunc);
eSubscribeEvent(eHideToast, hideToastFunc);
eSubscribeEvent(eShowToast, show);
eSubscribeEvent(eHideToast, hide);
return () => {
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
toastMessages = [];
sub1?.remove();
sub2?.remove();
eUnSubscribeEvent(eShowToast, showToastFunc);
eUnSubscribeEvent(eHideToast, hideToastFunc);
toastMessages.current = [];
eUnSubscribeEvent(eShowToast, show);
eUnSubscribeEvent(eHideToast, hide);
};
}, [hideToastFunc, keyboard, showToastFunc]);
}, [hide, show]);
return (
visible && (
<TouchableOpacity
onPress={() => {
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
hideToastFunc();
}}
onPress={hide}
activeOpacity={1}
style={{
width: DDS.isTab ? 400 : "100%",
@@ -209,7 +189,7 @@ export const Toast = ({ context = "global" }) => {
color={colors.pri}
size={SIZE.md}
onPress={() => {
hideToastFunc();
hide();
}}
>
{data.heading}
@@ -224,7 +204,7 @@ export const Toast = ({ context = "global" }) => {
paddingRight: 10
}}
onPress={() => {
hideToastFunc();
hide();
}}
>
{data.message}

View File

@@ -40,6 +40,8 @@ import { getElevation } from "../../../utils";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../icon-button";
import Paragraph from "../typography/paragraph";
import phone from "phone";
interface InputProps extends TextInputProps {
fwdRef?: RefObject<TextInput>;
validationType?:
@@ -92,7 +94,7 @@ const Input = ({
button,
onBlurInput,
onPress,
height = 50,
height = 45,
fontSize = SIZE.md,
onFocusInput,
buttons,
@@ -117,7 +119,7 @@ const Input = ({
? colors.red
: focus
? customColor || colors.accent
: colors.nav;
: colors.border;
const validate = async (value: string) => {
if (!validationType) return;
@@ -149,7 +151,6 @@ const Input = ({
isError = customValidator && value === customValidator();
break;
case "phonenumber": {
const { default: phone } = await import("phone");
const result = phone(value, {
strictDetection: true,
validateMobilePrefix: true

View File

@@ -65,7 +65,7 @@ export const Notice = ({
flexShrink: 1
}}
selectable={selectable}
size={isSmall ? SIZE.xs + 1 : SIZE.sm}
size={isSmall ? SIZE.xs : SIZE.sm}
>
{text}
</Paragraph>

View File

@@ -16,17 +16,15 @@ 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 { isReminderActive } from "@notesnook/core/collections/reminders";
import React from "react";
import {
formatReminderTime,
isReminderActive
} from "@notesnook/core/collections/reminders";
import { ViewStyle } from "react-native";
import { Reminder } from "../../../services/notifications";
import { Button, ButtonProps } from "../button";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { getFormattedReminderTime } from "../../../utils/time";
import { Button, ButtonProps } from "../button";
export const ReminderTime = ({
checkIsActive = true,
@@ -42,7 +40,7 @@ export const ReminderTime = ({
} & ButtonProps) => {
const colors = useThemeStore((state) => state.colors);
const reminder = props.reminder;
const time = !reminder ? undefined : formatReminderTime(reminder);
const time = !reminder ? undefined : getFormattedReminderTime(reminder);
const isTodayOrTomorrow =
(time?.includes("Today") || time?.includes("Tomorrow")) &&
!time?.includes("Last");

View File

@@ -37,7 +37,9 @@ const SheetWrapper = ({
onHasReachedTop,
keyboardMode,
overlay,
overlayOpacity = 0.3
overlayOpacity = 0.3,
enableGesturesInScrollView = false,
bottomPadding = true
}) => {
const colors = useThemeStore((state) => state.colors);
const deviceMode = useSettingStore((state) => state.deviceMode);
@@ -59,8 +61,8 @@ const SheetWrapper = ({
zIndex: 10,
paddingTop: 5,
paddingBottom: 0,
borderTopRightRadius: 20,
borderTopLeftRadius: 20,
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
alignSelf: "center",
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0
@@ -84,7 +86,8 @@ const SheetWrapper = ({
backdrop: "sheet-backdrop"
}}
indicatorStyle={{
width: 100
width: 100,
backgroundColor: colors.nav
}}
drawUnderStatusBar={false}
containerStyle={style}
@@ -98,9 +101,13 @@ const SheetWrapper = ({
indicatorColor={colors.nav}
onOpen={_onOpen}
keyboardDismissMode="none"
enableGesturesInScrollView={enableGesturesInScrollView}
defaultOverlayOpacity={overlayOpacity}
overlayColor={pitchBlack ? "#585858" : "#000000"}
overlayColor={pitchBlack ? "#585858" : "#2b2b2b"}
keyboardShouldPersistTaps="always"
openAnimationConfig={{
friction: 9
}}
ExtraOverlayComponent={
<>
{overlay}
@@ -116,14 +123,16 @@ const SheetWrapper = ({
>
<BouncingView>
{children}
<View
style={{
height:
Platform.OS === "ios" && insets.bottom !== 0
? insets.bottom + 5
: 20
}}
/>
{bottomPadding ? (
<View
style={{
height:
Platform.OS === "ios" && insets.bottom !== 0
? insets.bottom + 5
: 20
}}
/>
) : null}
</BouncingView>
</ActionSheet>
);

View File

@@ -46,6 +46,7 @@ export default function Tag({
marginLeft: 2,
marginTop: -10,
height: 20,
justifyContent: "center",
...style
}}
>

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useMemo } from "react";
import { Platform, Text, TextProps } from "react-native";
import { Platform, Text, TextProps, ViewStyle } from "react-native";
import Animated, {
ComplexAnimationBuilder,
Layout
@@ -30,15 +30,26 @@ interface HeadingProps extends TextProps {
size?: number;
layout?: ComplexAnimationBuilder;
animated?: boolean;
extraBold?: boolean;
}
const AnimatedText = Animated.createAnimatedComponent(Text);
const extraBoldStyle = {
fontFamily: Platform.OS === "android" ? "OpenSans-Bold" : undefined,
fontWeight: Platform.OS === "ios" ? "800" : undefined
};
const boldStyle = {
fontFamily: Platform.OS === "android" ? "OpenSans-SemiBold" : undefined,
fontWeight: Platform.OS === "ios" ? "600" : undefined
};
const Heading = ({
color,
size = SIZE.xl,
style,
animated,
extraBold,
...restProps
}: HeadingProps) => {
const colors = useThemeStore((state) => state.colors);
@@ -53,11 +64,9 @@ const Heading = ({
style={[
{
fontSize: size || SIZE.xl,
color: color || colors.heading,
fontFamily:
Platform.OS === "android" ? "OpenSans-SemiBold" : undefined,
fontWeight: Platform.OS === "ios" ? "600" : undefined
color: color || colors.heading
},
extraBold ? (extraBoldStyle as ViewStyle) : (boldStyle as ViewStyle),
style
]}
></Component>

View File

@@ -19,4 +19,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [];
export const features: FeatureType[] = [
{
title: "File & image sharing",
body: "You can now share images and files from other apps to Notesnook and sync them instantly."
},
{
title: "Configurable date/time formats",
body: "You can now change date and time format across the app from Settings > Behaviour"
},
{
title: "Change default note title",
body: "You can now change default note title for new notes using different templates like $date$, $time$ and $headline$. Go to Settings > Editor to try"
},
{
title: "Default notebook/topic",
body: "You can now set a default notebook or topic to add new notes to automatically. You can make a notebook default from properties."
}
];

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