Compare commits

...

432 Commits

Author SHA1 Message Date
c3ec7721ac feat: multiple blossom servers ()
Closes 

Reviewed-on: 
Co-authored-by: Stixx <stixx@nostrdev.com>
Co-committed-by: Stixx <stixx@nostrdev.com>
2025-04-15 08:02:56 +00:00
b
34f5ce38ed Merge branch 'main' into staging 2025-04-04 12:27:41 +00:00
b
ce19d0e50d chore: pipeline autoaccept install 2025-04-04 12:27:07 +00:00
b
dd47c28d18 Merge pull request 'Release' () from staging into main
Reviewed-on: 
2025-04-04 12:22:46 +00:00
b
a3698e7dec Merge branch 'main' into staging 2025-04-04 12:17:54 +00:00
tbk
13044d6b39 feat: enable pwa ()
This PR addresses 2 of 3 tasks from .
- [x] It should be possible to download SIGit as a PWA on a device homescreen.
- [x] This app should self-update

Co-authored-by: theborakompanioni <theborakompanioni+github@gmail.com>
Co-authored-by: b <b@4j.cx>
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
Co-authored-by: tbk <theborakompanioni+nostrdev@gmail.com>
Co-committed-by: tbk <theborakompanioni+nostrdev@gmail.com>
2025-04-03 11:40:04 +00:00
b
b1d86b3c33 Merge pull request 'chore(deps): run audit fix' () from tbk/deps-audit-fix into staging
Reviewed-on: 
2025-04-03 11:16:20 +00:00
theborakompanioni
dd2aa3dc40
chore(deps): run audit fix 2025-04-02 19:37:27 +02:00
ec9c4dad5d chore(git): merge pull request from fixes-7-3-25 into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2025-03-11 11:11:02 +00:00
en
493390bdc1 chore(deps): bump axios from 1.7.4 to 1.8.2 2025-03-11 11:05:53 +00:00
en
4f5dcc0336 refactor(hooks): add comments to local storage hook 2025-03-11 10:58:19 +00:00
en
afdc9449b1 refactor(settings): remove base settings page, go directly to profile 2025-03-10 12:36:47 +00:00
en
c1a9475a89 refactor(settings): update settings layout 2025-03-10 11:57:08 +00:00
en
745ba377d4 refactor(settings): remove cache links and page 2025-03-10 10:56:36 +00:00
en
8de86aac28 fix(marks): date input 2025-03-10 09:44:09 +00:00
en
c8f0d135f1 feat(marks): add job title and datetime 2025-03-07 12:47:55 +00:00
en
cc681af11a feat(marks): add full name 2025-03-07 12:15:26 +00:00
en
8e23a2d8a1 refactor: remove custom sigit cache page and links 2025-03-07 11:43:01 +00:00
en
cc65d85806 refactor(styles): update css for other marks during sign 2025-03-07 11:42:09 +00:00
c399dbf56c chore(git): merge pull request from 175-local-sigit-draft into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2025-03-06 17:45:01 +00:00
en
1d6131bf82 chore(git): merge branch 'staging' into 175-local-sigit-draft 2025-03-06 17:41:52 +00:00
093416a481 chore(git): merge pull request from 92-send-completion-dm into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2025-03-06 16:11:38 +00:00
en
bb9febe25d chore(git): merge branch 'staging' into 92-send-completion-dm 2025-03-06 16:07:02 +00:00
826be97b8b chore(git): merge pull request from 308-npub-search into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2025-03-06 15:42:06 +00:00
e95fabd4ae chore(git): merge pull request from 246-sigit-buttons into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2025-03-06 15:39:37 +00:00
en
4b5625e5bd fix(search): intercept nsec1, delete, and show warning 2025-02-19 10:54:58 +01:00
en
08b13c291b fix: hide DisplaySigit actions
Closes 
2025-02-18 18:38:54 +01:00
en
f422ee338c chore: remove extra comment whitespace 2025-02-18 17:45:50 +01:00
en
9f4a891d50 feat(create): add local draft, save progress to local storage
Closes 
2025-02-18 16:56:40 +01:00
en
13b88516ca feat(draft): serialize sigit and save/load to local storage 2025-02-17 19:06:19 +01:00
en
f7d0718b78 fix(search): tim input, add timeout
Fixes 
2025-02-07 19:14:25 +01:00
en
6f4b41d84b fix(login): remove default login redirect 2025-02-07 17:16:45 +01:00
en
1474fafde7 fix(dm): don't send private DM twice to same signer 2025-02-07 14:49:25 +01:00
en
e405b735f7 fix(dm): always add sigit relay when sending private DMs 2025-02-07 14:37:08 +01:00
en
25780a6789 chore(git): merge branch 'staging' into 92-send-completion-dm 2025-02-07 11:04:37 +01:00
semantic-release-bot
6c1c97a598 chore(release): 1.0.4 [skip ci]
## [1.0.4](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.3...v1.0.4) (2025-01-31)

### Bug Fixes

* adding jq package ([673516e](673516e3ce))
2025-01-31 22:23:09 +00:00
b
a82cab36ef Merge pull request 'fix: adding jq package' () from staging into main
Reviewed-on: 
2025-01-31 22:21:04 +00:00
b
0834e52316 Merge branch 'main' into staging 2025-01-31 22:19:51 +00:00
b
673516e3ce fix: adding jq package 2025-01-31 22:17:55 +00:00
semantic-release-bot
f05b9477f6 chore(release): 1.0.3 [skip ci]
## [1.0.3](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.2...v1.0.3) (2025-01-31)

### Bug Fixes

* bundling frontend with release ([889bc0e](889bc0e4fc))
2025-01-31 21:53:55 +00:00
b
8d66573a5e Merge pull request 'fix: bundling frontend with release' () from staging into main
Reviewed-on: 
2025-01-31 21:51:43 +00:00
b
ee3381d376 Merge branch 'main' into staging 2025-01-31 21:51:36 +00:00
b
889bc0e4fc fix: bundling frontend with release 2025-01-31 21:51:04 +00:00
semantic-release-bot
8da99c0ed6 chore(release): 1.0.2 [skip ci]
## [1.0.2](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.1...v1.0.2) (2025-01-31)

### Bug Fixes

* adding api to release url ([aa32dae](aa32dae622))
2025-01-31 20:18:35 +00:00
b
896c18fff0 Merge pull request 'fix: adding api to release url' () from staging into main
Reviewed-on: 
2025-01-31 20:16:30 +00:00
b
02063e1ea5 Merge branch 'main' into staging 2025-01-31 20:16:21 +00:00
b
aa32dae622 fix: adding api to release url 2025-01-31 20:15:52 +00:00
semantic-release-bot
233dbdf7db chore(release): 1.0.1 [skip ci]
## [1.0.1](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.0...v1.0.1) (2025-01-31)

### Bug Fixes

* test to see if the automated release works ([031deef](031deef6ca))
2025-01-31 20:12:24 +00:00
b
02120d6b4c Merge pull request 'fix: test to see if the automated release works' () from staging into main
Reviewed-on: 
2025-01-31 20:10:19 +00:00
b
1c35512ddf Merge branch 'main' into staging 2025-01-31 20:09:40 +00:00
b
031deef6ca fix: test to see if the automated release works 2025-01-31 20:09:07 +00:00
b
b828e75858 Merge pull request 'chore: comments in code' () from staging into main
Reviewed-on: 
2025-01-31 20:02:25 +00:00
b
870ac9bb30 Merge branch 'main' into staging 2025-01-31 20:02:17 +00:00
b
57c1002c2a chore: comments in code 2025-01-31 20:01:39 +00:00
en
47fcdea385 chore(git): merge staging into 92-send-completion-dm 2025-01-31 19:39:56 +01:00
en
37baf57093 fix(callback): login and private route redirect
Fix 
2025-01-31 19:32:28 +01:00
semantic-release-bot
eb2aa98860 chore(release): 1.0.0 [skip ci]
# 1.0.0 (2025-01-31)

### Bug Fixes

* add default title for sigit ([ef5376e](ef5376e2d1))
* add default typography styles ([2cd851a](2cd851a7c1))
* add file and page index, hide select if not active ([5f92906](5f92906032))
* add files and marked to sign page exports ([9dd190d](9dd190d65b))
* add keys and show name for counterparts ([8267eb6](8267eb624b))
* add mark label ([c3dacbe](c3dacbe111))
* add missing null and reduce warning limit ([bec3c92](bec3c92b03))
* add parantheses, invoke unixNow ([07d25eb](07d25ebbd2))
* add Roboto font ([6a1f04e](6a1f04ec6b))
* add show username ([62c1f1b](62c1f1b37b))
* add small avatar when select is not showing ([d8d51be](d8d51be603))
* add timeout in publishing updated app data and sending notifications ([6b135ac](6b135ac54d))
* add types to rootReducer, rename userRobotImage types ([70f6464](70f646444b))
* adding link to source and updating home page wording ([c3d5a10](c3d5a1042c))
* addressing comments ([8d8c38e](8d8c38e90b))
* adds notifications ([f38344b](f38344b9ac))
* AGPL Licence, closes []() ([55abe81](55abe814c9))
* amends RelayMap to return a default sigit relay when no other relays are found ([2355da0](2355da02d2))
* amends the relay look up method to return default relay set ([52fe523](52fe523196))
* app bar z-index ([87c6807](87c6807ba0))
* arrayBuffer access ([b3fc3c6](b3fc3c6715))
* background overlap ([202c98c](202c98c94c))
* bad margin value ([734026b](734026b2ee))
* better UX when clicking on logo when on home screen or `home` button in footer ([834d70d](834d70d774))
* bug, when valid npub, clicking + was saying npub was invalid ([99fa3ad](99fa3add56))
* build failing due to type issue ([652ea06](652ea06c0d))
* button colour ([4c04c12](4c04c12403))
* card icons ([0d49c49](0d49c49459))
* center block scrolling on mark items ([aec0d0b](aec0d0bdd8))
* change sign to create ([f35f469](f35f469547))
* **ci:** add license check in staging workflow ([4af5781](4af578133c))
* **ci:** fix hook colors ([ea7fde4](ea7fde4b38))
* **CI:** fixed secret ([3e360aa](3e360aab15))
* **ci:** run lint-staged always, fix lint-stage commands ([d43067f](d43067f70e))
* clear hasSubscribed after the logout ([1d1986f](1d1986f082))
* clicking logo not redirecting to home ([69efd9e](69efd9e09d))
* clicking on marked fileds is losing input text/squiggle, squiggle field is mobile friendly ([602e23c](602e23c719))
* color scheme ([d7f9807](d7f9807e20))
* **column-layout:** wrap content column to prevent expanding ([a8020e6](a8020e6db2))
* composition for links and buttons ([804bb6c](804bb6c9ac))
* convert npub/nip05 to lowercase on adding as signer/viewer ([fff0fd7](fff0fd762d))
* counterpart search NIP05 glitching ([0fd0f26](0fd0f26fc7))
* create page, improving message "preparing document for signing" ([98fbe80](98fbe80648))
* **create-page:** file list ([1caeb48](1caeb48e6c))
* **create-page:** only show signers in counterpart select ([29e6c85](29e6c85150))
* **create-page:** show other file types in content ([b12ce25](b12ce258eb))
* **create:** block if no signers ([15aaef9](15aaef948d))
* **create:** remove small drawn fields ([902ad73](902ad73faf)), closes []()
* **create:** throw on mark with no counterpart ([624afae](624afae851))
* **create:** uploading file adds to the existing file list, dedupe file list ([6d78d9e](6d78d9ed64)), closes []()
* **deps:** update axios ([115a397](115a3974e2))
* disable login, register fields, add coming soon ([0a74ad9](0a74ad97b2))
* disables redundant metaInNavState updates ([7463384](746338465d))
* display `no results` when no submissions are found ([bbe34b6](bbe34b6011))
* displays complete marks from other users ([4d4a5b6](4d4a5b63cf))
* **DM:** removed direct download link ([0fab6b5](0fab6b5cdc))
* **draw:** add resize cursor to resize handle ([0d1a7ba](0d1a7ba171))
* **drawfield:** match label and select ([923a47b](923a47b4d0))
* **drawing:** clamp DrawField within img ([2f54184](2f54184625)), closes []()
* enable verify button ([f4a837a](f4a837ae09))
* entering decryption key manually does not work because of encoded URI ([e498ecb](e498ecb082))
* **errors:** add custom timeout error ([9c545a4](9c545a477c))
* failed DM error handling ([608400d](608400d010))
* false positive case of navigator.online ([307f32b](307f32bb7b))
* fetch app data from after login ([fa7a6e8](fa7a6e85f4))
* file path ([79f37a8](79f37a842f))
* **files:** show other file types in content for create, fix sign and verify error ([86095cb](86095cba5c))
* first find metadata on purplepag relay and then try other relays ([6981bef](6981bef65a))
* font url typo ([fcd00d9](fcd00d9e9c))
* fonts ([aa5aa60](aa5aa60c6a))
* footer 'Home' button scroll to top when on home page, fixed logic ([afbe05b](afbe05b4c8))
* footer buttons ([e280e87](e280e87342))
* footer padding and responsiveness ([45f0764](45f0764fa8))
* footer portal on relays ([ebd5947](ebd59471c7))
* format fixed for iv in encryption key ([c4ef090](c4ef090f3c))
* gap, spacing ([99856fd](99856fd8f2))
* getRobohash function will do the conversion of pubkey ([9aa1066](9aa10664a7))
* **git-hooks:** add executable flag ([7b5a122](7b5a12246d))
* handle navigation after create ([00db735](00db735106))
* handle the case when zip entry is undefined ([e4675af](e4675af4dd))
* hanlde error in decryption of zip file ([660efb3](660efb3b67))
* home screen style fixed for mobile view ([6f8830a](6f8830a77c))
* **home-page:** sigit file type display now correctly shows multiple file types ([acc8c84](acc8c84617))
* **home:** focus outlines and decorations ([72d0e06](72d0e065ea))
* homepage alpha warning ([867e1b8](867e1b88c2))
* IconButton conflict, username layout ([9dae3a4](9dae3a48be))
* icons, use FontAwesome package ([6f4737d](6f4737d75c))
* If creator is not the first signer we should not redirect to /sign page ([ee3e0e1](ee3e0e1bb1))
* improve font support ([a63ea91](a63ea913d9))
* in pdf marking if counterpart does not have any of name, displayname, username then show pubkey ([42d74c6](42d74c656a))
* In sign page, when doc is fully signed, update search params with update file url and key ([05c3f49](05c3f49a17))
* include hidden folders in surfer upload ([970c5f5](970c5f5e8b))
* include purplepage and userkindpages relays when searching for user in create page ([8a9910d](8a9910db87))
* including signatures in both export and encrypted export ([6716c3d](6716c3da63))
* increased timeout for extension user prompt ([2c2eeba](2c2eeba83f))
* inform user then search term provided no results ([24463a5](24463a53c5))
* inlined svg background images ([c22b1e4](c22b1e4b5a))
* input font-family inherit ([f21d158](f21d158a8e))
* label ([0163d51](0163d51155))
* landing page ([cc9fb50](cc9fb50b07))
* landing page wording ([4dd6b6d](4dd6b6d7a4))
* last signer as default next ([39934f5](39934f59c3))
* leaky styling and warnings ([6f88f22](6f88f22933)), closes []()
* **lint:** add deps, remove any, update warning limit ([61f39d1](61f39d17ff))
* **lint:** update warning limit ([404f4aa](404f4aa3a1))
* list item key ([c7dfb28](c7dfb2864a))
* loading spinner states, timestamp the file, and lint fixes ([748cb16](748cb16f9f))
* loading spinner, improve desc readability, use favicon instead of circle ([5a4da18](5a4da1834b))
* **loading:** make sure the default spinner is absolute relative to root always ([4bc5882](4bc5882ab6))
* login with hex key does not work, missing proper error when nsec or private key is wrong ([213ae79](213ae79bf5))
* **login:** extension login infinite loading ([7c80643](7c80643aba)), closes []()
* **Login:** fixed loginWithExtension func ([be4e7ab](be4e7ab2bd))
* **login:** redirect to landing instead of login popup page ([84062f2](84062f2ed0))
* **login:** update login method before using nostrController instance ([1f98020](1f980201dd))
* **login:** use const and make sure to clear timeout always ([17c1700](17c1700554))
* logout user if decryption fails due to diff pubkeys ([c96a7fa](c96a7fac4f))
* logout user if signEvent's and auth's pubkeys are diff ([8153ef0](8153ef03fb))
* **LogOut:** used log out action instead of clearState utility ([803e242](803e242b01))
* looping trough robo sets, image not shown when visiting profile while not logged in ([6604ea2](6604ea2046))
* main css background, avoid overscroll showing white edge ([7570123](757012399a))
* manage pending relay connection requests ([f9fcfb1](f9fcfb1c9e))
* **mark:** css position ([413da78](413da78c5c))
* marking ([b22f577](b22f577cc2))
* **marks:** add default signer ([dfdcb84](dfdcb8419d))
* **marks:** add file grouping for marks, fix read pdf types ([b6479db](b6479db266))
* **marks:** assign selectedMarkValue to currentValue and mark.value ([78060fa](78060fa15f))
* **mark:** scroll into marks, add scroll margin and forwardRef ([82b7b9f](82b7b9f7ce)), closes []()
* **MetadataController:** fixed getting popular relays ([026537c](026537c75b))
* missing id/name on custom select input ([d0e3704](d0e3704ed6))
* **mobile:** active tab default state and styling ([6f7d4c9](6f7d4c9dcf))
* **mobile:** use dynamic vh and one-by-one horizontal scroll ([3628137](36281376bc))
* modal override removed ([64b6f83](64b6f8309f))
* move nostr login to nostr route ([3c22429](3c22429941))
* moves sample data to a separate json file ([1de8e89](1de8e89beb))
* moves styling to SVG ([38cd88f](38cd88fd86))
* navigation to profile page from username component ([d502474](d5024745f1))
* nested a links in card ([e4a7fa4](e4a7fa4892))
* next signer and spinner anim duration ([d8adb2c](d8adb2c744))
* no need to listen for authUrl in createNsecBunkerSigner method of NostrController ([3626368](3626368e95))
* node version bump from 18 to 20 ([354312b](354312bd96))
* nostr-login custom outbox relays ([555504f](555504f42f))
* nsec login, metadata overlapping, robohash image in metadata state ([e3e15b7](e3e15b7af1))
* **Offline:** fixed 0.0.0.0 host ([7be9897](7be98978dd))
* **online-detection:** use relative url ([8b4f1a8](8b4f1a8973))
* Opening a sigit asks you to sign when you are not the next signer ([ae3d461](ae3d461661))
* opening link to sign a file while not logged in is not redirecting correctly ([eff8827](eff8827a86))
* optional label for download button in filelist ([3c230e6](3c230e6fb4))
* outdated cache checks ([f0ba9da](f0ba9da8af))
* page scrolling ([97c8271](97c82718cb))
* pdf to png scaling is 1, bottom position is now included ([4556bd0](4556bd0c66))
* **pdf:** add border to style ([ecc1707](ecc1707212))
* **pdf:** add proper default  width value ([a442e71](a442e71087))
* **pdf:** dynamic mark scaling ([ea09daa](ea09daa669))
* **pdf:** font style consistency ([31f3675](31f36750cd))
* pdfjs import ([d5e0769](d5e0769692))
* **pdf:** keep upscaling to match viewport ([43beac4](43beac48e8))
* **pdf:** mark embedding, position, multiline, & placeholder ([f35e271](f35e2718ab)), closes []() []()
* **pdf:** reuse content width function ([59c3fc6](59c3fc69a2))
* **pdf:** scaling and font styles consistency ([ac3186a](ac3186a02e)), closes []()
* **pdf:** scaling on resize, add avatars to counterpart select ([4712031](4712031615))
* **pdf:** use minified version of pdf ([a3effd8](a3effd878b))
* placeholder avatar is incosistent across components ([d15943f](d15943f61b))
* popup forms designs ([e3ca3ab](e3ca3ab908))
* processing events ([25764c7](25764c7ab4))
* processing gift wraps and notifications ([]()) ([235e76b](235e76be4e))
* profile image scale ([58c457b](58c457b62c))
* profile page styling ([67e5c19](67e5c19870))
* profile picture inconsistencies, login with enter ([5f8e8fd](5f8e8fd6f4))
* push all files take 2 ([24916c5](24916c5806))
* reduce mui usage, implement design updates ([9189ff3](9189ff33bc))
* redundant updates ([2d0212f](2d0212fd6c))
* **relay-controller:** sigit relay immutability and relay list ([e0d6c03](e0d6c03639))
* **relays:** allow adding ws:// ([04f1d69](04f1d692a4)), closes []()
* **relays:** relay add button size height ([5f3d92d](5f3d92d62f)), closes []()
* removal of create nostr auth token ([60a7140](60a7140c6a))
* remove both from UserRole enum ([b527339](b5273393e6))
* remove duplicate states and fix default signer ([e05d3e5](e05d3e53a2))
* remove nostr image for placeholder avatar, use robohash instead ([4f4f7fb](4f4f7fb5c1))
* remove placeholder used for text ([d0a6297](d0a6297cce))
* remove screen on nostr-login launch ([8689c7f](8689c7f753))
* remove unstable fetch events loop ([5f0234a](5f0234a358))
* removed redundant variable ([2455856](245585662a))
* removed viewer/signer button ([2f9017b](2f9017b840))
* removes retrier and updates notification ([3d5006a](3d5006a715))
* removes unneeded notification ([b7bd922](b7bd922af3))
* removing file upload, avatar by robohash ([8e76202](8e7620201e))
* replace sign with upload in homepage ([021db56](021db5679a))
* return immediately from publish event when published to at least one relay and keep publishing to other in background ([7df6ab8](7df6ab8c84))
* reverting signing of nostr auth token ([38913e7](38913e770d))
* review suggestion ([15d4d0a](15d4d0a752))
* **review:** remove inline styles ([b8811d7](b8811d730a))
* robohash image missing with NIP05 login ([9baf0ec](9baf0ecaba))
* routing, removed useEffect ([8e71592](8e71592d88))
* search bar scaling ([272fcf9](272fcf93c6))
* search counterparts nip05 does not need to include '@' ([7b29d70](7b29d7055e))
* selected mark selection ([0d52cd7](0d52cd7113))
* show error if decrypt fails ([cc382f0](cc382f0726))
* show extension box for non-mark files, de-dupe css and code ([05a2dba](05a2dba164)), closes []()
* show import/export only for local ([67d545d](67d545de2f))
* sigit links and outline ([21caaa7](21caaa7009))
* sigit's wrapper zip should contain keys.json file ([ded8304](ded8304c66))
* **sigit:** add to submittedBy avatar badge for verified sigit creation ([b2c3cf2](b2c3cf2aca))
* **sigit:** excel extension typo, more excel types ([6b5a8a7](6b5a8a7375))
* sign buttons styles ([8c97476](8c974768a8))
* **sign:** allow signing without marks, hide loading and show toast for prevSig error ([20d1170](20d1170f7d))
* **sign:** allow signing without selectedMark - no currentUserMarks ([92f23ba](92f23bab91))
* **sign:** allow sumit without selectedMark ([cb0d2dd](cb0d2dd7bc))
* **sign:** always show PdfView ([8df5084](8df5084703))
* **signature:** force re-render on value change ([a1c3087](a1c308727f))
* signing order ([ec305c4](ec305c417b))
* simplify events, more ts and clean up ([6641cf2](6641cf2ee7))
* some linter warnings and an error ([f51afe3](f51afe3b67))
* **spinner:** remove dummy desc and use variants ([d1b9eb5](d1b9eb55d8))
* styles fixed in homepage ([6553ed8](6553ed89e0))
* styling ([2f29ea9](2f29ea9f35))
* styling ([d41d577](d41d577c29))
* styling ([e681513](e681513785))
* styling ([551a3f8](551a3f8509))
* styling ([12fe476](12fe476e97))
* svg attributes ([3a93622](3a93622966))
* **tabs:** add tab icons ([2be7f3d](2be7f3d51b))
* take 3 all files ([02f250c](02f250c76e))
* take 4 (all files) ([abf9c3e](abf9c3e4fd))
* take 5 files ([ea3f618](ea3f61897c))
* take 6 ([400d192](400d192fb0))
* take 7 ([3f944bd](3f944bdf73))
* title text align ([c5b1a9b](c5b1a9b380))
* toggle ([3549b6e](3549b6e542))
* top level container wrapper for other pages ([53b7b05](53b7b05ac5))
* typo ([6c5ed3a](6c5ed3a69c))
* unzip and use timeout util ([8b00ef5](8b00ef538b))
* update buttons and button icon design ([28184ab](28184ab038))
* update design buttons ([5d59ffc](5d59ffce28))
* update DM wording ([de00b9b](de00b9b5a7))
* update footer  design ([af689a0](af689a00f7))
* update logo and favicon ([017d1ab](017d1ab88b))
* update nsecBunker delegated key after logout ([962b2bc](962b2bcea6))
* update online and offline flows ([e8da0dc](e8da0dc76f))
* update popup design ([55158fc](55158fc313))
* update the logic for login with nsecbunker ([7c3c061](7c3c061b88))
* update the url in DM to contain fileUrl and encryption key ([9fa3df3](9fa3df3850))
* update user placeholder for create ([e7b0bbe](e7b0bbe23c))
* update verify to use file signature check ([18637bb](18637bbbc1))
* updated latest version of nostr-login which includes outboxRelays option ([6f6ed3c](6f6ed3c39f))
* updates blossom authorisation event ([dd53ded](dd53ded518))
* updating title on homepage ([481ef6c](481ef6cdc2))
* url ([79ef9eb](79ef9eb8d6))
* url encode the DM link payload ([38def3b](38def3bda5))
* use correct key for signer status, update signer badge icons ([3743a30](3743a30ef6))
* use dedicated key from nip78 in auth event for uploading files.zip ([8eaf9cb](8eaf9cb61c))
* use default relayMap if its undefined in redux store ([d7b5ea9](d7b5ea9b9e))
* use hash router instead of browser router ([3d980ca](3d980ca2e7))
* use iframe for nsecbunker auth ([c99a2a8](c99a2a81c2))
* use kind 0 event for nostr joining block ([9bb62cf](9bb62cf966))
* use kind 27235 in place of kind 1 wherever possible ([9073419](90734196e5))
* use old approach of using sha256 for generating d tag ([49c1714](49c1714962))
* use relays from nip65 for broadcasting DMs ([349e26b](349e26b628))
* userRobotImage reducer type fix ([ccc31c5](ccc31c51c9))
* useSigitProfile dep ([329fd3d](329fd3d27b))
* verify page robohash ([5e114f7](5e114f7fb8))
* **verify-page:** add mark styling ([423b6b6](423b6b6792))
* **verify-page:** export (download) files now includes files ([7278485](7278485b76))
* **verify-page:** map item keys ([58f70db](58f70db7f6))
* **verify-page:** parse and show mark values ([f88e2ad](f88e2ad680))
* **verify-page:** remove mark border in production, enable dev flag for css classes ([c3a3915](c3a39157ff))
* verify/sign link ([e48a396](e48a396990))
* **verify:** offline flow ([759a40a](759a40a4f9))
* when decrypting file, have better error messages ([5d6a358](5d6a3580a6))
* when opening a sigit after user signed it, asks user to sign again instead of redirecting to /verify ([ccb4036](ccb4036029))
* wording of adding counterparties ([33d58a2](33d58a2166))
* works offline card icon ([baa1a7b](baa1a7b040))

### Code Refactoring

* use signature pad, smoother signature line ([7c7a222](7c7a222d4f))

### Features

* ability to change the order of signers in create screen ([8deaae8](8deaae80de))
* add background images ([e9a1b98](e9a1b9894c))
* add banner and styling ([5f39b55](5f39b55f68))
* add cache setting page ([278d965](278d9655f6))
* add children support to routes arrays ([0b35f11](0b35f11abf))
* add color border to user's profile picture based on first 6 character of user's hexkey ([89850f8](89850f881d))
* add custom Container component for layouts ([e54eced](e54eced800))
* add dropzone and multiple files support ([83ddc1b](83ddc1bbc8))
* add exportedBy to useSigitMeta ([13254fb](13254fbe06))
* add MarkConfig and components ([dfa2832](dfa2832e8d))
* add minimal styling secondary button ([9a1d3d9](9a1d3d98bf))
* add modal with login, register, nostr routes ([868ae6f](868ae6f23e))
* add nostrLoginAuthMethod to state ([110621a](110621a125))
* add prev signer's signature in the content of next signer's signed event ([7947abf](7947abf0f9))
* Add Sigit ID as a path param ([75a715d](75a715d002))
* Add Sigit ID as a Path Param to /verify ([0008e98](0008e98146))
* add simple spinner wrapper ([01ca81b](01ca81be2a))
* add squiggle support ([de44370](de44370a96))
* add sticky layout with slots ([dfe67b9](dfe67b99ad))
* add sticky layout with slots ([e16b8cf](e16b8cfe3f))
* add SVGO, enable signature ([9286e43](9286e4304f))
* add the ability to create and sign while user is offline ([c3c9bf7](c3c9bf772d))
* add uploaded image file as preview ([ae08b07](ae08b07d74))
* add UserAvatar, UserIconButton ([20bb05d](20bb05ddc6)), closes []()
* add verify link in landing page ([8884389](8884389c6a))
* add verify page ([5c14402](5c1440244c))
* added a local cache based on browsers built in indexDB ([5b1147d](5b1147da5d))
* added a setting page ([e82023f](e82023f105))
* added hashes.json in zip ([d879c7d](d879c7d45a))
* added ndkContext and used it in relays page ([3c061d5](3c061d5920))
* added nsecbunker setting page ([b2a8cff](b2a8cff907))
* added profile banner ([6eedfb8](6eedfb8f3f))
* added profile view ([5d0076d](5d0076dd62))
* added the ability to login with nsecbunker connection string ([4973721](4973721608))
* added the ability to re-broadcast sigit ([5db4d1b](5db4d1b429))
* allow the user to login via nsecbunker using only domain part ([3efa557](3efa557976))
* **auth:** nsec login with url params ([995c7ce](995c7ce293))
* changed MIME type of the uploaded file to sigit ([4e7f9d6](4e7f9d650e))
* **ci:** add git hooks ([70f625f](70f625ffd1))
* **ci:** add lint-staged in pre-commit ([84d1379](84d13793ff))
* **ci:** add open pr workflow ([5290dda](5290dda52a))
* configured semantic releases ([c0b9039](c0b903929d))
* **content:** show other file types as gray box ([c9d7d0a](c9d7d0a6f5))
* convert hexkeys to npub in meta.json ([ee2f0cb](ee2f0cbc97))
* create page search users ([4af28ab](4af28abcb6))
* create signing request and send a DM to first signer with zip file url and encryption key ([bd1e841](bd1e8417c1))
* **create-page:** intial layout and page styling ([86c8cc0](86c8cc00fd))
* **create:** add counterpart component for drawing field ([4131eb5](4131eb5de1))
* **create:** add Image and File items ([889d6a0](889d6a0f44))
* **create:** touch support for dnd ([3e07575](3e075754e5))
* custom select component ([8d16831](8d168314de))
* **dashboard:** add sigits filtering, sorting, searching ([becd021](becd02153c))
* **export:** add icons and make encrypted be first/top option ([99d562a](99d562a3ed))
* extension icon label util component ([c3f60b1](c3f60b1e64))
* handle root _@ users on add counterpart ([897daaa](897daaa1fa))
* **home:** add search param to address bar and sync the state with navigation ([93b2477](93b2477839))
* implemented profile page ([c0547b2](c0547b2a1f))
* implemented relay controller and use that for fetching and publishing events ([a775d7b](a775d7b265))
* implemented the UI and logic for signing document ([a32abaf](a32abaf9e7))
* improve design for homepage ([de4d927](de4d927c73))
* improve verification process ([6611a85](6611a855d9))
* in offline mode navigate creator to sign screen after creation when creator is first signer ([1f7980e](1f7980e2ca))
* In sign page navigate to verify after export ([8f463b3](8f463b36c0))
* include the original files always ([db9cf9d](db9cf9d20c))
* landing page - larger cta button ([3149ba9](3149ba9757))
* landing page - responsive cards ([87e4536](87e4536713))
* landing page implementation and styling ([0a61ae5](0a61ae5f64))
* **loading-spinner:** add children support for default variant ([4d1e672](4d1e672268))
* logo and favicon ([a36ed8e](a36ed8eab0))
* maintain logged in sesssion ([2ed092b](2ed092bcbd))
* make block number link that will refernce to the event ([37bc205](37bc205ce4))
* make verify page public and add verify option in user menu list ([12ca854](12ca854c48))
* **meta:** add error handling for meta.json blossom operations ([7007492](7007492a0d))
* **meta:** send notifications with blossom instead of meta.json ([3d1bdec](3d1bdece4d))
* **mobile:** tabs and scrolling ([d9be051](d9be05165f))
* navigate to different pages based on uploaded file ([92b62a3](92b62a3cbe))
* nostr.json ([bb37a27](bb37a27321))
* **offline:** add decrypt as zip util ([8b5abe0](8b5abe02e2))
* **offline:** add signer service util class ([bcd5713](bcd57138ca))
* **offline:** split online and offline flow with dedicated buttons, remove export in sign, all counterparties can decrypt ([3f01ab8](3f01ab8fca))
* **opentimestamps:** adds OTS library and retrier function ([edfe9a2](edfe9a2954))
* **opentimestamps:** adds timestamps to create flow ([85bcfac](85bcfac2e0))
* **opentimestamps:** amends to flow to only upgrade users timestamps ([f12aaf1](f12aaf1c2b))
* **opentimestamps:** refactors to timestamp the nostr event id ([07f1a15](07f1a15aa1))
* **opentimestamps:** update the full flow ([21aa25a](21aa25a42a))
* **opentimestamps:** updates data model ([85bf907](85bf907f54))
* **opentimestamps:** updates data model and useSigitMeta hook ([edbe708](edbe708b65))
* **opentimestamps:** updates opentimestamps type ([b92790c](b92790ceed))
* **opentimestamps:** updates signing flow ([7f00f9e](7f00f9e8bf))
* **opentimestamps:** updates the flow and adds notifications ([2b630c9](2b630c94b6))
* **opentimestamps:** updates tooltip ([19b815e](19b815e528))
* **opentimestamps:** updates utils and adds comments ([a2138f1](a2138f1de1))
* **PDF Management:** added pdf pages preview with fields list ([e715f6a](e715f6ae6f))
* **pdf markings:** added drawing component, parsing pdfs and displaying in the UI ([8576034](8576034829))
* **pdf-fields:** add logic to hide signers on ESC ([e37f90d](e37f90d6db))
* **pdf-marking:** add pdf-view components ([b58ba62](b58ba625f9))
* **pdf-marking:** adds file downloading functionality ([6d881cc](6d881ccb45))
* **pdf-marking:** adds file validity check ([eca31ce](eca31cea4f))
* **pdf-marking:** adds file validity check ([ed7acd6](ed7acd6cb4))
* **pdf-marking:** binds text to marks and saves with signatures ([4a932ff](4a932ffe03))
* **pdf-marking:** implements png to pdf conversion and ability to download full sigits after signing ([cb9a443](cb9a443fb1))
* **pdf-marking:** integrates layouts ([64dbd7d](64dbd7d479))
* **pdf-marking:** integrates UserDetails ([2becab9](2becab9f79))
* **pdf-marking:** updates design and functionality of the pdf marking form ([ed0158e](ed0158e817))
* **pdf-marking:** updates mark type and adds pdf-view components ([296b135](296b135c06))
* **profile:** picture upload, robohash, website, npub cash ([041bd0d](041bd0daff))
* **Relay:** added methods to get info, most popular, connect and disconnect from relays ([ffb2379](ffb237991c))
* **Relays:** added logic to manage relays ([64f8227](64f822743f))
* **Relays:** improved relays page ([c37e8f3](c37e8f36c2))
* search users by nip05, npub and filter: serach, improved UX ([6c7cac2](6c7cac2336))
* show block number on user profile ([1eed099](1eed099059))
* Sign Directly From the Marking Screen fix: Marking inputs glitches, losing values ([0a0a9be](0a0a9bef34))
* **signature:** export signature files ([cdf26b6](cdf26b6614))
* **signature:** signature pad encrypt, upload, fetch, decrypt, render, add to pdf ([9551750](9551750cbe))
* **signature:** verify hash ([a371e98](a371e98e9e))
* **signers-dropdown:** improved hiding/displaying logic ([76b1fa7](76b1fa792c))
* **Store:** configured relays state ([106827b](106827b6da))
* update findMetadata method of metadata controller ([2b96172](2b9617232e))
* update signing flow ([1f9954b](1f9954befd))
* use nip04 for encryption and decryption of userData to store on blossom server ([18270c5](18270c5d8a))
* **verify-page:** add files view and content images ([2c586f3](2c586f3c13))

### Reverts

* "feat(pdf-marking): adds file validity check" ([268a4db](268a4db3ff))

### BREAKING CHANGES

* mark.value type changed
2025-01-31 14:48:48 +00:00
b
89dc4c01aa Merge pull request 'chore: Release' () from staging into main
Reviewed-on: 
2025-01-31 13:42:30 +00:00
s
ae7e09c4ca Merge branch 'main' into staging 2025-01-31 11:20:41 +00:00
s
e80c4024f8 Merge pull request 'chore(workflow): fixed release step in gitea workflow' () from fix-release-workflow into staging
Reviewed-on: 
2025-01-31 11:17:33 +00:00
daniyal
37fe28c070 chore(workflow): fixed release step in gitea workflow 2025-01-31 16:15:29 +05:00
b
05051e49fa Merge pull request 'fix: node version bump from 18 to 20' () from staging into main
Reviewed-on: 
2025-01-31 10:36:16 +00:00
b
8582f70652 Merge branch 'main' into staging 2025-01-31 10:31:24 +00:00
b
354312bd96 fix: node version bump from 18 to 20 2025-01-31 10:29:53 +00:00
b
d72250b6dc Merge pull request 'Release' () from staging into main
Reviewed-on: 
2025-01-31 10:25:14 +00:00
b
b18e891341 Merge branch 'main' into staging 2025-01-31 10:23:17 +00:00
b
461d43e2e1 Merge pull request 'feat: added the ability to re-broadcast sigit' () from issue-240 into staging
Reviewed-on: 
2025-01-31 10:20:39 +00:00
s
35e7ac4086 Merge branch 'staging' into issue-240 2025-01-31 10:13:41 +00:00
daniyal
5db4d1b429 feat: added the ability to re-broadcast sigit 2025-01-31 15:02:14 +05:00
b
af036b1bb7 Merge pull request 'feat: configured semantic releases' () from semantic-releases into staging
Reviewed-on: 
2025-01-29 13:49:15 +00:00
c0b903929d feat: configured semantic releases 2025-01-29 16:46:04 +03:00
en
efe3c2c9c7 refactor(dm): update private dm to use ndk 2025-01-24 14:53:17 +01:00
en
4b5955fa9c chore(ndk): bump ndk version 2025-01-24 14:52:34 +01:00
en
664ed9de06 refactor(cache): remove unused cache service 2025-01-24 14:52:09 +01:00
en
1e643c60e5 chore(git): merge branch 'staging' into 92-send-completion-dm 2025-01-24 12:00:17 +01:00
b
15000a2d14 Merge pull request 'release' () from staging into main
Reviewed-on: 
2025-01-23 18:54:21 +00:00
b
b97afdecfd Merge branch 'main' into staging 2025-01-23 18:50:53 +00:00
b
feea3197d0 Merge pull request 'Offline flow separation' () from 231-offline-flow into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2025-01-22 13:05:30 +00:00
b
c45e3912a2 Merge branch 'staging' into 231-offline-flow 2025-01-22 12:10:05 +00:00
f72fa1a886 chore(git): merge pull request from 297-settings-relays-allow-ws into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2025-01-22 09:36:08 +00:00
enes
a4310675c1 refactor(offline): update strings, create offline navigate to sign/verify w/o auto download 2025-01-20 21:24:18 +01:00
enes
5f3d92d62f fix(relays): relay add button size height
Closes 
2025-01-20 20:14:44 +01:00
enes
04f1d692a4 fix(relays): allow adding ws://
Closes 
2025-01-20 20:06:18 +01:00
enes
99d562a3ed feat(export): add icons and make encrypted be first/top option 2025-01-20 19:41:14 +01:00
enes
e60c4cbc31 chore(git): merge branch 'staging' into 231-offline-flow 2025-01-17 21:13:05 +01:00
enes
3f01ab8fca feat(offline): split online and offline flow with dedicated buttons, remove export in sign, all counterparties can decrypt 2025-01-17 21:12:31 +01:00
enes
b7410c7d33 refactor(offline): make both export types as optional 2025-01-17 21:09:38 +01:00
enes
b6a84dedbe refactor(offline): remove unused function 2025-01-17 21:07:47 +01:00
enes
8b5abe02e2 feat(offline): add decrypt as zip util 2025-01-17 21:06:55 +01:00
enes
7b2537e355 refactor(offline): useSigitMeta and remove async ops when parsing json 2025-01-17 21:05:59 +01:00
enes
bcd57138ca feat(offline): add signer service util class 2025-01-17 21:02:18 +01:00
2d472470be Merge pull request ' Change Naming from PdfFileHash to FileHash' () from issue-215-file-hash-naming into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2025-01-14 08:35:57 +00:00
c69d55c3a8 chore: adds comment 2025-01-14 10:32:28 +02:00
3be0fd7bbb chore: adds comment 2025-01-14 10:30:03 +02:00
enes
5079b68bdf chore(git): merge branch 'staging' into 231-offline-flow 2025-01-10 12:21:00 +01:00
70e7e5305e refactor: renames to fileHash 2025-01-09 12:58:23 +02:00
fdaae33aa1 Merge pull request ' Fixes Sign and Complete Spinner Issue' () from issue-288-sign-complete-spinner-fix into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2025-01-08 12:25:17 +00:00
746338465d fix: disables redundant metaInNavState updates 2025-01-08 12:29:59 +02:00
b
736dafce94 chore: testing guidance in contributing.md 2025-01-07 20:20:22 +00:00
b
b361ab3d99 Merge pull request 'staging release' () from staging into main
Reviewed-on: 
2025-01-07 10:10:29 +00:00
s
60b3a28435 Merge pull request 'chore: use-ndk' () from use-ndk into staging
Reviewed-on: 
2025-01-06 11:10:49 +00:00
daniyal
48f85f54c8 chore: quick fixes 2025-01-06 16:04:54 +05:00
enes
dbcc96aca2 refactor: split online and offline create 2025-01-04 20:36:14 +01:00
enes
9a1d3d98bf feat: add minimal styling secondary button 2025-01-04 19:28:30 +01:00
daniyal
14c103dd40 chore: merge staging into use-ndk 2025-01-04 12:21:47 +05:00
daniyal
0d93e16f3a chore: enable debug logs only in dev mode 2025-01-04 11:33:44 +05:00
daniyal
3a09d4c595 chore: process received events all together instead of one by one which casuses in-consistencies due to async nature of redux updates 2025-01-04 11:24:35 +05:00
daniyal
c4c0ecba4a chore: handle restore state action in user reducer 2025-01-04 11:21:49 +05:00
b
3fefcc5a98 Merge pull request 'Log out user if extension's pubkey and auth's pubkey are different' () from 290-user-ext-log-missmatch into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2025-01-02 09:44:28 +00:00
b
8af90d93cb Merge branch 'staging' into 290-user-ext-log-missmatch 2025-01-02 09:43:57 +00:00
b
222ad06644 Merge pull request 'allow signing and show PdfView without any marks (never created)' () from 286-missing-pdf-and-sign into staging
Reviewed-on: 
Reviewed-by: Stixx <m@noreply.git.nostrdev.com>
2025-01-02 09:43:46 +00:00
enes
c96a7fac4f fix: logout user if decryption fails due to diff pubkeys 2024-12-31 13:02:39 +01:00
enes
8153ef03fb fix: logout user if signEvent's and auth's pubkeys are diff 2024-12-31 13:01:42 +01:00
enes
84062f2ed0 fix(login): redirect to landing instead of login popup page 2024-12-31 12:59:41 +01:00
enes
ec43324cae refactor(nostr): capturePublicKey from signedEvent instead of nostr api call 2024-12-31 12:10:56 +01:00
enes
1f980201dd fix(login): update login method before using nostrController instance 2024-12-31 12:09:12 +01:00
daniyal
95f5398736 chore: replace the usage of ProfileMetadata with NDKUserProfile 2024-12-28 14:57:19 +05:00
daniyal
01bb68d87b chore: fix issue in publishing sigit 2024-12-28 00:44:21 +05:00
daniyal
006ed7b548 chore: fix fetching of user profile 2024-12-28 00:43:00 +05:00
daniyal
4f9fdd19b0 chore: remove import of deleted file 2024-12-27 17:21:02 +05:00
enes
66ad9b5edc chore(git): merge branch 'staging' into 286-missing-pdf-and-sign 2024-12-27 12:44:21 +01:00
daniyal
4e54d12175 chore: merge stating into use-ndk 2024-12-27 15:33:43 +05:00
b
1909534079 Merge pull request 'fix: Opening a sigit asks you to sign when you are not the next signer' () from issue-281 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-24 10:02:35 +00:00
a2b7423c8b chore(git): merge 2024-12-24 10:58:58 +01:00
ee3e0e1bb1 fix: If creator is not the first signer we should not redirect to /sign page 2024-12-24 10:58:29 +01:00
b
8000f4a349 Merge branch 'staging' into issue-281 2024-12-23 17:32:59 +00:00
enes
cb0d2dd7bc fix(sign): allow sumit without selectedMark 2024-12-23 17:45:13 +01:00
enes
92f23bab91 fix(sign): allow signing without selectedMark - no currentUserMarks 2024-12-23 17:44:33 +01:00
enes
8df5084703 fix(sign): always show PdfView 2024-12-23 16:39:24 +01:00
ae3d461661 fix: Opening a sigit asks you to sign when you are not the next signer 2024-12-23 13:57:25 +01:00
enes
ca7b87a967 refactor(create): reduce minimum field threshold size 2024-12-23 13:47:14 +01:00
b
a5e31b0bdf Merge pull request 'Refactor create page interactions and fix the "excel" bug' () from 282-create-page-interactions into staging
Reviewed-on: 
Reviewed-by: Stixx <m@noreply.git.nostrdev.com>
2024-12-23 10:47:27 +00:00
enes
1827a20755 refactor(create): update location state to keep latest selected files 2024-12-23 11:10:23 +01:00
daniyal
e8a53bc73e chore: removed relay controller in favor of NDKContext 2024-12-22 23:38:27 +05:00
daniyal
0ea6ba0033 chore: remove dvm utils file and use useDvm hook instead 2024-12-22 21:53:00 +05:00
daniyal
3615de70ad chore: include sigit relay explicitly when fetching event from user relays 2024-12-22 21:49:56 +05:00
enes
abea8dcd15 refactor(create): optimize the events and fix responsiveness of the drawn fields with proper re-renders 2024-12-21 17:15:59 +01:00
enes
4cde0796a2 refactor: bump z-index on loading spinner 2024-12-21 17:03:03 +01:00
enes
4131eb5de1 feat(create): add counterpart component for drawing field 2024-12-21 17:02:13 +01:00
enes
671bb0561a refactor(create): use immer for sigit files creation 2024-12-21 17:00:51 +01:00
daniyal
5c24c5bde0 chore(refactor): use getNDKRelayList function from NDKContext instead of findRelayListMetadata function of metadata controller 2024-12-20 21:58:45 +05:00
enes
889d6a0f44 feat(create): add Image and File items 2024-12-20 16:09:48 +01:00
enes
c391b01b90 build(deps): add immer package 2024-12-20 16:09:06 +01:00
enes
b7eadf0081 refactor(sign): cleaner divider code in PDVview 2024-12-20 16:07:12 +01:00
daniyal
458de18f12 chore(refactore): use NDKContext for findMetadata instead of metadata controller 2024-12-20 15:52:45 +05:00
daniyal
0cc1a32059 chore(refactor): replace authContoller with useAuth hook 2024-12-20 15:25:02 +05:00
b
d965e56f76 Merge pull request 'fix: build failing due to type issue' () from build-fix into staging
Reviewed-on: 
2024-12-19 09:18:17 +00:00
652ea06c0d fix: build failing due to type issue 2024-12-19 10:16:18 +01:00
b
6aeb98b233 Merge pull request 'fix: when opening a sigit after user signed it, asks user to sign again instead of redirecting to /verify' () from issue-277 into staging
Reviewed-on: 
2024-12-18 16:58:42 +00:00
ccb4036029 fix: when opening a sigit after user signed it, asks user to sign again instead of redirecting to /verify 2024-12-18 17:23:05 +01:00
b
9882393b7a Merge pull request 'Send notifications with blossom url to meta.json' () from 260-meta-blossom into staging
Reviewed-on: 
Reviewed-by: Stixx <m@noreply.git.nostrdev.com>
2024-12-18 11:46:57 +00:00
enes
829eb5fd83 chore(git): merge branch 'staging' into 260-meta-blossom 2024-12-18 12:40:01 +01:00
b
3a74bddb30 Merge pull request 'fix: include purplepage and userkindpages relays when searching for user in create page' () from issue-261 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-18 09:14:21 +00:00
eac9053208 chore(git): Merge branch 'issue-261' of ssh://stixx.git.nostrdev.com:29418/sigit/sigit.io into issue-261 2024-12-17 21:25:09 +01:00
40b081f059 Merge branch 'staging' into issue-261 2024-12-17 20:24:49 +00:00
6f6ed3c39f fix: updated latest version of nostr-login which includes outboxRelays option 2024-12-17 21:17:49 +01:00
enes
c5724858d6 refactor(sign): increase the size of final sign button 2024-12-13 13:44:17 +01:00
enes
2a23912c08 refactor(sign): autoFocus sign button, use mui/button for focus ripple effect 2024-12-13 13:36:44 +01:00
enes
e1e5ae7f1a build(vulnerabilities): bump dependencies with audit fix 2024-12-13 12:59:50 +01:00
enes
0be63265b5 chore(git): merge branch 'staging' into 260-meta-blossom 2024-12-13 12:53:37 +01:00
enes
7007492a0d feat(meta): add error handling for meta.json blossom operations 2024-12-13 12:39:00 +01:00
b
5ed3d2f389 Merge pull request 'New release' () from staging into main
Reviewed-on: 
2024-12-11 16:49:23 +00:00
b
c3e4f6055c Merge pull request 'Searching counterparts glitchy when includes @ (nip05)' () from issue-270 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-10 15:37:00 +00:00
b
1c0984cb3b Merge branch 'staging' into issue-270 2024-12-10 15:36:44 +00:00
b
80dd597ada Merge pull request 'feat: Sign Directly From the Marking Screen fix: Marking inputs glitches, losing values' () from issue-173 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-10 15:34:01 +00:00
99fa3add56 fix: bug, when valid npub, clicking + was saying npub was invalid 2024-12-10 15:29:24 +01:00
245585662a fix: removed redundant variable 2024-12-10 14:46:26 +01:00
6716c3da63 fix: including signatures in both export and encrypted export 2024-12-10 14:42:12 +01:00
8d8c38e90b fix: addressing comments 2024-12-09 22:37:27 +01:00
8e1acc0bd6 chore(git): Merge 2024-12-09 09:44:35 +01:00
555504f42f fix: nostr-login custom outbox relays 2024-12-09 09:44:22 +01:00
8bb556a446 Merge branch 'staging' into issue-261 2024-12-09 08:43:51 +00:00
afbe05b4c8 fix: footer 'Home' button scroll to top when on home page, fixed logic 2024-12-06 21:00:52 +01:00
7b29d7055e fix: search counterparts nip05 does not need to include '@' 2024-12-06 20:58:20 +01:00
enes
3d1bdece4d feat(meta): send notifications with blossom instead of meta.json 2024-12-06 20:00:38 +01:00
daniyal
2248128001 chore(refactor): use NDKContext in profile page 2024-12-06 21:12:40 +05:00
0fd0f26fc7 fix: counterpart search NIP05 glitching 2024-12-06 16:22:23 +01:00
69efd9e09d fix: clicking logo not redirecting to home 2024-12-06 15:49:57 +01:00
fc8f73962b chore(git): merge 2024-12-06 14:42:11 +01:00
d0231b0652 chore: typo 2024-12-06 14:41:44 +01:00
241d0cc79c Merge branch 'staging' into issue-173 2024-12-06 12:27:09 +00:00
0a0a9bef34 feat: Sign Directly From the Marking Screen fix: Marking inputs glitches, losing values 2024-12-06 13:24:10 +01:00
b
092bb98670 Merge pull request 'fix: better UX when clicking on logo when on home screen or home button in footer' () from issue-255 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-06 10:52:44 +00:00
b
f4cf497c1a Merge branch 'staging' into issue-255 2024-12-06 10:51:19 +00:00
b
e05dcf4a07 Merge pull request 'fix: inform user then search term provided no results' () from issue-248 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-06 10:51:08 +00:00
b
fd13fd5c7e Merge branch 'staging' into issue-248 2024-12-06 10:48:21 +00:00
b
f59e84d905 Merge pull request 'fix: display no results when no submissions are found' () from issue-254 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-06 10:48:11 +00:00
b
5e80935c0a Merge branch 'staging' into issue-254 2024-12-06 10:28:50 +00:00
b
047f961c8d Merge pull request 'fix: create page, improving message "preparing document for signing"' () from issue-253 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-06 10:28:39 +00:00
daniyal
3c061d5920 feat: added ndkContext and used it in relays page 2024-12-06 14:16:46 +05:00
98fbe80648 fix: create page, improving message "preparing document for signing" 2024-12-03 11:25:31 +01:00
bbe34b6011 fix: display no results when no submissions are found 2024-12-03 10:48:40 +01:00
24463a53c5 fix: inform user then search term provided no results 2024-12-02 15:34:17 +01:00
834d70d774 fix: better UX when clicking on logo when on home screen or home button in footer 2024-12-02 15:01:29 +01:00
f44e1bca06 Merge branch 'staging' into issue-261 2024-12-02 13:32:21 +00:00
8a9910db87 fix: include purplepage and userkindpages relays when searching for user in create page 2024-12-02 11:51:02 +01:00
8ece8283e1 Merge pull request 'fix: clicking on marked fileds is losing input text/squiggle, squiggle field is mobile friendly' () from issue-256 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-12-02 09:56:35 +00:00
5767abc902 Merge branch 'staging' into issue-256 2024-12-02 09:56:02 +00:00
55e2e6e35a chore(git): merge pull request from issue-234-empty-box-creation into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2024-11-29 14:41:16 +00:00
enes
6193c20ac3 refactor(create): update comment 2024-11-29 12:45:37 +01:00
enes
902ad73faf fix(create): remove small drawn fields
Closes 
2024-11-29 12:17:23 +01:00
enes
413da78c5c fix(mark): css position 2024-11-29 11:15:10 +01:00
602e23c719 fix: clicking on marked fileds is losing input text/squiggle, squiggle field is mobile friendly 2024-11-27 17:00:59 +01:00
52f8b92c5d Merge pull request 'Add Sigit ID as a Path Param to /verify' () from issue-232 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-11-26 11:24:04 +00:00
23fe48b615 chore(git): merge, verify route :id 2024-11-26 12:02:53 +01:00
57efef9806 chore(git): merge 2024-11-26 12:02:25 +01:00
760ac176e8 chore(git): merge pull request from feat/signature-pad into staging
Reviewed-on: 
Reviewed-by: Stixx <m@noreply.git.nostrdev.com>
2024-11-26 09:35:44 +00:00
enes
a371e98e9e feat(signature): verify hash 2024-11-22 16:31:38 +01:00
enes
3255e93121 chore(git): merge branch 'staging' into feat/signature-pad 2024-11-22 13:27:05 +01:00
82376838bd Merge pull request 'Create page: search users' () from issue-56 into staging
Reviewed-on: 
2024-11-21 10:18:32 +00:00
2f9017b840 fix: removed viewer/signer button 2024-11-21 11:06:31 +01:00
6c7cac2336 feat: search users by nip05, npub and filter: serach, improved UX 2024-11-21 09:20:20 +01:00
4af28abcb6 feat: create page search users 2024-11-19 12:03:41 +01:00
enes
206169dbaa build(vite): fix hmr circular reference warning 2024-11-18 17:29:46 +01:00
enes
be146fa0fa refactor(signature): apply strategy pattern and make it easier to expand with new tools 2024-11-18 17:20:20 +01:00
enes
f72ad37ec0 refactor(signature): save only decrypted signature files on export 2024-11-18 13:49:39 +01:00
enes
a1c308727f fix(signature): force re-render on value change 2024-11-18 13:39:54 +01:00
enes
cdf26b6614 feat(signature): export signature files 2024-11-15 18:04:40 +01:00
enes
9551750cbe feat(signature): signature pad encrypt, upload, fetch, decrypt, render, add to pdf 2024-11-15 17:51:11 +01:00
enes
3f081c1632 refactor(signature): use reduced point group data 2024-11-11 16:21:22 +01:00
enes
7fa9a008fa refactor(signature): stretch svg render 2024-11-08 17:34:22 +01:00
enes
6fd5014302 refactor(signature): fixed pad size, scale to fit mark 2024-11-08 17:33:55 +01:00
enes
7c7a222d4f refactor: use signature pad, smoother signature line
BREAKING CHANGE: mark.value type changed
2024-11-08 16:58:27 +01:00
c89e96b824 chore: added comment 2024-11-04 22:04:27 +01:00
22debeb033 chore(git): Merge branch 'staging' into issue-232 2024-11-04 14:28:27 +01:00
0008e98146 feat: Add Sigit ID as a Path Param to /verify 2024-11-04 13:35:06 +01:00
4cb6f07a68 Merge pull request 'issue-236-fixed' () from issue-236-fixed into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-11-04 07:43:58 +00:00
NostrDev
5b1654c341 chore: added handleEscapeButtonDown description 2024-11-04 10:42:55 +03:00
.
02f651acc7 chore: revert (wrong site) 2024-11-02 11:40:20 +00:00
.
cd0e4523e1 chore: goat@nostrdev.com 2024-11-02 11:39:36 +00:00
NostrDev
76b1fa792c feat(signers-dropdown): improved hiding/displaying logic 2024-11-01 17:00:46 +03:00
NostrDev
3a94fbc0ae chore(types): used KeyboardCode enum 2024-11-01 11:23:05 +03:00
NostrDev
e37f90d6db feat(pdf-fields): add logic to hide signers on ESC 2024-11-01 11:22:31 +03:00
b
cc059f6cb4 Merge pull request 'feat: signature squiggle' () from feat/signature into staging
Reviewed-on: 
Reviewed-by: b <b@4j.cx>
2024-10-28 16:23:28 +00:00
enes
de44370a96 feat: add squiggle support 2024-10-25 18:42:16 +02:00
enes
dfa2832e8d feat: add MarkConfig and components 2024-10-25 18:40:50 +02:00
enes
9286e4304f feat: add SVGO, enable signature 2024-10-25 18:38:47 +02:00
aae11589a4 Merge pull request 'issue-166-open-timestamps' () from issue-166-open-timestamps into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-10-25 11:18:46 +00:00
69f67fc812 chore: disables rules for specific parts of code 2024-10-25 14:15:12 +03:00
38cd88fd86 fix: moves styling to SVG 2024-10-25 13:13:22 +03:00
dbcd54cec0 chore: merge branch 'main' into issue-166-open-timestamps 2024-10-24 15:58:39 +03:00
2d0212fd6c fix: redundant updates 2024-10-24 12:54:47 +03:00
19b815e528 feat(opentimestamps): updates tooltip 2024-10-24 12:42:21 +03:00
b
33e7fc7771 Merge pull request 'Release' () from staging into main
Reviewed-on: 
2024-10-18 15:09:39 +00:00
97d9857bef Merge branch 'main' into staging 2024-10-18 14:59:01 +00:00
enes
4465b8c3ac refactor(landing): cards description 2024-10-18 16:54:31 +02:00
54047740f9 chore: updates packages 2024-10-18 11:26:25 +03:00
7f411f09a7 chore: merge branch 'main' into issue-166-open-timestamps 2024-10-18 11:24:31 +03:00
849e47da00 chore: updates packages 2024-10-18 11:03:51 +03:00
b04f4fb88d refactor: show sent dm count, don't sent twice to creator 2024-10-14 13:19:44 +02:00
3b4bf9aa29 fix: only send to next signer on create 2024-10-14 11:58:52 +02:00
b
bb323be87c Merge pull request 'Release' () from staging into main
Reviewed-on: 
2024-10-14 09:02:41 +00:00
b
fd2f179273 Merge branch 'main' into staging 2024-10-14 09:02:14 +00:00
b
4559f16d86 Merge pull request 'fix: add files and marked to sign page exports' () from fixes-10-11 into staging
Reviewed-on: 
2024-10-14 09:01:42 +00:00
e85e9519d2 feat: add private dm sending 2024-10-14 10:21:46 +02:00
d6f92accb0 chore(git): merge branch 'origin/fixes-10-11' into fixes-10-11 2024-10-14 09:56:22 +02:00
b
ee03cc545e Merge branch 'staging' into fixes-10-11 2024-10-13 12:47:38 +00:00
b
70e525357c Merge pull request 'feat: handle root _@ users on add counterpart' () from adding-domain-user-10-10 into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-10-13 12:46:55 +00:00
b
3eed2964a0 Merge branch 'staging' into fixes-10-11 2024-10-12 11:19:14 +00:00
b
3a0f155010 Merge pull request 'fix: processing events, stale sigits' () from hotfix-processing-events-10-12 into staging
Reviewed-on: 
2024-10-12 11:18:46 +00:00
1d1986f082 fix: clear hasSubscribed after the logout 2024-10-12 12:05:55 +02:00
25764c7ab4 fix: processing events
Partially revert to before 23a04faad89ae3138008f4b1b9a112bf944f279b
2024-10-12 11:52:43 +02:00
cc382f0726 fix: show error if decrypt fails 2024-10-11 16:43:55 +02:00
9dd190d65b fix: add files and marked to sign page exports
Skip marked if the file contains  no marks
2024-10-11 16:16:59 +02:00
c3dacbe111 fix: add mark label 2024-10-11 15:05:28 +02:00
897daaa1fa feat: handle root _@ users on add counterpart 2024-10-10 13:56:08 +02:00
b
ed90168e5d Merge pull request 'staging' () from staging into main
Reviewed-on: 
2024-10-09 13:50:23 +00:00
b
7f5fd4534f Merge branch 'main' into staging 2024-10-09 13:48:05 +00:00
b
7f172178a1 Merge pull request 'feat: include marked and original files in zip' () from 203-export-original-and-modified into staging
Reviewed-on: 
Reviewed-by: b <b@4j.cx>
2024-10-09 13:43:02 +00:00
1116867224 refactor: rename functions and labels 2024-10-09 11:52:32 +02:00
db9cf9d20c feat: include the original files always 2024-10-09 11:51:26 +02:00
58c457b62c fix: profile image scale 2024-10-09 10:58:31 +02:00
b6846c0006 chore(git): merge pull request from nostr-login-9-30 into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-10-09 08:54:33 +00:00
8deb5bd7cd Merge branch 'staging' into nostr-login-9-30 2024-10-09 08:35:13 +00:00
7b7f23a779 chore(git): merge pull request from beta-release into staging
Reviewed-on: 
2024-10-09 08:33:52 +00:00
db4a202363 refactor: update banner and package for beta release 2024-10-08 20:31:03 +02:00
3a507246ca refactor: add comments 2024-10-08 20:25:34 +02:00
f09d9b2378 refactor(ts): remove type assertion 2024-10-08 20:14:44 +02:00
fe9f282984 Merge branch 'staging' into nostr-login-9-30 2024-10-08 18:06:40 +00:00
aa4637dd0d refactor(login): add comments 2024-10-08 19:12:21 +02:00
23a04faad8 refactor(auth): main effect order and deps 2024-10-08 18:50:33 +02:00
ad2ec070be refactor(reducers): match state types and reducers 2024-10-08 18:42:34 +02:00
d610c79cad refactor: use useAppDispatch, useAppSelector hooks 2024-10-08 17:08:43 +02:00
b7bd922af3 fix: removes unneeded notification 2024-10-08 17:04:07 +02:00
f12aaf1c2b feat(opentimestamps): amends to flow to only upgrade users timestamps 2024-10-08 17:01:51 +02:00
6c5ed3a69c fix: typo 2024-10-08 13:47:30 +02:00
862012e405 refactor: update kind 27235 auth event with recommendations from nip98 2024-10-08 13:46:54 +02:00
8689c7f753 fix: remove screen on nostr-login launch
Ignores the init options param when screen is passed
2024-10-08 13:45:58 +02:00
b
a3c45b504e Merge pull request 'Release Oct 7th' () from staging into main
Reviewed-on: 
2024-10-08 09:02:11 +00:00
b
da30dba368 Merge branch 'main' into staging 2024-10-08 08:59:35 +00:00
.
51e2ab6f8a chore: lint fix 2024-10-08 09:57:44 +01:00
.
9091bbc251 chore: landing page wording 2024-10-07 22:55:48 +01:00
331759de5c refactor: add useCallback, add methods and split effects 2024-10-07 20:37:46 +02:00
995c7ce293 feat(auth): nsec login with url params 2024-10-07 19:17:19 +02:00
3d5006a715 fix: removes retrier and updates notification 2024-10-07 17:24:25 +02:00
f38344b9ac fix: adds notifications 2024-10-07 17:20:00 +02:00
2b630c94b6 feat(opentimestamps): updates the flow and adds notifications 2024-10-07 17:19:32 +02:00
edeb22fb37 chore: updates namings 2024-10-07 17:18:27 +02:00
a2138f1de1 feat(opentimestamps): updates utils and adds comments 2024-10-07 17:18:06 +02:00
85bf907f54 feat(opentimestamps): updates data model 2024-10-07 17:17:37 +02:00
3b447dcf6a chore: merge branch 'main' into issue-166-open-timestamps 2024-10-07 16:18:29 +02:00
532cdaed8e refactor(auth): open nostr-login directly 2024-10-07 15:36:29 +02:00
67d545de2f fix: show import/export only for local 2024-10-07 13:44:04 +02:00
637e26bf35 refactor: init nostr-login, login method strategies, remove bunker 2024-10-07 13:36:45 +02:00
110621a125 feat: add nostrLoginAuthMethod to state 2024-10-07 13:36:45 +02:00
59e153595a refactor: z-index levels 2024-10-07 13:36:45 +02:00
0b79ebd909 refactor: z-index levels 2024-10-07 13:36:45 +02:00
e2dbed2b03 refactor: add nostr-login package, show not-allowed on disabled form fields 2024-10-07 13:36:45 +02:00
7c26edf84e chore(git): merge pull request from fixes-10-7 into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-10-07 11:27:41 +00:00
2d7bb234f4 refactor: next on each mark, including the final one 2024-10-07 12:59:55 +02:00
c4d50293ff refactor: toolbox order 2024-10-07 12:53:43 +02:00
b
89971fb176 Merge pull request 'Add Sigit ID as a path param' () from issue-171 into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-10-07 09:16:30 +00:00
b
acad24dc06 Merge branch 'staging' into issue-171 2024-10-07 09:15:38 +00:00
.
55abe814c9 fix: AGPL Licence, closes 2024-10-06 20:28:27 +01:00
21aa25a42a feat(opentimestamps): update the full flow 2024-10-06 15:37:04 +02:00
b
e33996c1f9 Merge branch 'staging' into issue-171 2024-10-05 21:02:40 +00:00
edbe708b65 feat(opentimestamps): updates data model and useSigitMeta hook 2024-10-02 14:47:32 +02:00
b
7056ad3cd3 Merge pull request 'Release to main' () from staging into main
Reviewed-on: 
2024-10-01 20:19:09 +00:00
b
7dffe75bd7 Merge branch 'main' into staging 2024-10-01 20:15:23 +00:00
8da2510a18 Merge pull request from 201-toolbox-update into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-30 14:20:07 +00:00
b92790ceed feat(opentimestamps): updates opentimestamps type 2024-09-27 16:03:40 +03:00
7f00f9e8bf feat(opentimestamps): updates signing flow 2024-09-27 16:00:48 +03:00
07f1a15aa1 feat(opentimestamps): refactors to timestamp the nostr event id 2024-09-27 14:18:26 +03:00
85bcfac2e0 feat(opentimestamps): adds timestamps to create flow 2024-09-26 15:54:06 +03:00
edfe9a2954 feat(opentimestamps): adds OTS library and retrier function 2024-09-26 15:50:28 +03:00
633c23e459 refactor: center banner notice text 2024-09-20 11:29:35 +02:00
2e1d48168a refactor: rename userId to npub 2024-09-20 11:13:48 +02:00
e05d3e53a2 fix: remove duplicate states and fix default signer 2024-09-20 10:26:32 +02:00
d8d51be603 fix: add small avatar when select is not showing 2024-09-19 15:05:03 +02:00
5f92906032 fix: add file and page index, hide select if not active 2024-09-19 15:02:19 +02:00
70cca9dd10 refactor: add getProfileUsername utility func 2024-09-19 14:46:22 +02:00
9bae5b9ba2 Merge branch 'staging' into 201-toolbox-update 2024-09-19 11:31:03 +00:00
9432e99b3b chore(git): merge pull request from 186-header-design into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-19 11:30:46 +00:00
ff875cc9d7 chore(git): merge pull request from 184-upload-add into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-19 11:29:44 +00:00
a1bf88d243 chore(git): merge pull request from 145-default-signer into 201-toolbox-update
Reviewed-on: 
2024-09-19 11:24:16 +00:00
67c3c74515 Merge branch '201-toolbox-update' into 145-default-signer 2024-09-19 11:20:05 +00:00
f81f2b0523 refactor: better variable names 2024-09-19 13:15:54 +02:00
182ef40d8d Merge branch 'staging' into 201-toolbox-update 2024-09-19 10:00:45 +00:00
39934f59c3 fix: last signer as default next 2024-09-19 11:46:34 +02:00
d45ea63c20 refactor: remove header height from calc 2024-09-19 10:53:53 +02:00
b
dd97dfbaf0 Merge pull request 'New release' () from staging into main
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-19 08:23:14 +00:00
aec0d0bdd8 fix: center block scrolling on mark items 2024-09-19 09:54:51 +02:00
5f39b55f68 feat: add banner and styling 2024-09-19 09:54:19 +02:00
ebd59471c7 fix: footer portal on relays 2024-09-19 09:53:16 +02:00
c2a149c872 Merge branch 'main' into staging 2024-09-18 15:12:47 +00:00
0091d3ec9e Merge pull request 'Update Blossom Event' () from blossom-upgrade into staging
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-09-18 15:09:31 +00:00
dd53ded518 fix: updates blossom authorisation event 2024-09-18 17:57:36 +03:00
6d78d9ed64 fix(create): uploading file adds to the existing file list, dedupe file list
Closes 
2024-09-17 17:56:53 +02:00
dfdcb8419d fix(marks): add default signer 2024-09-17 17:27:37 +02:00
f8a4480994 refactor(toolbox): reduce number of mark types
Closes 
2024-09-17 17:22:15 +02:00
c52fecdf4e chore(git): merge pull request from 176-178-pdf-quality-multiline into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-17 13:59:17 +00:00
43beac48e8 fix(pdf): keep upscaling to match viewport 2024-09-17 14:41:54 +02:00
f35e2718ab fix(pdf): mark embedding, position, multiline, & placeholder
Closes  , 
2024-09-17 14:33:50 +02:00
6ba3b6ec89 Merge branch 'staging' into issue-171 2024-09-16 12:37:39 +00:00
84c374bb2c chore(git): merge pull request from 196-ext-login-infinite-loading into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-16 12:27:43 +00:00
a53914b59d Merge branch 'staging' into 196-ext-login-infinite-loading 2024-09-16 12:19:09 +00:00
68c10d1831 chore(git): merge pull request from offline-flow-9-13 into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2024-09-16 12:18:18 +00:00
b
5a2a0ad9c4 Merge branch 'staging' into 196-ext-login-infinite-loading 2024-09-16 11:45:22 +00:00
62c1f1b37b fix: add show username 2024-09-16 12:06:11 +02:00
8267eb624b fix: add keys and show name for counterparts 2024-09-16 11:06:55 +02:00
e1c750495e refactor: useSigitProfiles removed, per user profile hook in avatar, refactor name tooltip 2024-09-16 11:06:55 +02:00
8b00ef538b fix: unzip and use timeout util 2024-09-16 11:06:55 +02:00
13254fbe06 feat: add exportedBy to useSigitMeta 2024-09-16 11:06:55 +02:00
1dfab7b82b refactor: metadatacontroller as singleton 2024-09-16 11:06:55 +02:00
759a40a4f9 fix(verify): offline flow 2024-09-16 11:06:55 +02:00
8b4f1a8973 fix(online-detection): use relative url 2024-09-16 11:00:16 +02:00
79ef9eb8d6 fix: url 2024-09-13 17:47:55 +02:00
b
aa8214d015 Merge branch 'staging' into issue-171 2024-09-13 10:07:24 +00:00
ba24e7417d refactor: log timeout error only, no toast 2024-09-13 11:14:10 +02:00
ea7e3a0964 refactor: update url for online status 2024-09-13 11:06:06 +02:00
b
675a763af3 Merge pull request 'New Release' () from staging into main
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-09-12 12:17:47 +00:00
bf1b3beb63 Merge branch 'main' into staging 2024-09-12 12:09:40 +00:00
2e58b58a6a refactor: move publish button to the bottom 2024-09-12 13:30:59 +02:00
17c1700554 fix(login): use const and make sure to clear timeout always 2024-09-12 13:24:05 +02:00
9191336722 refactor(login): update the delay message and increase timers 2024-09-12 12:17:58 +02:00
32a6f9d7a3 Merge pull request from hotfix-remove-loop-9-12 into staging
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-12 09:32:54 +00:00
5f0234a358 fix: remove unstable fetch events loop 2024-09-12 11:27:55 +02:00
235e76be4e fix: processing gift wraps and notifications ()
This change will potentially close multiple issues related to the gift-wrapped events processing (, ). Further testing will be required to confirm before closing each.

The commented-out code causes the race condition during the processing of the gift wraps with sigits.

During the processing we perform checks to see if sigit is outdated. In cases where sigit includes multiple signers it's possible for a signer to receive multiple sigit updates at once (especially noticeable for 3rd, 4th signer). Due to async nature of processing we can have same sigit enter processing flow with different states. Since this code also updates user's app state, which includes uploads to the blossom server it takes time to upload local user state which causes both to check against the stale data and un-updated app state. This results in both sigits being "new" and both proceed to update user state and upload app data. We have no guarantees as in which event will update last, meaning that the final state we end up with could be already stale.

The issue is also complicated due to the fact that we mark the gift wraps as processed and it's impossible to update the state without creating a new gift wrap with correct state and processing it last to overwrite stale state.

This is temporary solution to stop broken sigit states until proper async implementation is ready.

Co-authored-by: b
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
Co-authored-by: enes <mulahasanovic@outlook.com>
Co-committed-by: enes <mulahasanovic@outlook.com>
2024-09-12 08:26:59 +00:00
e48a396990 fix: verify/sign link 2024-09-11 17:29:47 +02:00
7c80643aba fix(login): extension login infinite loading
Fixes 
2024-09-11 16:44:45 +02:00
9c545a477c fix(errors): add custom timeout error 2024-09-11 16:33:53 +02:00
4d1e672268 feat(loading-spinner): add children support for default variant 2024-09-11 16:33:13 +02:00
79e14d45a1 chore: comment fix 2024-09-11 15:41:42 +02:00
64e8ebba85 chore: renamed sigitKey to sigitCreateId 2024-09-11 13:30:39 +02:00
4bc5882ab6 fix(loading): make sure the default spinner is absolute relative to root always 2024-09-11 13:27:50 +02:00
5dc8d53503 chore: sigitCreateId naming 2024-09-11 12:33:40 +02:00
86a16c13ce chore: comments and lint (typing) 2024-09-11 12:29:38 +02:00
7c027825cd style: lint fix 2024-09-11 12:03:23 +02:00
8e71592d88 fix: routing, removed useEffect 2024-09-11 11:59:12 +02:00
75a715d002 feat: Add Sigit ID as a path param 2024-09-10 16:00:48 +02:00
e2b3dc13fb chore(git): merge pull request from cache-checks-9-9 into staging
Reviewed-on: 
Reviewed-by: s <s@noreply.git.nostrdev.com>
2024-09-10 08:48:53 +00:00
b
0244090c6a Merge branch 'staging' into cache-checks-9-9 2024-09-09 18:44:36 +00:00
f0ba9da8af fix: outdated cache checks 2024-09-09 14:16:58 +02:00
1d1c675dd7 Merge pull request from lint-0-warnings into staging
Reviewed-on: 
Reviewed-by: s
2024-09-09 08:55:28 +00:00
70f646444b fix: add types to rootReducer, rename userRobotImage types 2024-09-09 10:19:37 +02:00
b
09229f42c7 Merge pull request 'new release' () from staging into main
Reviewed-on: 
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-06 18:59:34 +00:00
b
f516fe82d7 Merge branch 'main' into staging 2024-09-06 15:37:16 +00:00
bf506705e6 chore(git): merge pull request from 174-add-users-updates into staging
Reviewed-on: 
Reviewed-by: s <sabir@4gl.io>
2024-09-06 10:22:56 +00:00
b8811d730a fix(review): remove inline styles 2024-09-05 13:24:34 +02:00
479cca2180 chore(git): merge pull request from 172-sign-marks-scroll into 174-add-users-updates 2024-09-05 09:09:11 +00:00
82b7b9f7ce fix(mark): scroll into marks, add scroll margin and forwardRef
Closes 
2024-09-05 09:32:01 +02:00
3e075754e5 feat(create): touch support for dnd 2024-09-05 09:31:52 +02:00
c6010a5bef refactor(create): add counterpart design update 2024-09-05 09:31:52 +02:00
81f40c345a refactor: use ellipsis instead of 3 dots 2024-09-05 09:31:52 +02:00
734026b2ee fix: bad margin value 2024-09-05 09:31:52 +02:00
5caa7f2282 refactor: update add counterparty design 2024-09-05 09:31:52 +02:00
fa7f0e2fc0 refactor(create): move add counterpart below, select role to toggle, remove duplicate code 2024-09-05 09:31:51 +02:00
7d0d4fcb48 chore(git): merge pull request from 177-sticky-side-columns into staging
Reviewed-on: 
Reviewed-by: b <b@4j.cx>
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-05 07:30:55 +00:00
9223857e18 refactor(drawing): fix mouse comments, use pointer 2024-09-04 14:44:27 +02:00
2be7f3d51b fix(tabs): add tab icons 2024-09-04 14:05:36 +02:00
36281376bc fix(mobile): use dynamic vh and one-by-one horizontal scroll 2024-09-04 13:44:10 +02:00
a3effd878b fix(pdf): use minified version of pdf 2024-09-03 16:46:54 +02:00
b20ffe6e87 refactor(mobile): drawing mouse to pointer events, field colors and actions feedback, touch action 2024-09-03 10:14:43 +02:00
757012399a fix: main css background, avoid overscroll showing white edge 2024-09-02 14:41:25 +02:00
c2d065a8a5 refactor(pdf-dist): optimize workers 2024-09-02 12:59:35 +02:00
d1b9eb55d8 fix(spinner): remove dummy desc and use variants 2024-09-02 12:49:51 +02:00
e5f8b797bb refactor(spinner): use data variant for styles 2024-09-02 11:36:31 +02:00
d627db5ac0 refactor(spinner): new default design 2024-09-02 11:35:39 +02:00
6abdb0ae2b refactor: files styling, move styling reset to app from module 2024-08-30 17:58:40 +02:00
be9bfc28c8 refactor(mobile): tabs navigation, observer active for tabs, tabs layout scrolling 2024-08-30 17:39:32 +02:00
2e9e9b0a06 refactor(mobile): styling for sign actions box 2024-08-30 17:37:57 +02:00
15aaef948d fix(create): block if no signers 2024-08-30 13:27:10 +02:00
bee566424d refactor: split signers and viewers in the users section 2024-08-30 12:55:04 +02:00
f8533b0ffd refactor(content): add small loading spinner to content sections
Closes 
2024-08-30 12:28:33 +02:00
6f7d4c9dcf fix(mobile): active tab default state and styling 2024-08-30 10:57:51 +02:00
d9be05165f feat(mobile): tabs and scrolling 2024-08-29 17:18:32 +02:00
2367eb1887 chore(browser-warn): add alt attribute to image, part 2 2024-08-29 17:03:16 +02:00
156bc59e1a chore(browser-warn): add alt attribute to image 2024-08-29 17:02:32 +02:00
a48751b9a8 refactor(footer): make footer a portal and use as needed 2024-08-29 16:43:09 +02:00
b3564389f9 refactor(sticky-columns-layout): initial responsiveness and breakpoints 2024-08-29 13:09:56 +02:00
5c8cbc1956 refactor(toolbox): responsiveness and cleanup 2024-08-29 13:09:56 +02:00
b
20496c7e3e Merge pull request 'Releasing new design' () from staging into main
Reviewed-on: 
Reviewed-by: eugene <eugene@nostrdev.com>
2024-08-21 11:38:24 +00:00
194 changed files with 21958 additions and 7318 deletions
.eslintrc.cjs
.gitea/workflows
.gitignore.releasercCHANGELOG.mdcontributing.md
docs
index.htmlpackage-lock.jsonpackage.json
public
src

@ -6,7 +6,7 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended'
],
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs'],
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs', "*.min.js"],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {

@ -15,20 +15,47 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18
node-version: 20
- name: Install Dependencies
run: npm ci
run: |
npm ci
apt-get update
apt-get install zip -y
apt-get install jq -y
- name: Create .env File
run: echo "VITE_MOST_POPULAR_RELAYS=${{ vars.VITE_MOST_POPULAR_RELAYS }}" > .env
- name: Create Build
run: npm run build
run: |
npm run build
zip -r frontend.zip dist/*
- name: Release Build
- name: Deploy Build
run: |
npm -g install cloudron-surfer
surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io
surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io
surfer put dist/* / --all -d
surfer put dist/.well-known / --all
surfer put dist/.well-known / --all
- name: Create Empty Release (assets are posted later)
run: |
npm i
npm i -g semantic-release
echo "do a semantic-release DRY RUN to make the job fail if there are no changes to release"
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.nostrdev.com/sigit/sigit.io semantic-release --dry-run | grep -q "There are no relevant changes, so no new version is released." && exit 1
echo "now do the actual release"
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.nostrdev.com/sigit/sigit.io semantic-release
- name: Upload assets to release
run: |
echo "fetching release id"
RELEASE_ID=`curl -k 'https://git.nostrdev.com/api/v1/repos/sigit/sigit.io/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.id'`
echo "fetching release body"
RELEASE_BODY=`curl -k 'https://git.nostrdev.com/api/v1/repos/sigit/sigit.io/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.body'`
echo "Updating release body"
curl --data '{"draft": false,"body":"'"$RELEASE_BODY\n\nFor installation instructions, please visit https://docs.sigit.io/#/"'"}' -X PATCH --header 'Content-Type: application/json' -k https://git.nostrdev.com/sigit/sigit.io/releases/$RELEASE_ID?access_token=${{ secrets.RELEASE_TOKEN }}
echo "Uploading assets"
URL="https://git.nostrdev.com/api/v1/repos/sigit/sigit.io/releases/$RELEASE_ID/assets?access_token=${{ secrets.RELEASE_TOKEN }}"
curl -k $URL -F attachment=@frontend.zip

@ -15,7 +15,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18
node-version: 20
- name: Audit
run: npm audit --omit=dev

1
.gitignore vendored

@ -9,6 +9,7 @@ lerna-debug.log*
node_modules
dist
dist-zip
dist-ssr
*.local

31
.releaserc Normal file

@ -0,0 +1,31 @@
{
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"package.json"
]
}
],
[
"@saithodev/semantic-release-gitea",
{
"giteaUrl": "https://git.nostrdev.com/",
"assets": [
{
"path": "dist-zip/dist.zip"
}
]
}
]
]
}

435
CHANGELOG.md Normal file

@ -0,0 +1,435 @@
## [1.0.4](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.3...v1.0.4) (2025-01-31)
### Bug Fixes
- adding jq package ([673516e](https://git.nostrdev.com/sigit/sigit.io/commit/673516e3ce26a4006c112cd19d5987e181a4b10d))
## [1.0.3](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.2...v1.0.3) (2025-01-31)
### Bug Fixes
- bundling frontend with release ([889bc0e](https://git.nostrdev.com/sigit/sigit.io/commit/889bc0e4fcc21b79d6fc643317ae3423497e265e))
## [1.0.2](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.1...v1.0.2) (2025-01-31)
### Bug Fixes
- adding api to release url ([aa32dae](https://git.nostrdev.com/sigit/sigit.io/commit/aa32dae62282be9c07ea6e4af514925585d97a2b))
## [1.0.1](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.0...v1.0.1) (2025-01-31)
### Bug Fixes
- test to see if the automated release works ([031deef](https://git.nostrdev.com/sigit/sigit.io/commit/031deef6ca3b9794d3fadf67b67884137e6af9f2))
# 1.0.0 (2025-01-31)
### Bug Fixes
- add default title for sigit ([ef5376e](https://git.nostrdev.com/sigit/sigit.io/commit/ef5376e2d1ec08a3327850a7035c03af2e16693d))
- add default typography styles ([2cd851a](https://git.nostrdev.com/sigit/sigit.io/commit/2cd851a7c153c051b55365481a4e9ea6938e8be3))
- add file and page index, hide select if not active ([5f92906](https://git.nostrdev.com/sigit/sigit.io/commit/5f92906032ab0ea6a09ae9d104a64f81fd10c095))
- add files and marked to sign page exports ([9dd190d](https://git.nostrdev.com/sigit/sigit.io/commit/9dd190d65b18429ded79213bdfdf91a88aac0062))
- add keys and show name for counterparts ([8267eb6](https://git.nostrdev.com/sigit/sigit.io/commit/8267eb624b76b0efc62a917da962a9865affad38))
- add mark label ([c3dacbe](https://git.nostrdev.com/sigit/sigit.io/commit/c3dacbe1114cdcc408a4dde3019a31c1570afa3e))
- add missing null and reduce warning limit ([bec3c92](https://git.nostrdev.com/sigit/sigit.io/commit/bec3c92b03c4127c9f768770640f6c41d0c329be))
- add parantheses, invoke unixNow ([07d25eb](https://git.nostrdev.com/sigit/sigit.io/commit/07d25ebbd2e125e5a51ddcb27696a3de3d58f0f9))
- add Roboto font ([6a1f04e](https://git.nostrdev.com/sigit/sigit.io/commit/6a1f04ec6b11f7a054ef9ce43283961ff40c6e78))
- add show username ([62c1f1b](https://git.nostrdev.com/sigit/sigit.io/commit/62c1f1b37ba9dc78a29d3f73617177a90c9b6eba))
- add small avatar when select is not showing ([d8d51be](https://git.nostrdev.com/sigit/sigit.io/commit/d8d51be603a9817ffc449eb2ffd81382cba47ec8))
- add timeout in publishing updated app data and sending notifications ([6b135ac](https://git.nostrdev.com/sigit/sigit.io/commit/6b135ac54dd9d8e4c9948be92edcfa907d419940))
- add types to rootReducer, rename userRobotImage types ([70f6464](https://git.nostrdev.com/sigit/sigit.io/commit/70f646444b9ed39798737851edc9b401c759a3d2))
- adding link to source and updating home page wording ([c3d5a10](https://git.nostrdev.com/sigit/sigit.io/commit/c3d5a1042c3d66e75b8cb537d5bfff8a99909a02))
- addressing comments ([8d8c38e](https://git.nostrdev.com/sigit/sigit.io/commit/8d8c38e90bce13c3a0b833a5598a790c2086a267))
- adds notifications ([f38344b](https://git.nostrdev.com/sigit/sigit.io/commit/f38344b9acbb538f617d76796f75139db626164f))
- AGPL Licence, closes [#197](https://git.nostrdev.com/sigit/sigit.io/issues/197) ([55abe81](https://git.nostrdev.com/sigit/sigit.io/commit/55abe814c967e3e126d40408239f62ac111ba9e7))
- amends RelayMap to return a default sigit relay when no other relays are found ([2355da0](https://git.nostrdev.com/sigit/sigit.io/commit/2355da02d2c760f94509bcb4e375e75b61a2ce9f))
- amends the relay look up method to return default relay set ([52fe523](https://git.nostrdev.com/sigit/sigit.io/commit/52fe523196986d651c7fa14224e2b4324683a6f4))
- app bar z-index ([87c6807](https://git.nostrdev.com/sigit/sigit.io/commit/87c6807ba08ef0217bed4744e6241d479bf74967))
- arrayBuffer access ([b3fc3c6](https://git.nostrdev.com/sigit/sigit.io/commit/b3fc3c6715739b92e63e3a14013fb97dc81360f7))
- background overlap ([202c98c](https://git.nostrdev.com/sigit/sigit.io/commit/202c98c94c3b0ae99d28055b002c6120e9845451))
- bad margin value ([734026b](https://git.nostrdev.com/sigit/sigit.io/commit/734026b2eeba2c5109ebdf0d8b5ef77f7ca36249))
- better UX when clicking on logo when on home screen or `home` button in footer ([834d70d](https://git.nostrdev.com/sigit/sigit.io/commit/834d70d7747148d08b3c19720aca8d56b3d0de68))
- bug, when valid npub, clicking + was saying npub was invalid ([99fa3ad](https://git.nostrdev.com/sigit/sigit.io/commit/99fa3add562c4efa013954c0e0df95723b9aea3d))
- build failing due to type issue ([652ea06](https://git.nostrdev.com/sigit/sigit.io/commit/652ea06c0de77af453f1c4aeee4d473fe265d93a))
- button colour ([4c04c12](https://git.nostrdev.com/sigit/sigit.io/commit/4c04c1240344c0f07a503bb0a5f56e7bfc791c8f))
- card icons ([0d49c49](https://git.nostrdev.com/sigit/sigit.io/commit/0d49c4945977826cf90f2002ca1e06880f646003))
- center block scrolling on mark items ([aec0d0b](https://git.nostrdev.com/sigit/sigit.io/commit/aec0d0bdd8bf435003e96fa90d1e9966e1b0b3e0))
- change sign to create ([f35f469](https://git.nostrdev.com/sigit/sigit.io/commit/f35f469547737aea8b50e29fb7813a69a482d85b))
- **ci:** add license check in staging workflow ([4af5781](https://git.nostrdev.com/sigit/sigit.io/commit/4af578133c17bbb5f3c4f60873dd1ae37e679e18))
- **ci:** fix hook colors ([ea7fde4](https://git.nostrdev.com/sigit/sigit.io/commit/ea7fde4b38234128920de116609ad05e367bf9dd))
- **CI:** fixed secret ([3e360aa](https://git.nostrdev.com/sigit/sigit.io/commit/3e360aab1510dc07c8c67b712ea666a72bb28469))
- **ci:** run lint-staged always, fix lint-stage commands ([d43067f](https://git.nostrdev.com/sigit/sigit.io/commit/d43067f70ebb5d52c08452402540406ca9ff4cfa))
- clear hasSubscribed after the logout ([1d1986f](https://git.nostrdev.com/sigit/sigit.io/commit/1d1986f0829f4c1ca183b017150ecfdbaa96a86c))
- clicking logo not redirecting to home ([69efd9e](https://git.nostrdev.com/sigit/sigit.io/commit/69efd9e09d043403c987a5ab7fddd68158a1d22a))
- clicking on marked fileds is losing input text/squiggle, squiggle field is mobile friendly ([602e23c](https://git.nostrdev.com/sigit/sigit.io/commit/602e23c719a0d583b194d627761efff14fb9b074))
- color scheme ([d7f9807](https://git.nostrdev.com/sigit/sigit.io/commit/d7f9807e20d0bb4d85ab8794173e3bd71adb4ca4))
- **column-layout:** wrap content column to prevent expanding ([a8020e6](https://git.nostrdev.com/sigit/sigit.io/commit/a8020e6db2ad88f29e6cf2bb35cca1cace56cb07))
- composition for links and buttons ([804bb6c](https://git.nostrdev.com/sigit/sigit.io/commit/804bb6c9acb7f4eedd547eba7516e91e4a218d86))
- convert npub/nip05 to lowercase on adding as signer/viewer ([fff0fd7](https://git.nostrdev.com/sigit/sigit.io/commit/fff0fd762dc0b3ddde04657207d09c795cdaa341))
- counterpart search NIP05 glitching ([0fd0f26](https://git.nostrdev.com/sigit/sigit.io/commit/0fd0f26fc7ef87aea7c48ccddae822d23d2b8853))
- create page, improving message "preparing document for signing" ([98fbe80](https://git.nostrdev.com/sigit/sigit.io/commit/98fbe80648e6b8e80af1e6c15de8bb963d27c070))
- **create-page:** file list ([1caeb48](https://git.nostrdev.com/sigit/sigit.io/commit/1caeb48e6c91e0c8fd052caa8df0a5299f3e5f03))
- **create-page:** only show signers in counterpart select ([29e6c85](https://git.nostrdev.com/sigit/sigit.io/commit/29e6c851504f884cdeae738bb70323a7703f2f74))
- **create-page:** show other file types in content ([b12ce25](https://git.nostrdev.com/sigit/sigit.io/commit/b12ce258eb83620aa0dae546f146d381b9d61093))
- **create:** block if no signers ([15aaef9](https://git.nostrdev.com/sigit/sigit.io/commit/15aaef948d264ad76b27dfa80ff22051e0eb1021))
- **create:** remove small drawn fields ([902ad73](https://git.nostrdev.com/sigit/sigit.io/commit/902ad73fafa184d8f36b03d3e4b839afa724bded)), closes [#234](https://git.nostrdev.com/sigit/sigit.io/issues/234)
- **create:** throw on mark with no counterpart ([624afae](https://git.nostrdev.com/sigit/sigit.io/commit/624afae8514420a36034ef29814920579da308f6))
- **create:** uploading file adds to the existing file list, dedupe file list ([6d78d9e](https://git.nostrdev.com/sigit/sigit.io/commit/6d78d9ed643296b6dfa6ab3ffd0ae5295a46243a)), closes [#184](https://git.nostrdev.com/sigit/sigit.io/issues/184)
- **deps:** update axios ([115a397](https://git.nostrdev.com/sigit/sigit.io/commit/115a3974e278c137aa2d490327bcc92e4ed0c492))
- disable login, register fields, add coming soon ([0a74ad9](https://git.nostrdev.com/sigit/sigit.io/commit/0a74ad97b2a54f36acb39f8f57bf646f595b7e37))
- disables redundant metaInNavState updates ([7463384](https://git.nostrdev.com/sigit/sigit.io/commit/746338465d1c370b503be257a9d57e132e0d7192))
- display `no results` when no submissions are found ([bbe34b6](https://git.nostrdev.com/sigit/sigit.io/commit/bbe34b60116af3081a45ec55dde6e6e7620818a3))
- displays complete marks from other users ([4d4a5b6](https://git.nostrdev.com/sigit/sigit.io/commit/4d4a5b63cf466af15d2b2414a52adeda8bd7cb8a))
- **DM:** removed direct download link ([0fab6b5](https://git.nostrdev.com/sigit/sigit.io/commit/0fab6b5cdc1420854abc81493b0a83e6993336c7))
- **draw:** add resize cursor to resize handle ([0d1a7ba](https://git.nostrdev.com/sigit/sigit.io/commit/0d1a7ba17118996d91c57737cfac4b036d796e31))
- **drawfield:** match label and select ([923a47b](https://git.nostrdev.com/sigit/sigit.io/commit/923a47b4d086563fc1f514b962cfdecb281d6b87))
- **drawing:** clamp DrawField within img ([2f54184](https://git.nostrdev.com/sigit/sigit.io/commit/2f5418462584ac5426c717680590d9156801ec57)), closes [#154](https://git.nostrdev.com/sigit/sigit.io/issues/154)
- enable verify button ([f4a837a](https://git.nostrdev.com/sigit/sigit.io/commit/f4a837ae098bf5b660499f0c005089d8f9f6b37a))
- entering decryption key manually does not work because of encoded URI ([e498ecb](https://git.nostrdev.com/sigit/sigit.io/commit/e498ecb082ebc5c2022b225f7824a12932282d6e))
- **errors:** add custom timeout error ([9c545a4](https://git.nostrdev.com/sigit/sigit.io/commit/9c545a477cf5e6e9ecf8f171af3857fd64b5a78d))
- failed DM error handling ([608400d](https://git.nostrdev.com/sigit/sigit.io/commit/608400d010262d3e2dc59cd03508a608560b521b))
- false positive case of navigator.online ([307f32b](https://git.nostrdev.com/sigit/sigit.io/commit/307f32bb7b15bf796a80ab464d4d6d844815c180))
- fetch app data from after login ([fa7a6e8](https://git.nostrdev.com/sigit/sigit.io/commit/fa7a6e85f4f7df3adac8cfe884f0592dd6c11aca))
- file path ([79f37a8](https://git.nostrdev.com/sigit/sigit.io/commit/79f37a842f919f052b08ca2afe341a296e55a18c))
- **files:** show other file types in content for create, fix sign and verify error ([86095cb](https://git.nostrdev.com/sigit/sigit.io/commit/86095cba5c624943c837b6b997f9485411444b01))
- first find metadata on purplepag relay and then try other relays ([6981bef](https://git.nostrdev.com/sigit/sigit.io/commit/6981bef65ad458db69874cfc782b4f85109f284b))
- font url typo ([fcd00d9](https://git.nostrdev.com/sigit/sigit.io/commit/fcd00d9e9ce42ab7e3f8c4a9836db3d76adadb34))
- fonts ([aa5aa60](https://git.nostrdev.com/sigit/sigit.io/commit/aa5aa60c6a749c8c4b37a9c2a7e0b15794653a73))
- footer 'Home' button scroll to top when on home page, fixed logic ([afbe05b](https://git.nostrdev.com/sigit/sigit.io/commit/afbe05b4c88d5042c17f4a59c0739dc8942642a9))
- footer buttons ([e280e87](https://git.nostrdev.com/sigit/sigit.io/commit/e280e873424d6e92839e64d274997e51c1c135ed))
- footer padding and responsiveness ([45f0764](https://git.nostrdev.com/sigit/sigit.io/commit/45f0764fa802bccc75b320527f7a18708b7302f4))
- footer portal on relays ([ebd5947](https://git.nostrdev.com/sigit/sigit.io/commit/ebd59471c79335555ddf43748b3dd3457b65eb42))
- format fixed for iv in encryption key ([c4ef090](https://git.nostrdev.com/sigit/sigit.io/commit/c4ef090f3c7e1da126fb5f09d4918d1c25867c0c))
- gap, spacing ([99856fd](https://git.nostrdev.com/sigit/sigit.io/commit/99856fd8f2255ae8d1a667c67b86cd48373a0fd5))
- getRobohash function will do the conversion of pubkey ([9aa1066](https://git.nostrdev.com/sigit/sigit.io/commit/9aa10664a7e5b9af4bde3cb6f5cc55d0334eaa54))
- **git-hooks:** add executable flag ([7b5a122](https://git.nostrdev.com/sigit/sigit.io/commit/7b5a12246d792672734747ca33ff77cfc05c0537))
- handle navigation after create ([00db735](https://git.nostrdev.com/sigit/sigit.io/commit/00db735106767ccd3a934c9d4f9124b40ef2b7b3))
- handle the case when zip entry is undefined ([e4675af](https://git.nostrdev.com/sigit/sigit.io/commit/e4675af4dd8267a88bed50bf3d3f1aa323d17987))
- hanlde error in decryption of zip file ([660efb3](https://git.nostrdev.com/sigit/sigit.io/commit/660efb3b677130ecfe1248405acb40f16f5c4572))
- home screen style fixed for mobile view ([6f8830a](https://git.nostrdev.com/sigit/sigit.io/commit/6f8830a77ccf40eed0c11ccc65ae5d736dfb981d))
- **home-page:** sigit file type display now correctly shows multiple file types ([acc8c84](https://git.nostrdev.com/sigit/sigit.io/commit/acc8c84617a38443d301804f540f5d40b4a0d461))
- **home:** focus outlines and decorations ([72d0e06](https://git.nostrdev.com/sigit/sigit.io/commit/72d0e065eae580611c5a9252c8521086495dd2c5))
- homepage alpha warning ([867e1b8](https://git.nostrdev.com/sigit/sigit.io/commit/867e1b88c2bd60b1f7e3f566d940e4e48f9043c0))
- IconButton conflict, username layout ([9dae3a4](https://git.nostrdev.com/sigit/sigit.io/commit/9dae3a48bef7f4d0d0ce273f6cf9ec78f47b1017))
- icons, use FontAwesome package ([6f4737d](https://git.nostrdev.com/sigit/sigit.io/commit/6f4737d75cc15b0d72608775153c119d10265f0f))
- If creator is not the first signer we should not redirect to /sign page ([ee3e0e1](https://git.nostrdev.com/sigit/sigit.io/commit/ee3e0e1bb1f338b912f0352acb61fdc7c1749e6e))
- improve font support ([a63ea91](https://git.nostrdev.com/sigit/sigit.io/commit/a63ea913d9c87028e78b49d8ef0886173fea877a))
- in pdf marking if counterpart does not have any of name, displayname, username then show pubkey ([42d74c6](https://git.nostrdev.com/sigit/sigit.io/commit/42d74c656a495a7600aa5e5232ea8ed1c231f524))
- In sign page, when doc is fully signed, update search params with update file url and key ([05c3f49](https://git.nostrdev.com/sigit/sigit.io/commit/05c3f49a17bc78b6b57a59a9a6dc475ab57d5034))
- include hidden folders in surfer upload ([970c5f5](https://git.nostrdev.com/sigit/sigit.io/commit/970c5f5e8bfc0129283fe14c7e6fa3c9a8a35ea4))
- include purplepage and userkindpages relays when searching for user in create page ([8a9910d](https://git.nostrdev.com/sigit/sigit.io/commit/8a9910db87dcd27488ef1588a18e4480b16adde8))
- including signatures in both export and encrypted export ([6716c3d](https://git.nostrdev.com/sigit/sigit.io/commit/6716c3da636f9c0a1b168cb2cb0a98da97a6c5c3))
- increased timeout for extension user prompt ([2c2eeba](https://git.nostrdev.com/sigit/sigit.io/commit/2c2eeba83f2fb6cdf73228733448228151f1de0f))
- inform user then search term provided no results ([24463a5](https://git.nostrdev.com/sigit/sigit.io/commit/24463a53c50f00313e3502438ae5f47254635253))
- inlined svg background images ([c22b1e4](https://git.nostrdev.com/sigit/sigit.io/commit/c22b1e4b5a04943e61ae162911c28496da2b51fd))
- input font-family inherit ([f21d158](https://git.nostrdev.com/sigit/sigit.io/commit/f21d158a8ec5fd204fd513f140cfa33b97cb5383))
- label ([0163d51](https://git.nostrdev.com/sigit/sigit.io/commit/0163d51155ffff17b41d9e60871143b6be68e7d6))
- landing page ([cc9fb50](https://git.nostrdev.com/sigit/sigit.io/commit/cc9fb50b079ff4f0d67ade1f30973f437f9e4ef6))
- landing page wording ([4dd6b6d](https://git.nostrdev.com/sigit/sigit.io/commit/4dd6b6d7a449a0f41db995fb577c42124b234137))
- last signer as default next ([39934f5](https://git.nostrdev.com/sigit/sigit.io/commit/39934f59c375d774f1fb4f7a97676304f5e9fd41))
- leaky styling and warnings ([6f88f22](https://git.nostrdev.com/sigit/sigit.io/commit/6f88f22933ddd6bf787c69d0dcaf12032d5ea4f9)), closes [#147](https://git.nostrdev.com/sigit/sigit.io/issues/147)
- **lint:** add deps, remove any, update warning limit ([61f39d1](https://git.nostrdev.com/sigit/sigit.io/commit/61f39d17ff4619c2b6beedf7d3189c03278aeede))
- **lint:** update warning limit ([404f4aa](https://git.nostrdev.com/sigit/sigit.io/commit/404f4aa3a1e8db7ff90300b107496f59a28a539e))
- list item key ([c7dfb28](https://git.nostrdev.com/sigit/sigit.io/commit/c7dfb2864acea7e20237b2f8da16f915f9544d43))
- loading spinner states, timestamp the file, and lint fixes ([748cb16](https://git.nostrdev.com/sigit/sigit.io/commit/748cb16f9fe44f8fc06ac6142cdd01c348bc7c1c))
- loading spinner, improve desc readability, use favicon instead of circle ([5a4da18](https://git.nostrdev.com/sigit/sigit.io/commit/5a4da1834b8628199d8577afda957fa359040b63))
- **loading:** make sure the default spinner is absolute relative to root always ([4bc5882](https://git.nostrdev.com/sigit/sigit.io/commit/4bc5882ab60cd4b31f532d506433d24c67548082))
- login with hex key does not work, missing proper error when nsec or private key is wrong ([213ae79](https://git.nostrdev.com/sigit/sigit.io/commit/213ae79bf52da893dccc4576dd690cddbb0e4bd8))
- **login:** extension login infinite loading ([7c80643](https://git.nostrdev.com/sigit/sigit.io/commit/7c80643aba266fc0279c593f1951931f3fbb9ce2)), closes [#196](https://git.nostrdev.com/sigit/sigit.io/issues/196)
- **Login:** fixed loginWithExtension func ([be4e7ab](https://git.nostrdev.com/sigit/sigit.io/commit/be4e7ab2bda98927be90c0150afc37e575fb1a8e))
- **login:** redirect to landing instead of login popup page ([84062f2](https://git.nostrdev.com/sigit/sigit.io/commit/84062f2ed0e9953c5c3830d792a60577ddb3ebe4))
- **login:** update login method before using nostrController instance ([1f98020](https://git.nostrdev.com/sigit/sigit.io/commit/1f980201dde2cf9b4e4ec8a0470e49944f797c5d))
- **login:** use const and make sure to clear timeout always ([17c1700](https://git.nostrdev.com/sigit/sigit.io/commit/17c170055488ba793658a32745c10bf8f2c32981))
- logout user if decryption fails due to diff pubkeys ([c96a7fa](https://git.nostrdev.com/sigit/sigit.io/commit/c96a7fac4fcf59eacf097be9c39fcc13cd6f04b9))
- logout user if signEvent's and auth's pubkeys are diff ([8153ef0](https://git.nostrdev.com/sigit/sigit.io/commit/8153ef03fbf693f6ec1ecd1425dc03d76d29416f))
- **LogOut:** used log out action instead of clearState utility ([803e242](https://git.nostrdev.com/sigit/sigit.io/commit/803e242b01fb3aa68c0d305b340f8515a58b1717))
- looping trough robo sets, image not shown when visiting profile while not logged in ([6604ea2](https://git.nostrdev.com/sigit/sigit.io/commit/6604ea2046afea40fc3deeb0548016b3ec1fe53d))
- main css background, avoid overscroll showing white edge ([7570123](https://git.nostrdev.com/sigit/sigit.io/commit/757012399ac2236b9643b5812f28b1477bc54738))
- manage pending relay connection requests ([f9fcfb1](https://git.nostrdev.com/sigit/sigit.io/commit/f9fcfb1c9e70a73acfda90ce4d1c1215623972df))
- **mark:** css position ([413da78](https://git.nostrdev.com/sigit/sigit.io/commit/413da78c5c06b5b35740692f6b7c00125a7c8969))
- marking ([b22f577](https://git.nostrdev.com/sigit/sigit.io/commit/b22f577cc2de21bb1f8370c84a1d263f09fd7eb6))
- **marks:** add default signer ([dfdcb84](https://git.nostrdev.com/sigit/sigit.io/commit/dfdcb8419d5b3c8ca34c6f9590107ce120a53174))
- **marks:** add file grouping for marks, fix read pdf types ([b6479db](https://git.nostrdev.com/sigit/sigit.io/commit/b6479db2665ef500d20831dcd941d5ce728b79d3))
- **marks:** assign selectedMarkValue to currentValue and mark.value ([78060fa](https://git.nostrdev.com/sigit/sigit.io/commit/78060fa15fb55c597918042559d544ff4528dc24))
- **mark:** scroll into marks, add scroll margin and forwardRef ([82b7b9f](https://git.nostrdev.com/sigit/sigit.io/commit/82b7b9f7ce1e8e6edfc547e18b56090236b02bdf)), closes [#172](https://git.nostrdev.com/sigit/sigit.io/issues/172)
- **MetadataController:** fixed getting popular relays ([026537c](https://git.nostrdev.com/sigit/sigit.io/commit/026537c75b26a74fb067c210d9aedd6105d7a23c))
- missing id/name on custom select input ([d0e3704](https://git.nostrdev.com/sigit/sigit.io/commit/d0e3704ed6a2c08eede388e353bcf76880873411))
- **mobile:** active tab default state and styling ([6f7d4c9](https://git.nostrdev.com/sigit/sigit.io/commit/6f7d4c9dcfb696264f67db11a3a83a3c52ac0103))
- **mobile:** use dynamic vh and one-by-one horizontal scroll ([3628137](https://git.nostrdev.com/sigit/sigit.io/commit/36281376bc2be5592193d01c483916b6c8859912))
- modal override removed ([64b6f83](https://git.nostrdev.com/sigit/sigit.io/commit/64b6f8309f361a6db6c6e91005b50969cdbfa549))
- move nostr login to nostr route ([3c22429](https://git.nostrdev.com/sigit/sigit.io/commit/3c22429941f476b4c55f4f862580fe1520a665a8))
- moves sample data to a separate json file ([1de8e89](https://git.nostrdev.com/sigit/sigit.io/commit/1de8e89beb555e881b822ed0f37e9a44b03f321c))
- moves styling to SVG ([38cd88f](https://git.nostrdev.com/sigit/sigit.io/commit/38cd88fd866e19168b91745d5cf98543eebbc0f7))
- navigation to profile page from username component ([d502474](https://git.nostrdev.com/sigit/sigit.io/commit/d5024745f163ef38a833c3253177ebc67f2e8642))
- nested a links in card ([e4a7fa4](https://git.nostrdev.com/sigit/sigit.io/commit/e4a7fa4892b05f648dfb988b898fb0658d2f22d4))
- next signer and spinner anim duration ([d8adb2c](https://git.nostrdev.com/sigit/sigit.io/commit/d8adb2c74471bf55351b3649c699f8b6d4360a47))
- no need to listen for authUrl in createNsecBunkerSigner method of NostrController ([3626368](https://git.nostrdev.com/sigit/sigit.io/commit/3626368e95b228f30dc6855f10d190f92d833aa3))
- node version bump from 18 to 20 ([354312b](https://git.nostrdev.com/sigit/sigit.io/commit/354312bd96d6092502812d591d41947182aa335b))
- nostr-login custom outbox relays ([555504f](https://git.nostrdev.com/sigit/sigit.io/commit/555504f42f030028af6b280d664e1024d63e1e12))
- nsec login, metadata overlapping, robohash image in metadata state ([e3e15b7](https://git.nostrdev.com/sigit/sigit.io/commit/e3e15b7af139028ec4a3f27d6f037f1465eb2a56))
- **Offline:** fixed 0.0.0.0 host ([7be9897](https://git.nostrdev.com/sigit/sigit.io/commit/7be98978dd2b492b4aad4b38f9289e1b45939132))
- **online-detection:** use relative url ([8b4f1a8](https://git.nostrdev.com/sigit/sigit.io/commit/8b4f1a8973abe0a395cacf9617aab80bb606bf61))
- Opening a sigit asks you to sign when you are not the next signer ([ae3d461](https://git.nostrdev.com/sigit/sigit.io/commit/ae3d461661f41f7243828015d26c2431ebb91fda))
- opening link to sign a file while not logged in is not redirecting correctly ([eff8827](https://git.nostrdev.com/sigit/sigit.io/commit/eff8827a86d97c88386798fe4e5449bad42b3539))
- optional label for download button in filelist ([3c230e6](https://git.nostrdev.com/sigit/sigit.io/commit/3c230e6fb4b5971669a6df8e4eeb1f9f78339873))
- outdated cache checks ([f0ba9da](https://git.nostrdev.com/sigit/sigit.io/commit/f0ba9da8af9abc24fdae0b6fe65b83326d904e44))
- page scrolling ([97c8271](https://git.nostrdev.com/sigit/sigit.io/commit/97c82718cb2991309894d2fd823f25035504b9be))
- pdf to png scaling is 1, bottom position is now included ([4556bd0](https://git.nostrdev.com/sigit/sigit.io/commit/4556bd0c66f275956fe850b77f1b5c2a392c7760))
- **pdf:** add border to style ([ecc1707](https://git.nostrdev.com/sigit/sigit.io/commit/ecc1707212fdee75775d38154ffbddc22619fb88))
- **pdf:** add proper default width value ([a442e71](https://git.nostrdev.com/sigit/sigit.io/commit/a442e71087c75f9e224794e4160b9f8d6dfc71d9))
- **pdf:** dynamic mark scaling ([ea09daa](https://git.nostrdev.com/sigit/sigit.io/commit/ea09daa6692e905c703d3800b8a8adbdb391f6b5))
- **pdf:** font style consistency ([31f3675](https://git.nostrdev.com/sigit/sigit.io/commit/31f36750cd5479fc05e2a86ade5153e6a65955f6))
- pdfjs import ([d5e0769](https://git.nostrdev.com/sigit/sigit.io/commit/d5e07696926f554894bb316df2e6a49e00983229))
- **pdf:** keep upscaling to match viewport ([43beac4](https://git.nostrdev.com/sigit/sigit.io/commit/43beac48e85c32b09e10dd611c259ce2c3783a4a))
- **pdf:** mark embedding, position, multiline, & placeholder ([f35e271](https://git.nostrdev.com/sigit/sigit.io/commit/f35e2718abcdae0d5c9624444d5ad33c5e368f33)), closes [#176](https://git.nostrdev.com/sigit/sigit.io/issues/176) [#178](https://git.nostrdev.com/sigit/sigit.io/issues/178)
- **pdf:** reuse content width function ([59c3fc6](https://git.nostrdev.com/sigit/sigit.io/commit/59c3fc69a255c54475daa6a2efad3ae8a4b3efd8))
- **pdf:** scaling and font styles consistency ([ac3186a](https://git.nostrdev.com/sigit/sigit.io/commit/ac3186a02ed441c6efc31aaf462a5b8b229f5fa1)), closes [#146](https://git.nostrdev.com/sigit/sigit.io/issues/146)
- **pdf:** scaling on resize, add avatars to counterpart select ([4712031](https://git.nostrdev.com/sigit/sigit.io/commit/47120316152a7a3157f7cd089cb2184053ae25ec))
- **pdf:** use minified version of pdf ([a3effd8](https://git.nostrdev.com/sigit/sigit.io/commit/a3effd878b25da50f0575f10dd094efae0b03ec7))
- placeholder avatar is incosistent across components ([d15943f](https://git.nostrdev.com/sigit/sigit.io/commit/d15943f61bdac9573d528795ff43903459c0da3b))
- popup forms designs ([e3ca3ab](https://git.nostrdev.com/sigit/sigit.io/commit/e3ca3ab9088e9a45d6d01c35811450c8c3f762dc))
- processing events ([25764c7](https://git.nostrdev.com/sigit/sigit.io/commit/25764c7ab41708f03e4c671857be519020bee46f))
- processing gift wraps and notifications ([#193](https://git.nostrdev.com/sigit/sigit.io/issues/193)) ([235e76b](https://git.nostrdev.com/sigit/sigit.io/commit/235e76be4e3eada7668bf802ff063394d958ff17))
- profile image scale ([58c457b](https://git.nostrdev.com/sigit/sigit.io/commit/58c457b62c67201fb83c845f9fa5a81b87dfdc65))
- profile page styling ([67e5c19](https://git.nostrdev.com/sigit/sigit.io/commit/67e5c19870bdd66ac1fc2b4dd3231a2fb77831a7))
- profile picture inconsistencies, login with enter ([5f8e8fd](https://git.nostrdev.com/sigit/sigit.io/commit/5f8e8fd6f4eb6bf74c04abb51c2af545eda2be45))
- push all files take 2 ([24916c5](https://git.nostrdev.com/sigit/sigit.io/commit/24916c58068bbe9e5dfc76cbf00c955add6744e4))
- reduce mui usage, implement design updates ([9189ff3](https://git.nostrdev.com/sigit/sigit.io/commit/9189ff33bc714c795f8acedd85995c0152882a35))
- redundant updates ([2d0212f](https://git.nostrdev.com/sigit/sigit.io/commit/2d0212fd6c683fb984eee55e1f815c2c6bbecae2))
- **relay-controller:** sigit relay immutability and relay list ([e0d6c03](https://git.nostrdev.com/sigit/sigit.io/commit/e0d6c0363951c66bdede17759ae712626a4a07a5))
- **relays:** allow adding ws:// ([04f1d69](https://git.nostrdev.com/sigit/sigit.io/commit/04f1d692a44123331129ee92443c92f9254403f4)), closes [#297](https://git.nostrdev.com/sigit/sigit.io/issues/297)
- **relays:** relay add button size height ([5f3d92d](https://git.nostrdev.com/sigit/sigit.io/commit/5f3d92d62f1f958f3e93ed4c9cd879c88a5c5d6c)), closes [#244](https://git.nostrdev.com/sigit/sigit.io/issues/244)
- removal of create nostr auth token ([60a7140](https://git.nostrdev.com/sigit/sigit.io/commit/60a7140c6a662c225c63108707ac9dd67990f88a))
- remove both from UserRole enum ([b527339](https://git.nostrdev.com/sigit/sigit.io/commit/b5273393e6edd56fa2e9c6b9a305e56e4555020c))
- remove duplicate states and fix default signer ([e05d3e5](https://git.nostrdev.com/sigit/sigit.io/commit/e05d3e53a2b663973adf2e02296b3a5bbfa8ee78))
- remove nostr image for placeholder avatar, use robohash instead ([4f4f7fb](https://git.nostrdev.com/sigit/sigit.io/commit/4f4f7fb5c1ed21747b4b3bea5771674cf8d159a5))
- remove placeholder used for text ([d0a6297](https://git.nostrdev.com/sigit/sigit.io/commit/d0a6297ccec0b61066cc2ca80e44dc6017581116))
- remove screen on nostr-login launch ([8689c7f](https://git.nostrdev.com/sigit/sigit.io/commit/8689c7f753fc0d5ab5e88bdf3d376da2c60148c0))
- remove unstable fetch events loop ([5f0234a](https://git.nostrdev.com/sigit/sigit.io/commit/5f0234a358788226b6bb0e71f95de8c70ef4bc3f))
- removed redundant variable ([2455856](https://git.nostrdev.com/sigit/sigit.io/commit/245585662a094da385702438bcdd72387cd5267f))
- removed viewer/signer button ([2f9017b](https://git.nostrdev.com/sigit/sigit.io/commit/2f9017b8403f2d42e773e27928b259830d123ecf))
- removes retrier and updates notification ([3d5006a](https://git.nostrdev.com/sigit/sigit.io/commit/3d5006a7154ee9be6bb165f8cafc1bcd59c20e26))
- removes unneeded notification ([b7bd922](https://git.nostrdev.com/sigit/sigit.io/commit/b7bd922af35d95a2d78a1b168ab1ec4c4d66a9ee))
- removing file upload, avatar by robohash ([8e76202](https://git.nostrdev.com/sigit/sigit.io/commit/8e7620201ea4252bc9b025f4d0b24930795c016a))
- replace sign with upload in homepage ([021db56](https://git.nostrdev.com/sigit/sigit.io/commit/021db5679a54ec4b6f44fbe86bb00cfa3125be82))
- return immediately from publish event when published to at least one relay and keep publishing to other in background ([7df6ab8](https://git.nostrdev.com/sigit/sigit.io/commit/7df6ab8c8495443ccc83dca937ee26cefb158441))
- reverting signing of nostr auth token ([38913e7](https://git.nostrdev.com/sigit/sigit.io/commit/38913e770de8b1900a58d21354963dadf6180d57))
- review suggestion ([15d4d0a](https://git.nostrdev.com/sigit/sigit.io/commit/15d4d0a75276ec20394d2ef5e68e737c8f412cee))
- **review:** remove inline styles ([b8811d7](https://git.nostrdev.com/sigit/sigit.io/commit/b8811d730a781c285ee6fc83a5faa6230aa45c3a))
- robohash image missing with NIP05 login ([9baf0ec](https://git.nostrdev.com/sigit/sigit.io/commit/9baf0ecabae1b32a74b29c7dd65bf6ffd67525e4))
- routing, removed useEffect ([8e71592](https://git.nostrdev.com/sigit/sigit.io/commit/8e71592d8815471bde6fb74230ba9742308daad8))
- search bar scaling ([272fcf9](https://git.nostrdev.com/sigit/sigit.io/commit/272fcf93c64005b06249690815bb320ad66b9798))
- search counterparts nip05 does not need to include '@' ([7b29d70](https://git.nostrdev.com/sigit/sigit.io/commit/7b29d7055eeb6bf7f9bbcc06fb2ecf0962157046))
- selected mark selection ([0d52cd7](https://git.nostrdev.com/sigit/sigit.io/commit/0d52cd71134c9b3eee41e1ea853dba65afc7c79b))
- show error if decrypt fails ([cc382f0](https://git.nostrdev.com/sigit/sigit.io/commit/cc382f072643918e83c399374bfe5ff77af794e4))
- show extension box for non-mark files, de-dupe css and code ([05a2dba](https://git.nostrdev.com/sigit/sigit.io/commit/05a2dba164f015098cafb29d143b308d8db7dc8a)), closes [#138](https://git.nostrdev.com/sigit/sigit.io/issues/138)
- show import/export only for local ([67d545d](https://git.nostrdev.com/sigit/sigit.io/commit/67d545de2fec2898ec12a4433ec9d722248a4b83))
- sigit links and outline ([21caaa7](https://git.nostrdev.com/sigit/sigit.io/commit/21caaa7009e49cd7cedc54104ab9438c330ed708))
- sigit's wrapper zip should contain keys.json file ([ded8304](https://git.nostrdev.com/sigit/sigit.io/commit/ded8304c669c257b5b782829ffb37be101af9cdd))
- **sigit:** add to submittedBy avatar badge for verified sigit creation ([b2c3cf2](https://git.nostrdev.com/sigit/sigit.io/commit/b2c3cf2aca05a8e232a53ac087929f3eb797e23d))
- **sigit:** excel extension typo, more excel types ([6b5a8a7](https://git.nostrdev.com/sigit/sigit.io/commit/6b5a8a7375d528ce4f8e53dd595e1bbde27ea433))
- sign buttons styles ([8c97476](https://git.nostrdev.com/sigit/sigit.io/commit/8c974768a81db75a0bd94e20db6a495126207f5b))
- **sign:** allow signing without marks, hide loading and show toast for prevSig error ([20d1170](https://git.nostrdev.com/sigit/sigit.io/commit/20d1170f7dd41832b83b34656a9f95e239f074cf))
- **sign:** allow signing without selectedMark - no currentUserMarks ([92f23ba](https://git.nostrdev.com/sigit/sigit.io/commit/92f23bab91225d888c101c3670868eae132114e9))
- **sign:** allow sumit without selectedMark ([cb0d2dd](https://git.nostrdev.com/sigit/sigit.io/commit/cb0d2dd7bc98a7e16c3d2e53fba4bd86a0c8a89d))
- **sign:** always show PdfView ([8df5084](https://git.nostrdev.com/sigit/sigit.io/commit/8df5084703baf2b7ae416af8d8d5064cec13ee19))
- **signature:** force re-render on value change ([a1c3087](https://git.nostrdev.com/sigit/sigit.io/commit/a1c308727f2786b48cb083bf0544a358ab211c2c))
- signing order ([ec305c4](https://git.nostrdev.com/sigit/sigit.io/commit/ec305c417bcca6d70a24cdae14c1b99be40b0064))
- simplify events, more ts and clean up ([6641cf2](https://git.nostrdev.com/sigit/sigit.io/commit/6641cf2ee703c4c973c69ecde864030fb2e91596))
- some linter warnings and an error ([f51afe3](https://git.nostrdev.com/sigit/sigit.io/commit/f51afe3b677d418cdf4c4d29132f63f9ff1bd56b))
- **spinner:** remove dummy desc and use variants ([d1b9eb5](https://git.nostrdev.com/sigit/sigit.io/commit/d1b9eb55d8b41c43b600b1e0f3432a7030dbec91))
- styles fixed in homepage ([6553ed8](https://git.nostrdev.com/sigit/sigit.io/commit/6553ed89e08b7d9279935b63db99d9571e5391d5))
- styling ([2f29ea9](https://git.nostrdev.com/sigit/sigit.io/commit/2f29ea9f35ad1c3285a9c01e7e51dfc37942c02f))
- styling ([d41d577](https://git.nostrdev.com/sigit/sigit.io/commit/d41d577c29af2135e4352186af1b4b434d22cc95))
- styling ([e681513](https://git.nostrdev.com/sigit/sigit.io/commit/e681513785bd0df6d16c0b34405ed834ca39740c))
- styling ([551a3f8](https://git.nostrdev.com/sigit/sigit.io/commit/551a3f8509ae78f921d66370bcd653dd3f6dc226))
- styling ([12fe476](https://git.nostrdev.com/sigit/sigit.io/commit/12fe476e97e2d907fcb41baa87bd8e1ca74f1b80))
- svg attributes ([3a93622](https://git.nostrdev.com/sigit/sigit.io/commit/3a9362296674374b6c4b79e1260b0c043a5ea52b))
- **tabs:** add tab icons ([2be7f3d](https://git.nostrdev.com/sigit/sigit.io/commit/2be7f3d51bfac0d58984229b04f97b5df8dbebc6))
- take 3 all files ([02f250c](https://git.nostrdev.com/sigit/sigit.io/commit/02f250c76eb6d1b7fb410394c88397e37dd527e4))
- take 4 (all files) ([abf9c3e](https://git.nostrdev.com/sigit/sigit.io/commit/abf9c3e4fd7b61d5f8794714484cb8a0c542d6e7))
- take 5 files ([ea3f618](https://git.nostrdev.com/sigit/sigit.io/commit/ea3f61897c7ab8601547e3b61a6bdd833a19ad12))
- take 6 ([400d192](https://git.nostrdev.com/sigit/sigit.io/commit/400d192fb0441bbe772d44a457f4e96a4d42d11f))
- take 7 ([3f944bd](https://git.nostrdev.com/sigit/sigit.io/commit/3f944bdf73103e6a0152c32bc788d363c323f42b))
- title text align ([c5b1a9b](https://git.nostrdev.com/sigit/sigit.io/commit/c5b1a9b3804c1a6003ba3d22afdb1b76b3d9db48))
- toggle ([3549b6e](https://git.nostrdev.com/sigit/sigit.io/commit/3549b6e54292b3d6fe456025cefc397ba0aa070d))
- top level container wrapper for other pages ([53b7b05](https://git.nostrdev.com/sigit/sigit.io/commit/53b7b05ac5ed75be25d84e5bb0a0a851ac04112d))
- typo ([6c5ed3a](https://git.nostrdev.com/sigit/sigit.io/commit/6c5ed3a69c7c025a507cf87f77bd408a3eee3de1))
- unzip and use timeout util ([8b00ef5](https://git.nostrdev.com/sigit/sigit.io/commit/8b00ef538b164b1116095fd5ffee95a5791667e5))
- update buttons and button icon design ([28184ab](https://git.nostrdev.com/sigit/sigit.io/commit/28184ab03864627138852b720e638ba56de5e1b5))
- update design buttons ([5d59ffc](https://git.nostrdev.com/sigit/sigit.io/commit/5d59ffce28e1248c15d04730794606fbf4b2e1dd))
- update DM wording ([de00b9b](https://git.nostrdev.com/sigit/sigit.io/commit/de00b9b5a70bdafa2c10c35ba8956fffef1802d3))
- update footer design ([af689a0](https://git.nostrdev.com/sigit/sigit.io/commit/af689a00f7848ce1a61c6a19eaa0e76a8e417d5c))
- update logo and favicon ([017d1ab](https://git.nostrdev.com/sigit/sigit.io/commit/017d1ab88b3aa8b99c96641a9f5b04745fa31259))
- update nsecBunker delegated key after logout ([962b2bc](https://git.nostrdev.com/sigit/sigit.io/commit/962b2bcea676ff247218196e7649ed17141b64b5))
- update online and offline flows ([e8da0dc](https://git.nostrdev.com/sigit/sigit.io/commit/e8da0dc76f37f9b8cc8033ce6480b9d806cec718))
- update popup design ([55158fc](https://git.nostrdev.com/sigit/sigit.io/commit/55158fc313e9a31055210e061ca136c106cdf03a))
- update the logic for login with nsecbunker ([7c3c061](https://git.nostrdev.com/sigit/sigit.io/commit/7c3c061b88029f7643f471f2cf2279cc115e0719))
- update the url in DM to contain fileUrl and encryption key ([9fa3df3](https://git.nostrdev.com/sigit/sigit.io/commit/9fa3df3850935b4798489e9980de4520f00c2b20))
- update user placeholder for create ([e7b0bbe](https://git.nostrdev.com/sigit/sigit.io/commit/e7b0bbe23c71421d9725b242cb62ed11e40ffdad))
- update verify to use file signature check ([18637bb](https://git.nostrdev.com/sigit/sigit.io/commit/18637bbbc193f970c03c9b19d522fff29390273f))
- updated latest version of nostr-login which includes outboxRelays option ([6f6ed3c](https://git.nostrdev.com/sigit/sigit.io/commit/6f6ed3c39f959d287f93ed1bd111d296b3e3fdf5))
- updates blossom authorisation event ([dd53ded](https://git.nostrdev.com/sigit/sigit.io/commit/dd53ded5186abf692a4b554a39a21f445672a5d4))
- updating title on homepage ([481ef6c](https://git.nostrdev.com/sigit/sigit.io/commit/481ef6cdc21ed89280e4ec748fbd0d8866180324))
- url ([79ef9eb](https://git.nostrdev.com/sigit/sigit.io/commit/79ef9eb8d6ced4cbb0517def4b7864176c78b1f4))
- url encode the DM link payload ([38def3b](https://git.nostrdev.com/sigit/sigit.io/commit/38def3bda5381259047f57065cad10b84a395d53))
- use correct key for signer status, update signer badge icons ([3743a30](https://git.nostrdev.com/sigit/sigit.io/commit/3743a30ef62084c6c3a8cfdfcb63d1f08f0162ed))
- use dedicated key from nip78 in auth event for uploading files.zip ([8eaf9cb](https://git.nostrdev.com/sigit/sigit.io/commit/8eaf9cb61cc6de9d60f40c110565f3d560f51229))
- use default relayMap if its undefined in redux store ([d7b5ea9](https://git.nostrdev.com/sigit/sigit.io/commit/d7b5ea9b9ead53193204af99374ced2465a83e4e))
- use hash router instead of browser router ([3d980ca](https://git.nostrdev.com/sigit/sigit.io/commit/3d980ca2e7a4afb2c37fce8f1ccef9e192af980d))
- use iframe for nsecbunker auth ([c99a2a8](https://git.nostrdev.com/sigit/sigit.io/commit/c99a2a81c265f601e2af00890132818d905d6133))
- use kind 0 event for nostr joining block ([9bb62cf](https://git.nostrdev.com/sigit/sigit.io/commit/9bb62cf96676aa5bca5882260f037ce00c5ee74f))
- use kind 27235 in place of kind 1 wherever possible ([9073419](https://git.nostrdev.com/sigit/sigit.io/commit/90734196e5f14b4c988b3ab36c1ab2a968e2842a))
- use old approach of using sha256 for generating d tag ([49c1714](https://git.nostrdev.com/sigit/sigit.io/commit/49c17149621670b7d44184762ded530fd66efbc1))
- use relays from nip65 for broadcasting DMs ([349e26b](https://git.nostrdev.com/sigit/sigit.io/commit/349e26b62888e72d7987ee6c1baab761b998ed22))
- userRobotImage reducer type fix ([ccc31c5](https://git.nostrdev.com/sigit/sigit.io/commit/ccc31c51c99945b5f0634bbe2ce11851d86ae367))
- useSigitProfile dep ([329fd3d](https://git.nostrdev.com/sigit/sigit.io/commit/329fd3d27beeb4ae08558887b731b9657de90237))
- verify page robohash ([5e114f7](https://git.nostrdev.com/sigit/sigit.io/commit/5e114f7fb86b4205aefd6125fe5fc2348d3cfb0b))
- **verify-page:** add mark styling ([423b6b6](https://git.nostrdev.com/sigit/sigit.io/commit/423b6b6792feca3ff5891bfc1d9f2181d8a00195))
- **verify-page:** export (download) files now includes files ([7278485](https://git.nostrdev.com/sigit/sigit.io/commit/7278485b76c6e4a6319d1f3a7941d4985dc93cad))
- **verify-page:** map item keys ([58f70db](https://git.nostrdev.com/sigit/sigit.io/commit/58f70db7f61a8a963289831d25b7ea877798a593))
- **verify-page:** parse and show mark values ([f88e2ad](https://git.nostrdev.com/sigit/sigit.io/commit/f88e2ad6804424755dacfc8c89da4fb8c2b90fcc))
- **verify-page:** remove mark border in production, enable dev flag for css classes ([c3a3915](https://git.nostrdev.com/sigit/sigit.io/commit/c3a39157ffdb217dc9e7fa16ff600386a1f95307))
- verify/sign link ([e48a396](https://git.nostrdev.com/sigit/sigit.io/commit/e48a3969904c1ec9759a60e9b21f998569ce6b13))
- **verify:** offline flow ([759a40a](https://git.nostrdev.com/sigit/sigit.io/commit/759a40a4f910d81fa95bc4b7304bc6a1eb6b8eda))
- when decrypting file, have better error messages ([5d6a358](https://git.nostrdev.com/sigit/sigit.io/commit/5d6a3580a6b3c97afc7a1f015458fb0a51101f52))
- when opening a sigit after user signed it, asks user to sign again instead of redirecting to /verify ([ccb4036](https://git.nostrdev.com/sigit/sigit.io/commit/ccb40360292af4bf10f24fc935c9ca869729ec8f))
- wording of adding counterparties ([33d58a2](https://git.nostrdev.com/sigit/sigit.io/commit/33d58a2166479f49e2596313a3359cd0204fadf5))
- works offline card icon ([baa1a7b](https://git.nostrdev.com/sigit/sigit.io/commit/baa1a7b040c7bdd2af6c01f2b45178868095e5d3))
### Code Refactoring
- use signature pad, smoother signature line ([7c7a222](https://git.nostrdev.com/sigit/sigit.io/commit/7c7a222d4fac7d119270f3d6b79b75f6d60032ff))
### Features
- ability to change the order of signers in create screen ([8deaae8](https://git.nostrdev.com/sigit/sigit.io/commit/8deaae80de8afe62e65248e91a94ffb378cc3952))
- add background images ([e9a1b98](https://git.nostrdev.com/sigit/sigit.io/commit/e9a1b9894c8c609289554ddb937ef0522a12bde3))
- add banner and styling ([5f39b55](https://git.nostrdev.com/sigit/sigit.io/commit/5f39b55f6860f2ee73c1d72e384915fb85ed4336))
- add cache setting page ([278d965](https://git.nostrdev.com/sigit/sigit.io/commit/278d9655f6ca587071451bdedc013fd1d2648395))
- add children support to routes arrays ([0b35f11](https://git.nostrdev.com/sigit/sigit.io/commit/0b35f11abf251b5f45bc7f7926275692cde69048))
- add color border to user's profile picture based on first 6 character of user's hexkey ([89850f8](https://git.nostrdev.com/sigit/sigit.io/commit/89850f881d6afbe9f67e1a07510f6978acfca0ac))
- add custom Container component for layouts ([e54eced](https://git.nostrdev.com/sigit/sigit.io/commit/e54eced800305c1e724b846c9fbff4cf97f77414))
- add dropzone and multiple files support ([83ddc1b](https://git.nostrdev.com/sigit/sigit.io/commit/83ddc1bbc810a9f0d20dbf381cca5404cb7eb4c5))
- add exportedBy to useSigitMeta ([13254fb](https://git.nostrdev.com/sigit/sigit.io/commit/13254fbe0641796eb40425d0910db2d9fc43645d))
- add MarkConfig and components ([dfa2832](https://git.nostrdev.com/sigit/sigit.io/commit/dfa2832e8d757842b848101323a4de03cc74f0a1))
- add minimal styling secondary button ([9a1d3d9](https://git.nostrdev.com/sigit/sigit.io/commit/9a1d3d98bf866b97e9d9748cdad9e9159b4ef7d9))
- add modal with login, register, nostr routes ([868ae6f](https://git.nostrdev.com/sigit/sigit.io/commit/868ae6f23e68bf3e0b4503caa74b822b68219438))
- add nostrLoginAuthMethod to state ([110621a](https://git.nostrdev.com/sigit/sigit.io/commit/110621a125230f56e13e430b732dd67730b45fff))
- add prev signer's signature in the content of next signer's signed event ([7947abf](https://git.nostrdev.com/sigit/sigit.io/commit/7947abf0f963fe2a1b926852527acc52e28eaacb))
- Add Sigit ID as a path param ([75a715d](https://git.nostrdev.com/sigit/sigit.io/commit/75a715d002f005e327442015dc322b278f59bc8e))
- Add Sigit ID as a Path Param to /verify ([0008e98](https://git.nostrdev.com/sigit/sigit.io/commit/0008e9814681de43068235f61c7bd88e7a1f3510))
- add simple spinner wrapper ([01ca81b](https://git.nostrdev.com/sigit/sigit.io/commit/01ca81be2a431e8242cbae50f2e58115fc17a335))
- add squiggle support ([de44370](https://git.nostrdev.com/sigit/sigit.io/commit/de44370a96e94846a2e0b47ca79599cdb127226a))
- add sticky layout with slots ([dfe67b9](https://git.nostrdev.com/sigit/sigit.io/commit/dfe67b99ad7d80b1ab7c3d41bd5f5281d1fc1f5e))
- add sticky layout with slots ([e16b8cf](https://git.nostrdev.com/sigit/sigit.io/commit/e16b8cfe3fe297983a3fd122ac25b89ca1568835))
- add SVGO, enable signature ([9286e43](https://git.nostrdev.com/sigit/sigit.io/commit/9286e4304f52a3eaf2a87b6d2b9ebecc32d398ba))
- add the ability to create and sign while user is offline ([c3c9bf7](https://git.nostrdev.com/sigit/sigit.io/commit/c3c9bf772d5e10ba6bf55d39e8f21ba261828b60))
- add uploaded image file as preview ([ae08b07](https://git.nostrdev.com/sigit/sigit.io/commit/ae08b07d7404bb8a9988a700ebce72d777e2d725))
- add UserAvatar, UserIconButton ([20bb05d](https://git.nostrdev.com/sigit/sigit.io/commit/20bb05ddc61e5126e46a1e4943619d67d7f4cc27)), closes [#68](https://git.nostrdev.com/sigit/sigit.io/issues/68)
- add verify link in landing page ([8884389](https://git.nostrdev.com/sigit/sigit.io/commit/8884389c6ad59cf695d7f03e67cc37095abaee55))
- add verify page ([5c14402](https://git.nostrdev.com/sigit/sigit.io/commit/5c1440244cbd3e6d9b80e557dc1a511c1a067871))
- added a local cache based on browsers built in indexDB ([5b1147d](https://git.nostrdev.com/sigit/sigit.io/commit/5b1147da5db4f04eb622633ad5397d6a6c8056b0))
- added a setting page ([e82023f](https://git.nostrdev.com/sigit/sigit.io/commit/e82023f105117ead6ab623cb4915c96edc7cbac7))
- added hashes.json in zip ([d879c7d](https://git.nostrdev.com/sigit/sigit.io/commit/d879c7d45a0d4c6356008a6572277c3e443ce806))
- added ndkContext and used it in relays page ([3c061d5](https://git.nostrdev.com/sigit/sigit.io/commit/3c061d5920e2d518b6a837a61e151cc1586b88b7))
- added nsecbunker setting page ([b2a8cff](https://git.nostrdev.com/sigit/sigit.io/commit/b2a8cff907161511f944a02d829b4230007360fa))
- added profile banner ([6eedfb8](https://git.nostrdev.com/sigit/sigit.io/commit/6eedfb8f3fe0785a98ed36408061c9ed7ab9645a))
- added profile view ([5d0076d](https://git.nostrdev.com/sigit/sigit.io/commit/5d0076dd62f0055d0280186182fdb0d3a409b6af))
- added the ability to login with nsecbunker connection string ([4973721](https://git.nostrdev.com/sigit/sigit.io/commit/497372160843f11d0b206fa490992d257004d773))
- added the ability to re-broadcast sigit ([5db4d1b](https://git.nostrdev.com/sigit/sigit.io/commit/5db4d1b4291f37986d517f22b021cf80fffb10c7))
- allow the user to login via nsecbunker using only domain part ([3efa557](https://git.nostrdev.com/sigit/sigit.io/commit/3efa557976f72e6b898876ce0c1c3736f620f71b))
- **auth:** nsec login with url params ([995c7ce](https://git.nostrdev.com/sigit/sigit.io/commit/995c7ce293474ce098900a057d6ae442c90df71c))
- changed MIME type of the uploaded file to sigit ([4e7f9d6](https://git.nostrdev.com/sigit/sigit.io/commit/4e7f9d650ed77db5c2ff7388a140fed790e2d784))
- **ci:** add git hooks ([70f625f](https://git.nostrdev.com/sigit/sigit.io/commit/70f625ffd128f132cc3f92a4465b7e4d73a9ed97))
- **ci:** add lint-staged in pre-commit ([84d1379](https://git.nostrdev.com/sigit/sigit.io/commit/84d13793ffd5fab5c2f0148fb7825c21f23431a0))
- **ci:** add open pr workflow ([5290dda](https://git.nostrdev.com/sigit/sigit.io/commit/5290dda52a76093b0e99ecbfc340fa6fae99f728))
- configured semantic releases ([c0b9039](https://git.nostrdev.com/sigit/sigit.io/commit/c0b903929d478ce3eb2c7636bf9ad0da5b32534d))
- **content:** show other file types as gray box ([c9d7d0a](https://git.nostrdev.com/sigit/sigit.io/commit/c9d7d0a6f58708db866b9099c49800ce48930c65))
- convert hexkeys to npub in meta.json ([ee2f0cb](https://git.nostrdev.com/sigit/sigit.io/commit/ee2f0cbc970cdcb6d491f0689735b59a31c825ec))
- create page search users ([4af28ab](https://git.nostrdev.com/sigit/sigit.io/commit/4af28abcb666351a348f3375f4eb1d21d18fbf65))
- create signing request and send a DM to first signer with zip file url and encryption key ([bd1e841](https://git.nostrdev.com/sigit/sigit.io/commit/bd1e8417c17ea559d1a3f09e85e5f0e96b5dd1f4))
- **create-page:** intial layout and page styling ([86c8cc0](https://git.nostrdev.com/sigit/sigit.io/commit/86c8cc00fd9a019690c9f700c2496459ea1d3a54))
- **create:** add counterpart component for drawing field ([4131eb5](https://git.nostrdev.com/sigit/sigit.io/commit/4131eb5de1e139a5c0db35fb0128c2562279dc50))
- **create:** add Image and File items ([889d6a0](https://git.nostrdev.com/sigit/sigit.io/commit/889d6a0f440bbec8d9ad6362324693dbb4c5511e))
- **create:** touch support for dnd ([3e07575](https://git.nostrdev.com/sigit/sigit.io/commit/3e075754e5ec8b858f2e5a658a6137d4be188380))
- custom select component ([8d16831](https://git.nostrdev.com/sigit/sigit.io/commit/8d168314de807bfb7b5d96ddc0cf82109afdf343))
- **dashboard:** add sigits filtering, sorting, searching ([becd021](https://git.nostrdev.com/sigit/sigit.io/commit/becd02153c9cecb45041ab7e0b05b8a8cfbcb08a))
- **export:** add icons and make encrypted be first/top option ([99d562a](https://git.nostrdev.com/sigit/sigit.io/commit/99d562a3edb62b09664091946a59e88020460d39))
- extension icon label util component ([c3f60b1](https://git.nostrdev.com/sigit/sigit.io/commit/c3f60b1e643ff2e9ccf8f483a971b1970cf7d786))
- handle root \_@ users on add counterpart ([897daaa](https://git.nostrdev.com/sigit/sigit.io/commit/897daaa1fa57a587b5562fd9c94526dd22485b65))
- **home:** add search param to address bar and sync the state with navigation ([93b2477](https://git.nostrdev.com/sigit/sigit.io/commit/93b2477839900598195bbb6ab28c82493a8abc98))
- implemented profile page ([c0547b2](https://git.nostrdev.com/sigit/sigit.io/commit/c0547b2a1f05e02dea247822672700a5d15f79e7))
- implemented relay controller and use that for fetching and publishing events ([a775d7b](https://git.nostrdev.com/sigit/sigit.io/commit/a775d7b265594d575f106898585b8f1dcebbce6f))
- implemented the UI and logic for signing document ([a32abaf](https://git.nostrdev.com/sigit/sigit.io/commit/a32abaf9e703d481b3b8fc45739b312e907979a9))
- improve design for homepage ([de4d927](https://git.nostrdev.com/sigit/sigit.io/commit/de4d927c73dc50d2b2ce85af232c9bcd7b98d091))
- improve verification process ([6611a85](https://git.nostrdev.com/sigit/sigit.io/commit/6611a855d9342a028cbe5e4a88cac058ca18a62a))
- in offline mode navigate creator to sign screen after creation when creator is first signer ([1f7980e](https://git.nostrdev.com/sigit/sigit.io/commit/1f7980e2ca285117a8971a4d5d94ea5b56d15ff7))
- In sign page navigate to verify after export ([8f463b3](https://git.nostrdev.com/sigit/sigit.io/commit/8f463b36c08761a4a8e4ff9b67068be90eef8ea6))
- include the original files always ([db9cf9d](https://git.nostrdev.com/sigit/sigit.io/commit/db9cf9d20cf78cae85ba431eafea2365337f1b1e))
- landing page - larger cta button ([3149ba9](https://git.nostrdev.com/sigit/sigit.io/commit/3149ba975777c557bae46b86e77f20392b9ebaec))
- landing page - responsive cards ([87e4536](https://git.nostrdev.com/sigit/sigit.io/commit/87e4536713795765e9300e72b3262395b07e76b3))
- landing page implementation and styling ([0a61ae5](https://git.nostrdev.com/sigit/sigit.io/commit/0a61ae5f6455d76c1f7f925abce9dfeb94fbc1fa))
- **loading-spinner:** add children support for default variant ([4d1e672](https://git.nostrdev.com/sigit/sigit.io/commit/4d1e6722681849c72d8ad5cfaebc543ab61dd907))
- logo and favicon ([a36ed8e](https://git.nostrdev.com/sigit/sigit.io/commit/a36ed8eab0059876c15c10b7e60bd6103a3ddd4b))
- maintain logged in sesssion ([2ed092b](https://git.nostrdev.com/sigit/sigit.io/commit/2ed092bcbd7ecffe38d1fa9704ed847b53a99b41))
- make block number link that will refernce to the event ([37bc205](https://git.nostrdev.com/sigit/sigit.io/commit/37bc205ce4e8ea8eb1cbcd5f1a57703501bd7c52))
- make verify page public and add verify option in user menu list ([12ca854](https://git.nostrdev.com/sigit/sigit.io/commit/12ca854c4852f28367a51cb35f9dec6e2e5ff025))
- **meta:** add error handling for meta.json blossom operations ([7007492](https://git.nostrdev.com/sigit/sigit.io/commit/7007492a0d1e9d21f505a300aa6b2ca24cf0b585))
- **meta:** send notifications with blossom instead of meta.json ([3d1bdec](https://git.nostrdev.com/sigit/sigit.io/commit/3d1bdece4d881f347e974506af9d01d9be01f4f7))
- **mobile:** tabs and scrolling ([d9be051](https://git.nostrdev.com/sigit/sigit.io/commit/d9be05165fad1fe07f51dbccc47b0fe2e675ee86))
- navigate to different pages based on uploaded file ([92b62a3](https://git.nostrdev.com/sigit/sigit.io/commit/92b62a3cbed8461cbbb25cb841bec9063f11e90d))
- nostr.json ([bb37a27](https://git.nostrdev.com/sigit/sigit.io/commit/bb37a27321cd9b26537b8fbbe2b39902b6c85fc4))
- **offline:** add decrypt as zip util ([8b5abe0](https://git.nostrdev.com/sigit/sigit.io/commit/8b5abe02e2b9d3f3101afa014c4fa4655ed4b099))
- **offline:** add signer service util class ([bcd5713](https://git.nostrdev.com/sigit/sigit.io/commit/bcd57138caeb03b03f5b9a4df403534d076a4a15))
- **offline:** split online and offline flow with dedicated buttons, remove export in sign, all counterparties can decrypt ([3f01ab8](https://git.nostrdev.com/sigit/sigit.io/commit/3f01ab8fcaf7aa94460215418315f56190e4f4b0))
- **opentimestamps:** adds OTS library and retrier function ([edfe9a2](https://git.nostrdev.com/sigit/sigit.io/commit/edfe9a2954b1222716f9cd43516b6a041de8bb1b))
- **opentimestamps:** adds timestamps to create flow ([85bcfac](https://git.nostrdev.com/sigit/sigit.io/commit/85bcfac2e0aa8bffaf258c89b5c73b35f108f38b))
- **opentimestamps:** amends to flow to only upgrade users timestamps ([f12aaf1](https://git.nostrdev.com/sigit/sigit.io/commit/f12aaf1c2bfa4c8d3ee26e9c4c14ae5759551381))
- **opentimestamps:** refactors to timestamp the nostr event id ([07f1a15](https://git.nostrdev.com/sigit/sigit.io/commit/07f1a15aa1775a857b526a8d343b8f1708535ed0))
- **opentimestamps:** update the full flow ([21aa25a](https://git.nostrdev.com/sigit/sigit.io/commit/21aa25a42a2797d84f20be576d016f2075c09fa0))
- **opentimestamps:** updates data model ([85bf907](https://git.nostrdev.com/sigit/sigit.io/commit/85bf907f54a9e81758e66e30bfd72f3aa79fd06a))
- **opentimestamps:** updates data model and useSigitMeta hook ([edbe708](https://git.nostrdev.com/sigit/sigit.io/commit/edbe708b65e342033c2e66bce21ac3d1622088c3))
- **opentimestamps:** updates opentimestamps type ([b92790c](https://git.nostrdev.com/sigit/sigit.io/commit/b92790ceede513f6aaa6c0d04bf31ab70550d507))
- **opentimestamps:** updates signing flow ([7f00f9e](https://git.nostrdev.com/sigit/sigit.io/commit/7f00f9e8bf8b15e811361aad2a2d75941ba056eb))
- **opentimestamps:** updates the flow and adds notifications ([2b630c9](https://git.nostrdev.com/sigit/sigit.io/commit/2b630c94b639368d5d4789f38c747c190dfec547))
- **opentimestamps:** updates tooltip ([19b815e](https://git.nostrdev.com/sigit/sigit.io/commit/19b815e52819b3e79c41e6f8d5c076bb6d7fd6b6))
- **opentimestamps:** updates utils and adds comments ([a2138f1](https://git.nostrdev.com/sigit/sigit.io/commit/a2138f1de18f7085349699a885576e00100989df))
- **PDF Management:** added pdf pages preview with fields list ([e715f6a](https://git.nostrdev.com/sigit/sigit.io/commit/e715f6ae6f8da06027edad85eb806ab7b806d33a))
- **pdf markings:** added drawing component, parsing pdfs and displaying in the UI ([8576034](https://git.nostrdev.com/sigit/sigit.io/commit/8576034829563d1116f14ca7f0928c55adbabf3c))
- **pdf-fields:** add logic to hide signers on ESC ([e37f90d](https://git.nostrdev.com/sigit/sigit.io/commit/e37f90d6db713434912530988a80af3390ed8a92))
- **pdf-marking:** add pdf-view components ([b58ba62](https://git.nostrdev.com/sigit/sigit.io/commit/b58ba625f9087c74e81217f3418201673654524f))
- **pdf-marking:** adds file downloading functionality ([6d881cc](https://git.nostrdev.com/sigit/sigit.io/commit/6d881ccb45e440c343b768c6d26d6933b2a4b813))
- **pdf-marking:** adds file validity check ([eca31ce](https://git.nostrdev.com/sigit/sigit.io/commit/eca31cea4f68730ab5d70428902a04514d268764))
- **pdf-marking:** adds file validity check ([ed7acd6](https://git.nostrdev.com/sigit/sigit.io/commit/ed7acd6cb4c73ee2907fb5062a10dbb8d369f7c9))
- **pdf-marking:** binds text to marks and saves with signatures ([4a932ff](https://git.nostrdev.com/sigit/sigit.io/commit/4a932ffe03cd4adad33abfdc7355335e4501038f))
- **pdf-marking:** implements png to pdf conversion and ability to download full sigits after signing ([cb9a443](https://git.nostrdev.com/sigit/sigit.io/commit/cb9a443fb18d5c562fe73400bb7aeedb4abf3f7e))
- **pdf-marking:** integrates layouts ([64dbd7d](https://git.nostrdev.com/sigit/sigit.io/commit/64dbd7d479bb4baebc43d72f04dbedf7a18d02a7))
- **pdf-marking:** integrates UserDetails ([2becab9](https://git.nostrdev.com/sigit/sigit.io/commit/2becab9f79e1cb3aaf91178d67c70f9e98c4f98b))
- **pdf-marking:** updates design and functionality of the pdf marking form ([ed0158e](https://git.nostrdev.com/sigit/sigit.io/commit/ed0158e8177b79a56124c57569f5cba81d57b40b))
- **pdf-marking:** updates mark type and adds pdf-view components ([296b135](https://git.nostrdev.com/sigit/sigit.io/commit/296b135c064ef877faa951bce06e4d3b6928b4cb))
- **profile:** picture upload, robohash, website, npub cash ([041bd0d](https://git.nostrdev.com/sigit/sigit.io/commit/041bd0daff4ad06b5ef54798f4c43642c05a2d25))
- **Relay:** added methods to get info, most popular, connect and disconnect from relays ([ffb2379](https://git.nostrdev.com/sigit/sigit.io/commit/ffb237991cb669285ff9add7a3c610a1218dabcd))
- **Relays:** added logic to manage relays ([64f8227](https://git.nostrdev.com/sigit/sigit.io/commit/64f822743f8fc323ee47f715555c9f9fc579bf5c))
- **Relays:** improved relays page ([c37e8f3](https://git.nostrdev.com/sigit/sigit.io/commit/c37e8f36c26701a4743d38282f94801104becea7))
- search users by nip05, npub and filter: serach, improved UX ([6c7cac2](https://git.nostrdev.com/sigit/sigit.io/commit/6c7cac23361103665e318f52097677f4d95887b1))
- show block number on user profile ([1eed099](https://git.nostrdev.com/sigit/sigit.io/commit/1eed099059fe169e166c979e5900ceeb6da557b7))
- Sign Directly From the Marking Screen fix: Marking inputs glitches, losing values ([0a0a9be](https://git.nostrdev.com/sigit/sigit.io/commit/0a0a9bef348e798d37d892b602e19e82e41d0fba))
- **signature:** export signature files ([cdf26b6](https://git.nostrdev.com/sigit/sigit.io/commit/cdf26b6614fc33f840a20596a064b20cc503275a))
- **signature:** signature pad encrypt, upload, fetch, decrypt, render, add to pdf ([9551750](https://git.nostrdev.com/sigit/sigit.io/commit/9551750cbe0d84abc983e8746dcf67aedf99c525))
- **signature:** verify hash ([a371e98](https://git.nostrdev.com/sigit/sigit.io/commit/a371e98e9e402ba0ee4b674687f6dc71352eb78c))
- **signers-dropdown:** improved hiding/displaying logic ([76b1fa7](https://git.nostrdev.com/sigit/sigit.io/commit/76b1fa792c8cd27f36b30eecd829e75d810d5e00))
- **Store:** configured relays state ([106827b](https://git.nostrdev.com/sigit/sigit.io/commit/106827b6da2553acf7db27dfe0fe1b292837b654))
- update findMetadata method of metadata controller ([2b96172](https://git.nostrdev.com/sigit/sigit.io/commit/2b9617232ed3cd283249a43a6eb78db1297500ca))
- update signing flow ([1f9954b](https://git.nostrdev.com/sigit/sigit.io/commit/1f9954befd01c4e9f90330f3db89aa75f0039bbe))
- use nip04 for encryption and decryption of userData to store on blossom server ([18270c5](https://git.nostrdev.com/sigit/sigit.io/commit/18270c5d8afc376a6cef3b7cc3ea66a272797637))
- **verify-page:** add files view and content images ([2c586f3](https://git.nostrdev.com/sigit/sigit.io/commit/2c586f3c13f15010b08324557bbd89ba35fd00cb))
### Reverts
- "feat(pdf-marking): adds file validity check" ([268a4db](https://git.nostrdev.com/sigit/sigit.io/commit/268a4db3ff211566af3e8cf77838c54d3e9c861e))
### BREAKING CHANGES
- mark.value type changed

@ -6,19 +6,19 @@ Welcome to Sigit! We are thrilled that you are interested in contributing to thi
### Reporting Bugs
If you encounter a bug while using Sigit, please [open an issue](https://git.sigit.io/g/web/issues/new) on this repository. Provide as much detail as possible, including steps to reproduce the bug.
If you encounter a bug while using Sigit, please [open an issue](https://git.nostrdev.com/sigit/sigit.io/issues/new) on this repository. Provide as much detail as possible, including steps to reproduce the bug.
### Suggesting Enhancements
If you have an idea for how to improve Sigit, we would love to hear from you! [Open an issue](https://git.sigit.io/g/web/issues/new) to suggest an enhancement.
If you have an idea for how to improve Sigit, we would love to hear from you! [Open an issue](https://git.nostrdev.com/sigit/sigit.io/issues/new) to suggest an enhancement.
### Pull Requests
We welcome pull requests from contributors! To contribute code changes:
1. Fork the repository and create your branch from `main`.
1. Fork the repository and create your branch from `staging`.
2. Make your changes and ensure they pass any existing tests.
3. Write meaningful commit messages.
3. Write meaningful commit messages (conventional commit standard)
4. Submit a pull request, describing your changes in detail and referencing any related issues.
## Development Setup
@ -35,4 +35,14 @@ All contributions, including pull requests, undergo code review. Code review ens
## Contact
If you have questions or need further assistance, you can reach out to [maintainer's email].
If you have questions or need further assistance, you can reach out to `npub1d0csynrrxcynkcedktdzrdj6gnras2psg48mf46kxjazs8skrjgq9uzhlq`
## Testing
The following items should be tested with each release:
- Create a SIGit with at least 3 signers
- Create a SIGit where the creator is not the first signer
- Create a SIGit where one co-signer has no marks
- Create a SIGit using a file other than a PDF
- Use several login mechanisms, browsers, operating systems whilst testing

181
docs/blossom-flow.drawio Normal file

@ -0,0 +1,181 @@
<mxfile host="drawio-plugin" modified="2024-12-24T14:10:11.548Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" etag="j0ni0qfsydyzknoEy44Z" version="22.1.22" type="embed">
<diagram id="ADjf0_COJFV7FXKRQUJh" name="Page-1">
<mxGraphModel dx="1130" dy="824" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" background="#ffffff" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="13" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="11" target="12" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="11" value="User login" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="30" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="15" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="12" target="14" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="12" value="Find all blossom servers provided by a user" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="150" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="14" value="Fetch all SIGITS from each blossom server and display them all" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="280" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="22" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="17" target="21" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="17" value="User opens a SIGIT" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="30" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="21" target="23" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="21" value="Display a blossom server where this SIGIT was found" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="345" y="150" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="23" value="Display other blossom servers where SIGIT is not found, and button which will publish to a blossom server" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="345" y="250" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="270" y="380" as="sourcePoint" />
<mxPoint x="270" y="30" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="27" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="580" y="380" as="sourcePoint" />
<mxPoint x="580" y="30" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="30" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="28" target="29" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="28" value="Settings page (settings/servers)" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="640" y="30" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="32" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="29" target="31" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="29" value="Allow user to add and remove Blossom servers" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="665" y="150" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="31" value="Show suggested Blossom servers" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="665" y="250" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="33" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="40" y="426" as="sourcePoint" />
<mxPoint x="820" y="426" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="34" target="38" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="34" value="User 1" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="620" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="35" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;Servers: A, B, C&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=15;" parent="1" vertex="1">
<mxGeometry x="70" y="570" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="36" target="41" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="36" value="User 2" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="620" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="37" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;Servers: D, E&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=15;" parent="1" vertex="1">
<mxGeometry x="350" y="570" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="38" target="36" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="38" value="Creates document, sign and publish to A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="770" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="44" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="41" target="43" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="41" value="Reads the files From A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="770" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="43" value="Sign, update the meta.json and publish to D, E" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="920" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="45" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;File Servers (Blossom)&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
<mxGeometry x="330" y="440" width="190" height="30" as="geometry" />
</mxCell>
<mxCell id="46" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;Sigits&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
<mxGeometry x="40" y="510" width="50" height="30" as="geometry" />
</mxCell>
<mxCell id="47" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;User App Data&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
<mxGeometry x="40" y="1050" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="52" value="Loads the page" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="49" target="51" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="49" value="User" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1290" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="50" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;Servers: A, B, C&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=15;" parent="1" vertex="1">
<mxGeometry x="70" y="1240" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="61" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="51" target="60" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="51" value="Fetche the app data from A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1450" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="64" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="58" target="63" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="58" value="List the sigits found in the app data" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1750" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="62" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="60" target="58" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="60" value="We have 3 data sources, we merge all 3 sets into 1 object" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1600" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="63" value="User opens a SIGIT (flow is on the top)" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="65" y="1920" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="65" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;text-decoration:underline;&quot;&gt;&lt;b&gt;interface UserAppData&lt;/b&gt;&lt;/p&gt;&lt;hr&gt;&lt;p style=&quot;margin: 0px 0px 0px 8px; font-size: 14px;&quot;&gt;sigits&lt;br style=&quot;border-color: var(--border-color); text-align: center;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;blossomVersions&lt;/span&gt;&lt;br style=&quot;border-color: var(--border-color); text-align: center;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;processedGiftWrapps&lt;/span&gt;&lt;br style=&quot;border-color: var(--border-color); text-align: center;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;keyPair&lt;/span&gt;&lt;br&gt;&lt;/p&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" parent="1" vertex="1">
<mxGeometry x="40" y="1100" width="160" height="110" as="geometry" />
</mxCell>
<mxCell id="68" value="Creates a SIGIT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="66" target="67" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="66" value="User" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1290" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="70" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="67" target="69" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="67" value="Publish the SIGIT to A,B,C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1450" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="72" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="69" target="71" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="69" value="Capture the SIGIT urls and publish them in UserAppData to the servers A,B,C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1610" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="74" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="71" target="73">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="71" value="Keep last&amp;nbsp; 10 versions of blossom which includes SIGIT" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1770" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="79" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="73" target="77">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="73" value="Every blossom version can have multiple links that user added" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="345" y="1920" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="77" value="Get latest version, if it has multiple URLs choose the first one which matches the hash" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="2060" width="170" height="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

@ -4,10 +4,12 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="/app.webmanifest" />
<title>SIGit</title>
</head>
<body>
<div id="root"></div>
<script src="/opentimestamps.min.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

11348
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,14 +1,14 @@
{
"name": "sigit",
"private": true,
"version": "0.0.0",
"version": "1.0.4",
"type": "module",
"homepage": "https://sigit.io/",
"license": "AGPL-3.0-or-later ",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 2",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
@ -17,7 +17,8 @@
"preview": "vite preview",
"preinstall": "git config core.hooksPath .git-hooks",
"license-checker": "node licenseChecker.cjs",
"lint-staged": "lint-staged"
"lint-staged": "lint-staged",
"release": "commit-and-tag-version"
},
"dependencies": {
"@emotion/react": "11.11.4",
@ -30,23 +31,29 @@
"@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0",
"@nostr-dev-kit/ndk": "2.11.0",
"@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
"@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
"axios": "^1.8.2",
"crypto-hash": "3.0.0",
"crypto-js": "^4.2.0",
"dexie": "4.0.8",
"dnd-core": "16.0.1",
"file-saver": "2.0.5",
"idb": "8.0.0",
"jszip": "3.10.1",
"lodash": "4.17.21",
"material-ui-popup-state": "^5.3.1",
"mui-file-input": "4.0.4",
"nostr-login": "1.6.14",
"nostr-tools": "2.7.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.4.168",
"rdndmb-html5-to-touch": "^8.0.3",
"react": "^18.2.0",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dnd": "^16.0.1",
"react-dnd-multi-backend": "^8.0.3",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-redux": "9.1.0",
@ -54,18 +61,27 @@
"react-singleton-hook": "^4.0.1",
"react-toastify": "10.0.4",
"redux": "5.0.1",
"signature_pad": "^5.0.4",
"tseep": "1.2.1"
},
"devDependencies": {
"@saithodev/semantic-release-gitea": "^2.1.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^10.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/npm": "11.0.0",
"@semantic-release/release-notes-generator": "^11.0.4",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "2.0.7",
"@types/lodash": "4.14.202",
"@types/pdfjs-dist": "^2.10.378",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@types/svgo": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1",
"commit-and-tag-version": "^11.2.2",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
@ -75,6 +91,8 @@
"ts-css-modules-vite-plugin": "1.0.20",
"typescript": "^5.2.2",
"vite": "^5.1.4",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-zip-pack": "^1.2.4",
"vite-tsconfig-paths": "4.3.2"
},
"lint-staged": {

@ -1,15 +1,15 @@
{
"names": {
"_": "6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90"
},
"relays": {
"6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90": [
"wss://brb.io",
"wss://nostr.v0l.io",
"wss://nostr.coinos.io",
"wss://rsslay.nostr.net",
"wss://relay.current.fyi",
"wss://nos.io"
]
}
"names": {
"_": "6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90"
},
"relays": {
"6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90": [
"wss://brb.io",
"wss://nostr.v0l.io",
"wss://nostr.coinos.io",
"wss://rsslay.nostr.net",
"wss://relay.current.fyi",
"wss://nos.io"
]
}
}

58
public/app.webmanifest Normal file

@ -0,0 +1,58 @@
{
"short_name": "SIGit",
"name": "SIGit",
"description": "A decentralised document signing tool",
"icons": [
{
"src": "favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "favicon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "favicon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "favicon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "favicon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "favicon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "favicon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "favicon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "favicon-64x64.png",
"sizes": "64x64",
"type": "image/png"
}
],
"start_url": "/",
"display_override": ["minimal-ui", "standalone"],
"display": "minimal-ui",
"orientation": "any",
"theme_color": "#7d54a3",
"background_color": "#ffffff"
}

3
public/config.json Normal file

@ -0,0 +1,3 @@
{
"SIGIT_BLOSSOM": "https://blossom.sigit.io"
}

BIN
public/favicon-128x128.png Normal file

Binary file not shown.

After

(image error) Size: 3.3 KiB

BIN
public/favicon-144x144.png Normal file

Binary file not shown.

After

(image error) Size: 3.7 KiB

BIN
public/favicon-192x192.png Normal file

Binary file not shown.

After

(image error) Size: 5.2 KiB

BIN
public/favicon-256x256.png Normal file

Binary file not shown.

After

(image error) Size: 8.0 KiB

BIN
public/favicon-384x384.png Normal file

Binary file not shown.

After

(image error) Size: 13 KiB

BIN
public/favicon-512x512.png Normal file

Binary file not shown.

After

(image error) Size: 15 KiB

BIN
public/favicon-64x64.png Normal file

Binary file not shown.

After

(image error) Size: 1.5 KiB

BIN
public/favicon-72x72.png Normal file

Binary file not shown.

After

(image error) Size: 1.7 KiB

BIN
public/favicon-96x96.png Normal file

Binary file not shown.

After

(image error) Size: 2.3 KiB

25
public/favicon.svg Normal file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 282.61 282.61">
<defs>
<style>
.cls-1 {
fill: #47b17d;
}
.cls-2 {
fill: #4c82a3;
}
.cls-3 {
fill: #7d54a3;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1" transform="translate(0, 13.775)">
<g>
<path class="cls-2" d="M181.53,115.06h0c-9.4-36.67-56.77-24.79-121.09-12.57C-3.54,114.64-25.35,19.85,37.72,3.62,46.91,1.26,56.55,0,66.47,0c63.55,0,115.06,51.51,115.06,115.06Z"/>
<path class="cls-1" d="M100,140h0c9.4,36.67,56.77,24.79,121.09,12.57,63.98-12.16,85.79,82.64,22.72,98.86-9.19,2.36-18.83,3.62-28.76,3.62-63.55,0-115.06-51.51-115.06-115.06Z"/>
<circle class="cls-3" cx="140.77" cy="127.53" r="24.88"/>
</g>
</g>
</svg>

After

(image error) Size: 859 B

2
public/opentimestamps.min.js vendored Normal file

File diff suppressed because one or more lines are too long

@ -41,6 +41,7 @@ p {
body {
color: $text-color;
background: $body-background-color;
font-family: $font-familiy;
letter-spacing: $letter-spacing;
font-size: $body-font-size;
@ -70,6 +71,18 @@ input {
font-family: inherit;
}
ul {
list-style-type: none; /* Removes bullet points */
margin: 0; /* Removes default margin */
padding: 0; /* Removes default padding */
}
li {
list-style-type: none; /* Removes the bullets */
margin: 0; /* Removes any default margin */
padding: 0; /* Removes any default padding */
}
// Shared styles for center content (Create, Sign, Verify)
.files-wrapper {
display: flex;
@ -87,10 +100,10 @@ input {
// - first-child Header height, default body padding, and center content border (10px) and padding (10px)
// - others We don't include border and padding and scroll to the top of the image
&:first-child {
scroll-margin-top: $header-height + $body-vertical-padding + 20px;
scroll-margin-top: $body-vertical-padding + 20px;
}
&:not(:first-child) {
scroll-margin-top: $header-height + $body-vertical-padding;
scroll-margin-top: $body-vertical-padding;
}
}
@ -122,12 +135,20 @@ input {
// Consistent styling for every file mark
// Reverts some of the design defaults for font
.file-mark {
font-family: Arial;
font-size: 16px;
font-family: 'Roboto', serif;
font-style: normal;
font-weight: normal;
color: black;
letter-spacing: normal;
border: 1px solid transparent;
line-height: 1;
font-size: 16px;
color: black;
outline: 1px solid transparent;
justify-content: start;
align-items: start;
scroll-margin-top: $body-vertical-padding;
}
[data-dev='true'] {
@ -148,3 +169,18 @@ input {
color: rgba(0, 0, 0, 0.25);
font-size: 14px;
}
.settings-container {
width: 100%;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
grid-gap: 15px;
}
.text-center {
text-align: center;
}

@ -1,21 +1,20 @@
import { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { Navigate, Route, Routes } from 'react-router-dom'
import { AuthController, NostrController } from './controllers'
import { useAppSelector, useAuth } from './hooks'
import { MainLayout } from './layouts/Main'
import {
appPrivateRoutes,
appPublicRoutes,
privateRoutes,
publicRoutes,
recursiveRouteRenderer
} from './routes'
import { State } from './store/rootReducer'
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
} from './routes/util'
import './App.scss'
const App = () => {
const authState = useSelector((state: State) => state.auth)
const { checkSession } = useAuth()
const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
useEffect(() => {
if (window.location.hostname === '0.0.0.0') {
@ -25,34 +24,12 @@ const App = () => {
window.location.hostname = 'localhost'
}
generateBunkerDelegatedKey()
const authController = new AuthController()
authController.checkSession()
}, [])
const generateBunkerDelegatedKey = () => {
const existingKey = getNsecBunkerDelegatedKey()
if (!existingKey) {
const nostrController = NostrController.getInstance()
const newDelegatedKey = nostrController.generateDelegatedKey()
saveNsecBunkerDelegatedKey(newDelegatedKey)
}
}
const handleRootRedirect = () => {
if (authState.loggedIn) return appPrivateRoutes.homePage
const callbackPathEncoded = btoa(
window.location.href.split(`${window.location.origin}/#`)[1]
)
return `${appPublicRoutes.login}?callbackPath=${callbackPathEncoded}`
}
checkSession()
}, [checkSession])
// Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true
const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => {
return !authState.loggedIn || !r.hiddenWhenLoggedIn
return !isLoggedIn || !r.hiddenWhenLoggedIn
})
const privateRouteList = recursiveRouteRenderer(privateRoutes)
@ -60,9 +37,9 @@ const App = () => {
return (
<Routes>
<Route element={<MainLayout />}>
{authState?.loggedIn && privateRouteList}
{publicRoutesList}
<Route path="*" element={<Navigate to={handleRootRedirect()} />} />
{privateRouteList}
<Route path="*" element={<Navigate to={'/'} />} />
</Route>
</Routes>
)

Binary file not shown.

Binary file not shown.

After

(image error) Size: 186 KiB

@ -9,71 +9,47 @@ import {
} from '@mui/material'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
setAuthState,
setMetadataEvent,
userLogOutAction
} from '../../store/actions'
import { State } from '../../store/rootReducer'
import { Dispatch } from '../../store/store'
import { useAppSelector } from '../../hooks/store'
import Username from '../username'
import { Link, useNavigate } from 'react-router-dom'
import { MetadataController, NostrController } from '../../controllers'
import {
appPublicRoutes,
appPrivateRoutes,
getProfileRoute
} from '../../routes'
import {
clearAuthToken,
hexToNpub,
saveNsecBunkerDelegatedKey,
shorten
} from '../../utils'
import { getProfileUsername, hexToNpub } from '../../utils'
import styles from './style.module.scss'
import { setUserRobotImage } from '../../store/userRobotImage/action'
import { Container } from '../Container'
import { ButtonIcon } from '../ButtonIcon'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClose } from '@fortawesome/free-solid-svg-icons'
import useMediaQuery from '@mui/material/useMediaQuery'
import { useLogout } from '../../hooks/useLogout'
const metadataController = new MetadataController()
import { launch as launchNostrLoginDialog } from 'nostr-login'
export const AppBar = () => {
const navigate = useNavigate()
const dispatch: Dispatch = useDispatch()
const logout = useLogout()
const [username, setUsername] = useState('')
const [userAvatar, setUserAvatar] = useState('')
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
const authState = useSelector((state: State) => state.auth)
const metadataState = useSelector((state: State) => state.metadata)
const userRobotImage = useSelector((state: State) => state.userRobotImage)
const authState = useAppSelector((state) => state.auth)
const userProfile = useAppSelector((state) => state.user.profile)
const userRobotImage = useAppSelector((state) => state.user.robotImage)
useEffect(() => {
if (metadataState) {
if (metadataState.content) {
const { picture, display_name, name } = JSON.parse(
metadataState.content
)
if (picture || userRobotImage) {
setUserAvatar(picture || userRobotImage)
}
const npub = authState.usersPubkey
? hexToNpub(authState.usersPubkey)
: ''
setUsername(shorten(display_name || name || npub, 7))
} else {
setUserAvatar(userRobotImage || '')
setUsername('')
}
const npub = authState.usersPubkey ? hexToNpub(authState.usersPubkey) : ''
if (userProfile) {
setUserAvatar(userProfile.image || userRobotImage || '')
setUsername(getProfileUsername(npub, userProfile))
} else {
setUserAvatar('')
setUsername(getProfileUsername(npub))
}
}, [metadataState, userRobotImage, authState.usersPubkey])
}, [userRobotImage, authState.usersPubkey, userProfile])
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget)
@ -92,151 +68,167 @@ export const AppBar = () => {
const handleLogout = () => {
handleCloseUserMenu()
dispatch(
setAuthState({
keyPair: undefined,
loggedIn: false,
usersPubkey: undefined,
loginMethod: undefined,
nsecBunkerPubkey: undefined
})
)
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
dispatch(setUserRobotImage(null))
// clear authToken saved in local storage
clearAuthToken()
dispatch(userLogOutAction())
// update nsecBunker delegated key after logout
const nostrController = NostrController.getInstance()
const newDelegatedKey = nostrController.generateDelegatedKey()
saveNsecBunkerDelegatedKey(newDelegatedKey)
logout()
navigate('/')
}
const isAuthenticated = authState?.loggedIn === true
const matches = useMediaQuery('(max-width:767px)')
const [isBannerVisible, setIsBannerVisible] = useState(true)
const handleBannerHide = () => {
setIsBannerVisible(false)
}
return (
<AppBarMui
position="fixed"
className={styles.AppBar}
sx={{
boxShadow: 'none'
}}
>
<Container>
<Toolbar className={styles.toolbar} disableGutters={true}>
<Box className={styles.logoWrapper}>
<img src="/logo.svg" alt="Logo" onClick={() => navigate('/')} />
</Box>
<Box className={styles.rightSideBox}>
{!isAuthenticated && (
<>
{isAuthenticated && isBannerVisible && (
<div className={styles.banner}>
<Container>
<div className={styles.bannerInner}>
<p className={styles.bannerText}>
SIGit is currently Beta software (available for user experience
testing), use at your own risk!
</p>
<Button
startIcon={<ButtonIcon />}
onClick={() => {
navigate(appPublicRoutes.nostr)
}}
variant="contained"
aria-label={`close banner`}
variant="text"
onClick={handleBannerHide}
>
LOGIN
<FontAwesomeIcon icon={faClose} />
</Button>
)}
</div>
</Container>
</div>
)}
<AppBarMui
position={matches ? 'sticky' : 'static'}
className={styles.AppBar}
sx={{
boxShadow: 'none'
}}
>
<Container>
<Toolbar className={styles.toolbar} disableGutters={true}>
<Box className={styles.logoWrapper}>
<img
src="/logo.svg"
alt="Logo"
onClick={() => {
if (['', '#/'].includes(window.location.hash)) {
location.reload()
} else {
navigate('/')
}
}}
/>
</Box>
{isAuthenticated && (
<>
<Username
username={username}
avatarContent={userAvatar}
handleClick={handleOpenUserMenu}
/>
<Menu
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
<Box className={styles.rightSideBox}>
{!isAuthenticated && (
<Button
startIcon={<ButtonIcon />}
onClick={() => {
launchNostrLoginDialog()
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={!!anchorElUser}
onClose={handleCloseUserMenu}
variant="contained"
>
<MenuItem
sx={{
justifyContent: 'center',
display: { md: 'none' },
fontWeight: 500,
fontSize: '14px',
color: 'var(--text-color)'
}}
>
<Typography variant="h6">{username}</Typography>
</MenuItem>
<MenuItem
onClick={handleProfile}
sx={{
justifyContent: 'center'
}}
>
Profile
</MenuItem>
<MenuItem
onClick={() => {
setAnchorElUser(null)
LOGIN
</Button>
)}
navigate(appPrivateRoutes.settings)
{isAuthenticated && (
<>
<Username
username={username}
avatarContent={userAvatar}
handleClick={handleOpenUserMenu}
/>
<Menu
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
sx={{
justifyContent: 'center'
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
>
Settings
</MenuItem>
<MenuItem
onClick={() => {
setAnchorElUser(null)
navigate(appPublicRoutes.verify)
}}
sx={{
justifyContent: 'center'
}}
>
Verify
</MenuItem>
<Link
to={appPublicRoutes.source}
target="_blank"
style={{ color: 'inherit', textDecoration: 'inherit' }}
open={!!anchorElUser}
onClose={handleCloseUserMenu}
>
<MenuItem
sx={{
justifyContent: 'center',
display: { md: 'none' },
fontWeight: 500,
fontSize: '14px',
color: 'var(--text-color)'
}}
>
<Typography variant="h6">{username}</Typography>
</MenuItem>
<MenuItem
onClick={handleProfile}
sx={{
justifyContent: 'center'
}}
>
Source
Profile
</MenuItem>
</Link>
<MenuItem
onClick={handleLogout}
sx={{
justifyContent: 'center'
}}
>
Logout
</MenuItem>
</Menu>
</>
)}
</Box>
</Toolbar>
</Container>
</AppBarMui>
<MenuItem
onClick={() => {
setAnchorElUser(null)
navigate(appPrivateRoutes.profileSettings)
}}
sx={{
justifyContent: 'center'
}}
>
Settings
</MenuItem>
<MenuItem
onClick={() => {
setAnchorElUser(null)
navigate(appPublicRoutes.verify)
}}
sx={{
justifyContent: 'center'
}}
>
Verify
</MenuItem>
<Link
to={appPublicRoutes.source}
target="_blank"
style={{ color: 'inherit', textDecoration: 'inherit' }}
>
<MenuItem
sx={{
justifyContent: 'center'
}}
>
Source
</MenuItem>
</Link>
<MenuItem
onClick={handleLogout}
sx={{
justifyContent: 'center'
}}
>
Logout
</MenuItem>
</Menu>
</>
)}
</Box>
</Toolbar>
</Container>
</AppBarMui>
</>
)
}

@ -34,3 +34,42 @@
justify-content: flex-end;
}
}
.banner {
color: #ffffff;
background-color: $primary-main;
}
.bannerInner {
display: flex;
gap: 10px;
padding-block: 10px;
z-index: 1;
width: 100%;
justify-content: space-between;
flex-direction: row;
button {
min-width: 44px;
color: inherit;
}
&:hover,
&.active,
&:focus-within {
background: $primary-main;
color: inherit;
button {
color: inherit;
}
}
}
.bannerText {
margin-left: 54px;
flex-grow: 1;
text-align: center;
}

@ -0,0 +1,24 @@
import { PropsWithChildren } from 'react'
import styles from './style.module.scss'
interface ButtonUnderlineProps {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
disabled?: boolean | undefined
}
export const ButtonUnderline = ({
onClick,
disabled = false,
children
}: PropsWithChildren<ButtonUnderlineProps>) => {
return (
<button
type="button"
className={styles.button}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
)
}

@ -0,0 +1,25 @@
@import '../../styles/colors.scss';
.button {
color: $primary-main !important;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: max-content;
margin: 0 auto;
// Override default styling
border: none !important;
outline: none !important;
// Override leaky css in sign page
background: transparent !important;
&:focus,
&:hover {
text-decoration: underline;
text-decoration-color: inherit;
}
}

@ -0,0 +1,117 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Tooltip, Button, Divider } from '@mui/material'
import {
faCalendar,
faFile,
faFileCircleExclamation,
faPen,
faTrash
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { SigitDraft, UserRole } from '../../types'
import { appPrivateRoutes } from '../../routes'
import {
formatTimestamp,
getSigitDraft,
npubToHex,
SigitStatus,
SignStatus
} from '../../utils'
import { DisplaySigner } from '../DisplaySigner'
import { UserAvatarGroup } from '../UserAvatarGroup'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useAppSelector, useDidMount } from '../../hooks'
import styles from './style.module.scss'
interface LocalDraftSigitProps {
handleDraftDelete: () => void
}
export const LocalDraftSigit = ({
handleDraftDelete
}: LocalDraftSigitProps) => {
const [draft, setDraft] = useState<SigitDraft>()
useDidMount(async () => {
// Check if draft exists and add link to direct
const draft = await getSigitDraft()
if (draft) {
setDraft(draft)
}
})
const submittedBy = useAppSelector((state) => state.auth.usersPubkey)
if (!draft) return null
const extensions = draft.files.map((f) => f.extension)
const isSame = extensions.every((e) => extensions[0] === e)
return (
<div className={styles.itemWrapper}>
<Link className={styles.insetLink} to={appPrivateRoutes.create}></Link>
<p className={`line-clamp-2 ${styles.title}`}>{draft.title}</p>
<div className={styles.users}>
{submittedBy && (
<DisplaySigner status={SignStatus.Pending} pubkey={submittedBy} />
)}
{submittedBy && draft.users.length ? (
<Divider orientation="vertical" flexItem />
) : null}
<UserAvatarGroup max={7}>
{draft.users.map((user) => {
const pubkey = npubToHex(user.pubkey)!
return (
<DisplaySigner
key={pubkey}
status={
user.role === UserRole.signer
? SignStatus.Pending
: SignStatus.Viewer
}
pubkey={pubkey}
/>
)
})}
</UserAvatarGroup>
</div>
<div className={`${styles.details} ${styles.iconLabel}`}>
<FontAwesomeIcon icon={faCalendar} />
{formatTimestamp(draft.lastUpdated)}
</div>
<div className={`${styles.details} ${styles.status}`}>
<span className={styles.iconLabel}>
<FontAwesomeIcon icon={faPen} /> {SigitStatus.LocalDraft}
</span>
{extensions.length > 0 ? (
<span className={styles.iconLabel}>
{!isSame ? (
<>
<FontAwesomeIcon icon={faFile} /> Multiple File Types
</>
) : (
getExtensionIconLabel(extensions[0])
)}
</span>
) : (
<>
<FontAwesomeIcon icon={faFileCircleExclamation} /> &mdash;
</>
)}
</div>
<div className={styles.itemActions}>
<Tooltip title="Delete" arrow placement="top" disableInteractive>
<Button
onClick={handleDraftDelete}
sx={{
color: 'var(--primary-main)',
minWidth: '34px',
padding: '10px'
}}
variant={'text'}
>
<FontAwesomeIcon icon={faTrash} />
</Button>
</Tooltip>
</div>
</div>
)
}

@ -1,7 +1,12 @@
import { Meta } from '../../types'
import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils'
import {
hexToNpub,
SigitCardDisplayInfo,
SigitStatus,
SignStatus
} from '../../utils'
import { Link } from 'react-router-dom'
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
import { formatTimestamp, npubToHex } from '../../utils'
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
import { Button, Divider, Tooltip } from '@mui/material'
import { DisplaySigner } from '../DisplaySigner'
@ -17,99 +22,73 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { UserAvatarGroup } from '../UserAvatarGroup'
import styles from './style.module.scss'
import { TooltipChild } from '../TooltipChild'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import { useSigitMeta } from '../../hooks/useSigitMeta'
import { extractFileExtensions } from '../../utils/file'
import { useAppSelector } from '../../hooks'
type SigitProps = {
sigitCreateId: string
meta: Meta
parsedMeta: SigitCardDisplayInfo
}
export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
export const DisplaySigit = ({
meta,
parsedMeta,
sigitCreateId: sigitCreateId
}: SigitProps) => {
const { usersPubkey } = useAppSelector((state) => state.auth)
const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
parsedMeta
const { signersStatus, fileHashes } = useSigitMeta(meta)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers
])
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
const currentUserNpub: string = usersPubkey ? hexToNpub(usersPubkey) : ''
const currentUserNextSigner =
signersStatus[currentUserNpub as `npub1${string}`] === SignStatus.Awaiting
return (
<div className={styles.itemWrapper}>
<Link
to={
signedStatus === SigitStatus.Complete
? appPublicRoutes.verify
: appPrivateRoutes.sign
}
state={{ meta }}
className={styles.insetLink}
></Link>
{signedStatus === SigitStatus.Complete || !currentUserNextSigner ? (
<Link
to={`${appPublicRoutes.verify}/${sigitCreateId}`}
className={styles.insetLink}
></Link>
) : (
<Link
to={`${appPrivateRoutes.sign}/${sigitCreateId}`}
className={styles.insetLink}
></Link>
)}
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
<div className={styles.users}>
{submittedBy &&
(function () {
const profile = profiles[submittedBy]
return (
<Tooltip
key={submittedBy}
title={
profile?.display_name ||
profile?.name ||
shorten(hexToNpub(submittedBy))
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
profile={profile}
pubkey={submittedBy}
/>
</TooltipChild>
</Tooltip>
)
})()}
{submittedBy && (
<DisplaySigner
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
pubkey={submittedBy}
/>
)}
{submittedBy && signers.length ? (
<Divider orientation="vertical" flexItem />
) : null}
<UserAvatarGroup max={7}>
{signers.map((signer) => {
const pubkey = npubToHex(signer)!
const profile = profiles[pubkey]
return (
<Tooltip
key={signer}
title={
profile?.display_name || profile?.name || shorten(pubkey)
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={signersStatus[signer]}
profile={profile}
pubkey={pubkey}
/>
</TooltipChild>
</Tooltip>
<DisplaySigner
key={pubkey}
status={signersStatus[signer]}
pubkey={pubkey}
/>
)
})}
</UserAvatarGroup>
</div>
<div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}>
<div className={`${styles.details} ${styles.iconLabel}`}>
<FontAwesomeIcon icon={faCalendar} />
{createdAt ? formatTimestamp(createdAt) : null}
</div>
@ -133,32 +112,37 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
</>
)}
</div>
<div className={styles.itemActions}>
<Tooltip title="Duplicate" arrow placement="top" disableInteractive>
<Button
sx={{
color: 'var(--primary-main)',
minWidth: '34px',
padding: '10px'
}}
variant={'text'}
>
<FontAwesomeIcon icon={faCopy} />
</Button>
</Tooltip>
<Tooltip title="Archive" arrow placement="top" disableInteractive>
<Button
sx={{
color: 'var(--primary-main)',
minWidth: '34px',
padding: '10px'
}}
variant={'text'}
>
<FontAwesomeIcon icon={faArchive} />
</Button>
</Tooltip>
</div>
{
// TODO: enable buttons once feature is ready
false && (
<div className={styles.itemActions}>
<Tooltip title="Duplicate" arrow placement="top" disableInteractive>
<Button
sx={{
color: 'var(--primary-main)',
minWidth: '34px',
padding: '10px'
}}
variant={'text'}
>
<FontAwesomeIcon icon={faCopy} />
</Button>
</Tooltip>
<Tooltip title="Archive" arrow placement="top" disableInteractive>
<Button
sx={{
color: 'var(--primary-main)',
minWidth: '34px',
padding: '10px'
}}
variant={'text'}
>
<FontAwesomeIcon icon={faArchive} />
</Button>
</Tooltip>
</div>
)
}
</div>
)
}

@ -1,5 +1,4 @@
import { Badge } from '@mui/material'
import { ProfileMetadata } from '../../types'
import styles from './style.module.scss'
import { UserAvatar } from '../UserAvatar'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -15,38 +14,33 @@ import { SignStatus } from '../../utils'
import { Spinner } from '../Spinner'
type DisplaySignerProps = {
profile: ProfileMetadata
pubkey: string
status: SignStatus
}
export const DisplaySigner = ({
status,
profile,
pubkey
}: DisplaySignerProps) => {
const getStatusIcon = (status: SignStatus) => {
switch (status) {
case SignStatus.Signed:
return <FontAwesomeIcon icon={faCheck} />
case SignStatus.Awaiting:
return (
<Spinner>
<FontAwesomeIcon icon={faHourglass} />
</Spinner>
)
case SignStatus.Pending:
return <FontAwesomeIcon icon={faEllipsis} />
case SignStatus.Invalid:
return <FontAwesomeIcon icon={faExclamation} />
case SignStatus.Viewer:
return <FontAwesomeIcon icon={faEye} />
const getStatusIcon = (status: SignStatus) => {
switch (status) {
case SignStatus.Signed:
return <FontAwesomeIcon icon={faCheck} />
case SignStatus.Awaiting:
return (
<Spinner>
<FontAwesomeIcon icon={faHourglass} />
</Spinner>
)
case SignStatus.Pending:
return <FontAwesomeIcon icon={faEllipsis} />
case SignStatus.Invalid:
return <FontAwesomeIcon icon={faExclamation} />
case SignStatus.Viewer:
return <FontAwesomeIcon icon={faEye} />
default:
return <FontAwesomeIcon icon={faQuestion} />
}
default:
return <FontAwesomeIcon icon={faQuestion} />
}
}
export const DisplaySigner = ({ status, pubkey }: DisplaySignerProps) => {
return (
<Badge
className={styles.signer}
@ -56,7 +50,7 @@ export const DisplaySigner = ({
<div className={styles.statusBadge}>{getStatusIcon(status)}</div>
}
>
<UserAvatar pubkey={pubkey} image={profile?.picture} />
<UserAvatar pubkey={pubkey} />
</Badge>
)
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,3 @@
.counterpartSelectValue {
display: flex;
}

@ -0,0 +1,47 @@
import React from 'react'
import { User } from '../../../types'
import _ from 'lodash'
import { npubToHex, getProfileUsername } from '../../../utils'
import { AvatarIconButton } from '../../UserAvatarIconButton'
import styles from './Counterpart.module.scss'
import { NDKUserProfile } from '@nostr-dev-kit/ndk'
interface CounterpartProps {
npub: string
userProfiles: {
[key: string]: NDKUserProfile
}
signers: User[]
}
export const Counterpart = React.memo(
({ npub, userProfiles, signers }: CounterpartProps) => {
let displayValue = _.truncate(npub, { length: 16 })
const signer = signers.find((u) => u.pubkey === npubToHex(npub))
if (signer) {
const profile = userProfiles[signer.pubkey]
displayValue = getProfileUsername(npub, profile)
return (
<div className={styles.counterpartSelectValue}>
<AvatarIconButton
src={profile?.image}
hexKey={signer.pubkey || undefined}
sx={{
padding: 0,
marginRight: '6px',
'> img': {
width: '21px',
height: '21px'
}
}}
/>
{displayValue}
</div>
)
}
return displayValue
}
)

@ -0,0 +1,19 @@
import React from 'react'
import { SigitFile } from '../../../utils/file'
import { ExtensionFileBox } from '../../ExtensionFileBox'
import { ImageItem } from './ImageItem'
interface FileItemProps {
file: SigitFile
}
export const FileItem = React.memo(({ file }: FileItemProps) => {
const content = <ExtensionFileBox extension={file.extension} />
if (file.isImage) return <ImageItem file={file} />
return (
<div key={file.name} className="file-wrapper" id={`file-${file.name}`}>
{content}
</div>
)
})

@ -0,0 +1,10 @@
import React from 'react'
import { SigitFile } from '../../../utils/file'
interface ImageItemProps {
file: SigitFile
}
export const ImageItem = React.memo(({ file }: ImageItemProps) => {
return <img className="file-image" src={file.objectUrl} alt={file.name} />
})

@ -13,10 +13,20 @@
}
}
.pdfImageWrapper:focus {
outline: none;
}
.placeholder {
position: absolute;
opacity: 0.5;
inset: 0;
}
.drawingRectangle {
position: absolute;
border: 1px solid #01aaad;
z-index: 50;
outline: 1px solid #01aaad;
z-index: 40;
background-color: #01aaad4b;
cursor: pointer;
display: flex;
@ -28,10 +38,6 @@
visibility: hidden;
}
&.edited {
border: 1px dotted #01aaad;
}
.resizeHandle {
position: absolute;
right: -5px;
@ -41,7 +47,7 @@
background-color: #fff;
border: 1px solid rgb(160, 160, 160);
border-radius: 50%;
cursor: nwse-resize;
cursor: grab;
// Increase the area a bit so it's easier to click
&::after {
@ -78,3 +84,35 @@
padding: 5px 0;
}
}
.counterpartAvatar {
img {
width: 21px;
height: 21px;
}
}
.signingRectangle {
position: absolute;
outline: 1px solid #01aaad;
z-index: 40;
background-color: #01aaad4b;
cursor: pointer;
&.edited {
outline: 1px dotted #01aaad;
}
}
.drawingRectanglePreview {
position: absolute;
outline: 1px solid;
z-index: 50;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
touch-action: none;
opacity: 0.8;
}

@ -1,54 +1,104 @@
import React from 'react'
import { Button, Menu, MenuItem } from '@mui/material'
import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faCheck,
faLock,
faTriangleExclamation
} from '@fortawesome/free-solid-svg-icons'
import { CurrentUserFile } from '../../types/file.ts'
import styles from './style.module.scss'
import { Button } from '@mui/material'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
interface FileListProps {
files: CurrentUserFile[]
currentFile: CurrentUserFile
setCurrentFile: (file: CurrentUserFile) => void
handleDownload: () => void
downloadLabel?: string
handleExport?: () => void
handleEncryptedExport?: () => void
reBroadcastSigit?: () => void
}
const FileList = ({
files,
currentFile,
setCurrentFile,
handleDownload,
downloadLabel
handleExport,
handleEncryptedExport,
reBroadcastSigit
}: FileListProps) => {
const isActive = (file: CurrentUserFile) => file.id === currentFile.id
return (
<div className={styles.wrap}>
<div className={styles.container}>
<ul className={styles.files}>
{files.map((currentUserFile: CurrentUserFile) => (
<li
key={currentUserFile.id}
className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`}
onClick={() => setCurrentFile(currentUserFile)}
>
<div className={styles.fileNumber}>{currentUserFile.id}</div>
<div className={styles.fileInfo}>
<div className={styles.fileName}>
{currentUserFile.file.name}
</div>
</div>
<ul className={styles.files}>
{files.map((currentUserFile: CurrentUserFile) => (
<li
key={currentUserFile.id}
className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`}
onClick={() => setCurrentFile(currentUserFile)}
>
<div className={styles.fileNumber}>{currentUserFile.id}</div>
<div className={styles.fileInfo}>
<div className={styles.fileName}>{currentUserFile.file.name}</div>
</div>
<div className={styles.fileVisual}>
{currentUserFile.isHashValid && (
<FontAwesomeIcon icon={faCheck} />
<div className={styles.fileVisual}>
{currentUserFile.isHashValid && (
<FontAwesomeIcon icon={faCheck} />
)}
</div>
</li>
))}
</ul>
{(typeof handleExport === 'function' ||
typeof handleEncryptedExport === 'function') && (
<PopupState variant="popover" popupId="download-popup-menu">
{(popupState) => (
<React.Fragment>
<Button variant="contained" {...bindTrigger(popupState)}>
Export files
</Button>
<Menu {...bindMenu(popupState)}>
{typeof handleEncryptedExport === 'function' && (
<MenuItem
onClick={() => {
popupState.close
handleEncryptedExport()
}}
>
<FontAwesomeIcon
color={'var(--mui-palette-primary-main)'}
icon={faLock}
/>
&nbsp; ENCRYPTED
</MenuItem>
)}
</div>
</li>
))}
</ul>
</div>
<Button variant="contained" fullWidth onClick={handleDownload}>
{downloadLabel || 'Download Files'}
</Button>
{typeof handleExport === 'function' && (
<MenuItem
onClick={() => {
popupState.close
handleExport()
}}
>
<FontAwesomeIcon
color={'var(--mui-palette-primary-main)'}
icon={faTriangleExclamation}
/>
&nbsp; UNENCRYPTED
</MenuItem>
)}
</Menu>
</React.Fragment>
)}
</PopupState>
)}
{typeof reBroadcastSigit === 'function' && (
<Button variant="contained" onClick={reBroadcastSigit}>
Re-Broadcast
</Button>
)}
</div>
)
}

@ -1,12 +1,3 @@
.container {
border-radius: 4px;
background: white;
padding: 15px;
display: flex;
flex-direction: column;
grid-gap: 0px;
}
.filesPageContainer {
width: 100%;
display: grid;
@ -15,18 +6,6 @@
flex-grow: 1;
}
ul {
list-style-type: none; /* Removes bullet points */
margin: 0; /* Removes default margin */
padding: 0; /* Removes default padding */
}
li {
list-style-type: none; /* Removes the bullets */
margin: 0; /* Removes any default margin */
padding: 0; /* Removes any default padding */
}
.wrap {
display: flex;
flex-direction: column;
@ -34,14 +13,16 @@ li {
}
.files {
border-radius: 4px;
background: white;
padding: 15px;
display: flex;
flex-direction: column;
width: 100%;
grid-gap: 15px;
max-height: 350px;
overflow: auto;
padding: 0 5px 0 0;
margin: 0 -5px 0 0;
overflow-y: auto;
overflow-x: none;
}
.files::-webkit-scrollbar {

@ -4,125 +4,134 @@ import styles from './style.module.scss'
import { Container } from '../Container'
import nostrImage from '../../assets/images/nostr.gif'
import { appPublicRoutes } from '../../routes'
import { createPortal } from 'react-dom'
export const Footer = () => (
<footer className={`${styles.borderTop} ${styles.footer}`}>
<Container
style={{
paddingBlock: '50px'
}}
>
<Box
display={'grid'}
sx={{
gridTemplateColumns: {
xs: '1fr',
md: '0.5fr 2fr 0.5fr'
},
alignItems: {
xs: 'center',
md: 'start'
}
export const Footer = () =>
createPortal(
<footer className={`${styles.borderTop} ${styles.footer}`}>
<Container
style={{
paddingBlock: '50px'
}}
gap={'50px'}
>
<LinkMui
<Box
display={'grid'}
sx={{
justifySelf: {
gridTemplateColumns: {
xs: '1fr',
md: '0.5fr 2fr 0.5fr'
},
alignItems: {
xs: 'center',
md: 'start'
}
}}
component={Link}
to={'/'}
className={styles.logo}
gap={'50px'}
>
<img src="/logo.svg" alt="Logo" />
</LinkMui>
<Box
display={'grid'}
sx={{
gap: '15px',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
xl: 'repeat(3, 1fr)'
},
borderBlock: {
xs: 'solid 1px rgba(0, 0, 0, 0.1)',
md: 'unset'
},
paddingY: {
xs: '10px',
md: 'unset'
}
}}
component={'nav'}
className={styles.nav}
>
<Button
<LinkMui
sx={{
justifyContent: 'center'
justifySelf: {
xs: 'center',
md: 'start'
}
}}
component={Link}
to={'/'}
variant={'text'}
className={styles.logo}
>
Home
</Button>
<Button
<img src="/logo.svg" alt="Logo" />
</LinkMui>
<Box
display={'grid'}
sx={{
justifyContent: 'center'
gap: '15px',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
xl: 'repeat(3, 1fr)'
},
borderBlock: {
xs: 'solid 1px rgba(0, 0, 0, 0.1)',
md: 'unset'
},
paddingY: {
xs: '10px',
md: 'unset'
}
}}
component={LinkMui}
href={appPublicRoutes.docs}
target="_blank"
variant={'text'}
component={'nav'}
className={styles.nav}
>
Documentation
</Button>
<Button
<Button
sx={{
justifyContent: 'center'
}}
component={Link}
to={'/'}
onClick={(event) => {
if (['', '#/'].includes(window.location.hash)) {
event.preventDefault()
window.scrollTo(0, 0)
}
}}
variant={'text'}
>
Home
</Button>
<Button
sx={{
justifyContent: 'center'
}}
component={LinkMui}
href={appPublicRoutes.docs}
target="_blank"
variant={'text'}
>
Documentation
</Button>
<Button
sx={{
justifyContent: 'center'
}}
component={LinkMui}
href={appPublicRoutes.source}
target="_blank"
variant={'text'}
>
Source
</Button>
</Box>
<Box
className={styles.links}
sx={{
justifyContent: 'center'
justifySelf: {
xs: 'center',
md: 'end'
}
}}
component={LinkMui}
href={appPublicRoutes.source}
target="_blank"
variant={'text'}
>
Source
</Button>
<Button
component={LinkMui}
href="https://snort.social/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv"
target="_blank"
sx={{
minWidth: '45px',
padding: '10px'
}}
variant={'contained'}
>
<img src={nostrImage} width="25" alt="nostr logo" height="25" />
</Button>
</Box>
</Box>
<Box
className={styles.links}
sx={{
justifySelf: {
xs: 'center',
md: 'end'
}
}}
>
<Button
component={LinkMui}
href="https://snort.social/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv"
target="_blank"
sx={{
minWidth: '45px',
padding: '10px'
}}
variant={'contained'}
>
<img src={nostrImage} width="25" alt="nostr logo" height="25" />
</Button>
</Box>
</Box>
</Container>
<div className={`${styles.borderTop} ${styles.credits}`}>
Built by&nbsp;
<a href="https://nostrdev.com/" target="_blank">
Nostr Dev
</a>{' '}
2024.
</div>
</footer>
)
</Container>
<div className={`${styles.borderTop} ${styles.credits}`}>
Built by&nbsp;
<a rel="noopener" href="https://nostrdev.com/" target="_blank">
Nostr Dev
</a>{' '}
2024.
</div>
</footer>,
document.getElementById('root')!
)

@ -1,18 +1,43 @@
import { createPortal } from 'react-dom'
import styles from './style.module.scss'
import { PropsWithChildren } from 'react'
interface Props {
desc: string
desc?: string
variant?: 'small' | 'default'
}
export const LoadingSpinner = (props: Props) => {
const { desc } = props
export const LoadingSpinner = (props: PropsWithChildren<Props>) => {
const { desc, children, variant = 'default' } = props
return (
<div className={styles.loadingSpinnerOverlay}>
<div className={styles.loadingSpinnerContainer}>
<div className={styles.loadingSpinner}></div>
{desc && <span className={styles.loadingSpinnerDesc}>{desc}</span>}
</div>
</div>
)
switch (variant) {
case 'small':
return (
<div
className={`${styles.loadingSpinnerContainer}`}
data-variant={variant}
>
<div className={styles.loadingSpinner}></div>
</div>
)
default:
return createPortal(
<div className={styles.loadingSpinnerOverlay}>
<div
className={styles.loadingSpinnerContainer}
data-variant={variant}
>
<div className={styles.loadingSpinner}></div>
{desc && (
<div className={styles.loadingSpinnerDesc}>
{desc}
{children}
</div>
)}
</div>
</div>,
document.getElementById('root')!
)
}
}

@ -2,37 +2,56 @@
.loadingSpinnerOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
z-index: 70;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.loadingSpinnerContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.loadingSpinnerContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
&[data-variant='default'] {
width: 100%;
max-width: 500px;
margin: 25px 20px;
background: $overlay-background-color;
border-radius: 4px;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
}
.loadingSpinner {
background: url('/favicon.png') no-repeat center / cover;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
&[data-variant='small'] {
min-height: 250px;
}
}
.loadingSpinner {
background: url('/favicon.png') no-repeat center / cover;
margin: 40px 25px;
width: 65px;
height: 65px;
animation: spin 1s linear infinite;
}
.loadingSpinnerDesc {
color: white;
margin-top: 13px;
width: 100%;
padding: 15px;
border-top: solid 1px rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.5);
font-size: 16px;
font-weight: 400;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
@keyframes spin {

@ -1,22 +1,27 @@
import { CurrentUserMark } from '../../types/mark.ts'
import styles from './style.module.scss'
import { MARK_TYPE_TRANSLATION, NEXT, SIGN } from '../../utils/const.ts'
import {
findNextIncompleteCurrentUserMark,
getToolboxLabelByMarkType,
isCurrentUserMarksComplete,
isCurrentValueLast
} from '../../utils'
import React, { useState } from 'react'
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck, faDownload } from '@fortawesome/free-solid-svg-icons'
import { Button } from '@mui/material'
import styles from './style.module.scss'
import { ButtonUnderline } from '../ButtonUnderline/index.tsx'
interface MarkFormFieldProps {
currentUserMarks: CurrentUserMark[]
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
handleSelectedMarkValueChange: (
event: React.ChangeEvent<HTMLInputElement>
handleSelectedMarkValueChange: (value: string) => void
handleSubmit: (
event: React.MouseEvent<HTMLButtonElement>,
type: 'online' | 'offline'
) => void
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
selectedMark: CurrentUserMark
selectedMark: CurrentUserMark | null
selectedMarkValue: string
}
@ -32,28 +37,52 @@ const MarkFormField = ({
handleCurrentUserMarkChange
}: MarkFormFieldProps) => {
const [displayActions, setDisplayActions] = useState(true)
const getSubmitButtonText = () => (isReadyToSign() ? SIGN : NEXT)
const [complete, setComplete] = useState(false)
const isReadyToSign = () =>
isCurrentUserMarksComplete(currentUserMarks) ||
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
const isCurrent = (currentMark: CurrentUserMark) =>
currentMark.id === selectedMark.id
currentMark.id === selectedMark?.id && !complete
const isDone = (currentMark: CurrentUserMark) =>
isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted
const findNext = () => {
return (
currentUserMarks[selectedMark.id] ||
currentUserMarks[selectedMark!.id] ||
findNextIncompleteCurrentUserMark(currentUserMarks)
)
}
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
console.log('handle form submit runs...')
return isReadyToSign()
? handleSubmit(event)
: handleCurrentUserMarkChange(findNext()!)
// Without this line, we lose mark values when switching
handleCurrentUserMarkChange(selectedMark!)
if (!complete) {
isReadyToSign()
? setComplete(true)
: handleCurrentUserMarkChange(findNext()!)
}
}
const toggleActions = () => setDisplayActions(!displayActions)
const markLabel = selectedMark
? getToolboxLabelByMarkType(selectedMark.mark.type)
: ''
const handleCurrentUserMarkClick = (mark: CurrentUserMark) => {
setComplete(false)
handleCurrentUserMarkChange(mark)
}
const handleSelectCompleteMark = () => {
if (currentUserMarks.length) handleCurrentUserMarkChange(selectedMark!)
setComplete(true)
}
const handleSignAndComplete =
(type: 'online' | 'offline') =>
(event: React.MouseEvent<HTMLButtonElement>) => {
handleSubmit(event, type)
}
return (
<div className={styles.container}>
<div className={styles.trigger}>
@ -61,6 +90,7 @@ const MarkFormField = ({
onClick={toggleActions}
className={styles.triggerBtn}
type="button"
title="Toggle"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -78,33 +108,64 @@ const MarkFormField = ({
<div className={styles.actionsWrapper}>
<div className={styles.actionsTop}>
<div className={styles.actionsTopInfo}>
<p className={styles.actionsTopInfoText}>Add your signature</p>
{!complete && selectedMark ? (
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
) : (
<p className={styles.actionsTopInfoText}>Finish</p>
)}
</div>
</div>
<div className={styles.inputWrapper}>
<form onSubmit={(e) => handleFormSubmit(e)}>
<input
className={styles.input}
placeholder={
MARK_TYPE_TRANSLATION[selectedMark.mark.type.valueOf()]
}
onChange={handleSelectedMarkValueChange}
value={selectedMarkValue}
/>
<div className={styles.actionsBottom}>
<button type="submit" className={styles.submitButton}>
{getSubmitButtonText()}
</button>
</div>
</form>
{!complete && selectedMark ? (
<form onSubmit={(e) => handleFormSubmit(e)}>
<MarkInput
markType={selectedMark.mark.type}
key={selectedMark.id}
value={selectedMarkValue}
placeholder={markLabel}
handler={handleSelectedMarkValueChange}
userMark={selectedMark}
/>
<div className={styles.actionsBottom}>
<Button type="submit" className={styles.submitButton}>
NEXT
</Button>
</div>
</form>
) : (
<>
<div className={styles.actionsBottom}>
<Button
onClick={handleSignAndComplete('online')}
className={[
styles.submitButton,
styles.completeButton
].join(' ')}
disabled={!isReadyToSign()}
autoFocus
>
SIGN AND BROADCAST
</Button>
</div>
<ButtonUnderline
onClick={handleSignAndComplete('offline')}
disabled={!isReadyToSign()}
>
<FontAwesomeIcon icon={faDownload} />
Sign and export locally instead
</ButtonUnderline>
</>
)}
<div className={styles.footerContainer}>
<div className={styles.footer}>
{currentUserMarks.map((mark, index) => {
return (
<div className={styles.pagination} key={index}>
<button
className={`${styles.paginationButton} ${isDone(mark) && styles.paginationButtonDone}`}
onClick={() => handleCurrentUserMarkChange(mark)}
type="button"
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
onClick={() => handleCurrentUserMarkClick(mark)}
>
{mark.id}
</button>
@ -114,6 +175,22 @@ const MarkFormField = ({
</div>
)
})}
<div className={styles.pagination}>
<button
type="button"
className={`${styles.paginationButton} ${isReadyToSign() ? styles.paginationButtonDone : ''}`}
onClick={handleSelectCompleteMark}
title="Complete"
>
<FontAwesomeIcon
className={styles.finishPage}
icon={faCheck}
/>
</button>
{complete && (
<div className={styles.paginationButtonCurrent}></div>
)}
</div>
</div>
</div>
</div>

@ -1,13 +1,21 @@
@import '../../styles/sizes.scss';
.container {
width: 100%;
display: flex;
flex-direction: column;
position: fixed;
bottom: 0;
right: 0;
left: 0;
@media only screen and (min-width: 768px) {
bottom: 0;
right: 0;
left: 0;
}
bottom: $tabs-height + 5px;
right: 5px;
left: 5px;
align-items: center;
z-index: 1000;
z-index: 40;
button {
transition: ease 0.2s;
@ -62,6 +70,11 @@
margin-top: 10px;
}
.completeButton {
font-size: 18px;
padding: 10px 20px;
}
.paginationButton {
font-size: 12px;
padding: 5px 10px;
@ -70,7 +83,8 @@
color: rgba(0, 0, 0, 0.5);
}
.paginationButton:hover {
.paginationButton:hover,
.paginationButton:focus {
background: #447592;
color: rgba(255, 255, 255, 0.5);
}
@ -107,14 +121,14 @@
.actions {
background: white;
width: 100%;
border-radius: 4px;
border-radius: 5px;
padding: 10px 20px;
display: none;
flex-direction: column;
align-items: center;
grid-gap: 15px;
box-shadow: 0 -2px 4px 0 rgb(0, 0, 0, 0.1);
max-width: 750px;
max-width: 450px;
&.expanded {
display: flex;
@ -208,3 +222,7 @@
flex-direction: column;
grid-gap: 5px;
}
.finishPage {
padding: 1px 0;
}

@ -0,0 +1,24 @@
import { MarkInputProps } from '../MarkStrategy'
import styles from '../../MarkFormField/style.module.scss'
import { useEffect, useRef } from 'react'
export const MarkInputDateTime = ({ handler, placeholder }: MarkInputProps) => {
const ref = useRef<HTMLInputElement>(null)
useEffect(() => {
if (ref.current) {
const date = new Date()
ref.current.value = date.toISOString().slice(0, 16)
handler(date.toUTCString())
}
}, [handler])
return (
<input
type="datetime-local"
ref={ref}
className={styles.input}
placeholder={placeholder}
readOnly={true}
disabled={true}
/>
)
}

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputDateTime } from './Input'
export const DateTimeStrategy: MarkStrategy = {
input: MarkInputDateTime,
render: ({ value }) => <>{value}</>
}

@ -0,0 +1,20 @@
import { useDidMount } from '../../../hooks'
import { useLocalStorage } from '../../../hooks/useLocalStorage'
import { MarkInputProps } from '../MarkStrategy'
import { MarkInputText } from '../Text/Input'
export const MarkInputFullName = (props: MarkInputProps) => {
const [fullName, setFullName] = useLocalStorage('mark-fullname', '')
useDidMount(() => {
props.handler(fullName)
})
return MarkInputText({
...props,
placeholder: 'Full Name',
value: fullName,
handler: (value) => {
setFullName(value)
props.handler(value)
}
})
}

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputFullName } from './Input'
export const FullNameStrategy: MarkStrategy = {
input: MarkInputFullName,
render: ({ value }) => <>{value}</>
}

@ -0,0 +1,20 @@
import { useDidMount } from '../../../hooks'
import { useLocalStorage } from '../../../hooks/useLocalStorage'
import { MarkInputProps } from '../MarkStrategy'
import { MarkInputText } from '../Text/Input'
export const MarkInputJobTitle = (props: MarkInputProps) => {
const [jobTitle, setjobTitle] = useLocalStorage('mark-jobtitle', '')
useDidMount(() => {
props.handler(jobTitle)
})
return MarkInputText({
...props,
placeholder: 'Job Title',
value: jobTitle,
handler: (value) => {
setjobTitle(value)
props.handler(value)
}
})
}

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputJobTitle } from './Input'
export const JobTitleStrategy: MarkStrategy = {
input: MarkInputJobTitle,
render: ({ value }) => <>{value}</>
}

@ -0,0 +1,16 @@
import { MarkType } from '../../types/drawing'
import { MARK_TYPE_CONFIG, MarkInputProps } from './MarkStrategy'
interface MarkInputComponentProps extends MarkInputProps {
markType: MarkType
}
export const MarkInput = ({ markType, ...rest }: MarkInputComponentProps) => {
const { input: InputComponent } = MARK_TYPE_CONFIG[markType] || {}
if (typeof InputComponent !== 'undefined') {
return <InputComponent {...rest} />
}
return null
}

@ -0,0 +1,20 @@
import { MarkType } from '../../types/drawing'
import { MARK_TYPE_CONFIG, MarkRenderProps } from './MarkStrategy'
interface MarkRenderComponentProps extends MarkRenderProps {
markType: MarkType
}
export const MarkRender = ({ markType, ...rest }: MarkRenderComponentProps) => {
const { render: RenderComponent } = MARK_TYPE_CONFIG[markType] || {}
if (typeof RenderComponent !== 'undefined') {
return <RenderComponent {...rest} />
}
return <DefaultRenderComponent {...rest} />
}
const DefaultRenderComponent = ({ value }: MarkRenderProps) => (
<span>{value}</span>
)

@ -0,0 +1,38 @@
import { MarkType } from '../../types/drawing'
import { CurrentUserMark, Mark } from '../../types/mark'
import { TextStrategy } from './Text'
import { SignatureStrategy } from './Signature'
import { FullNameStrategy } from './FullName'
import { JobTitleStrategy } from './JobTitle'
import { DateTimeStrategy } from './DateTime'
export interface MarkInputProps {
value: string
handler: (value: string) => void
placeholder?: string
userMark?: CurrentUserMark
}
export interface MarkRenderProps {
value?: string
mark: Mark
}
export interface MarkStrategy {
input: React.FC<MarkInputProps>
render: React.FC<MarkRenderProps>
encryptAndUpload?: (value: string, key?: string) => Promise<string>
fetchAndDecrypt?: (value: string, key?: string) => Promise<string>
}
export type MarkStrategies = {
[key in MarkType]?: MarkStrategy
}
export const MARK_TYPE_CONFIG: MarkStrategies = {
[MarkType.TEXT]: TextStrategy,
[MarkType.SIGNATURE]: SignatureStrategy,
[MarkType.FULLNAME]: FullNameStrategy,
[MarkType.JOBTITLE]: JobTitleStrategy,
[MarkType.DATETIME]: DateTimeStrategy
}

@ -0,0 +1,44 @@
@import '../../../styles/colors.scss';
$padding: 5px;
.wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: $padding;
}
.relative {
position: relative;
outline: 1px solid black;
}
.canvas {
background-color: $body-background-color;
cursor: crosshair;
// Disable panning/zooming when touching canvas element
-ms-touch-action: none;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
.absolute {
position: absolute;
inset: 0;
pointer-events: none;
}
.reset {
cursor: pointer;
position: absolute;
top: 0;
right: $padding;
color: $primary-main;
&:hover {
color: $primary-dark;
}
}

@ -0,0 +1,101 @@
import { useCallback, useEffect, useRef } from 'react'
import { faEraser } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { MarkRenderSignature } from './Render'
import SignaturePad from 'signature_pad'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils/const'
import { BasicPoint } from 'signature_pad/dist/types/point'
import { MarkInputProps } from '../MarkStrategy'
import styles from './Input.module.scss'
export const MarkInputSignature = ({
value,
handler,
userMark
}: MarkInputProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const signaturePad = useRef<SignaturePad | null>(null)
const update = useCallback(() => {
const data = signaturePad.current?.toData()
const reduced = data?.map((pg) => pg.points)
const json = JSON.stringify(reduced)
if (signaturePad.current && !signaturePad.current?.isEmpty()) {
handler(json)
} else {
handler('')
}
}, [handler])
useEffect(() => {
const handleEndStroke = () => {
update()
}
if (canvasRef.current) {
if (signaturePad.current === null) {
signaturePad.current = new SignaturePad(
canvasRef.current,
SIGNATURE_PAD_OPTIONS
)
}
signaturePad.current.addEventListener('endStroke', handleEndStroke)
}
return () => {
window.removeEventListener('endStroke', handleEndStroke)
}
}, [update])
useEffect(() => {
if (signaturePad.current) {
if (value) {
signaturePad.current.fromData(
JSON.parse(value).map((p: BasicPoint[]) => ({
points: p
}))
)
} else {
signaturePad.current?.clear()
}
}
update()
}, [update, value])
const handleReset = () => {
signaturePad.current?.clear()
update()
}
return (
<div className={styles.wrapper}>
<div
className={styles.relative}
style={{
width: SIGNATURE_PAD_SIZE.width,
height: SIGNATURE_PAD_SIZE.height
}}
>
<canvas
width={SIGNATURE_PAD_SIZE.width}
height={SIGNATURE_PAD_SIZE.height}
ref={canvasRef}
className={styles.canvas}
></canvas>
{typeof userMark?.mark !== 'undefined' && (
<div className={styles.absolute}>
<MarkRenderSignature
key={userMark.mark.value}
value={userMark.mark.value}
mark={userMark.mark}
/>
</div>
)}
<div className={styles.reset}>
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
</div>
</div>
</div>
)
}

@ -0,0 +1,9 @@
.img {
width: 100%;
height: 100%;
object-fit: contain;
overflow: hidden;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}

@ -0,0 +1,27 @@
import { useEffect, useState } from 'react'
import SignaturePad from 'signature_pad'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils'
import { BasicPoint } from 'signature_pad/dist/types/point'
import { MarkRenderProps } from '../MarkStrategy'
import styles from './Render.module.scss'
export const MarkRenderSignature = ({ value }: MarkRenderProps) => {
const [dataUrl, setDataUrl] = useState<string | undefined>()
useEffect(() => {
if (value) {
const canvas = document.createElement('canvas')
canvas.width = SIGNATURE_PAD_SIZE.width
canvas.height = SIGNATURE_PAD_SIZE.height
const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS)
pad.fromData(
JSON.parse(value).map((p: BasicPoint[]) => ({
points: p
}))
)
setDataUrl(canvas.toDataURL('image/webp'))
}
}, [value])
return dataUrl ? <img src={dataUrl} className={styles.img} alt="" /> : null
}

@ -0,0 +1,97 @@
import axios from 'axios'
import {
decryptArrayBuffer,
encryptArrayBuffer,
getHash,
uploadToFileStorage
} from '../../../utils'
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputSignature } from './Input'
import { MarkRenderSignature } from './Render'
export const SignatureStrategy: MarkStrategy = {
input: MarkInputSignature,
render: MarkRenderSignature,
/**
* Encrypts a stringified signature object, creates an encrypted JSON file,
* and uploads it to a file storage if the user is online.
* @param value
* @param encryptionKey
* @returns the original value string
*/
encryptAndUpload: async (value, encryptionKey) => {
// Value is the stringified signature object
// Encode it to the arrayBuffer
const encoder = new TextEncoder()
const uint8Array = encoder.encode(value)
if (!encryptionKey) {
throw new Error('Signature requires an encryption key')
}
// Encrypt the file contents with the same encryption key from the create signature
const encryptedArrayBuffer = await encryptArrayBuffer(
uint8Array,
encryptionKey
)
const hash = await getHash(encryptedArrayBuffer)
if (!hash) {
throw new Error("Can't get encrypted file hash.")
}
// Create the encrypted json file from array buffer and hash
const file = new File([encryptedArrayBuffer], `${hash}.json`)
try {
const urls = await uploadToFileStorage(file)
console.info(
`${file.name} uploaded to following file storages: ${urls.join(', ')}`
)
return value
} catch (error) {
if (error instanceof Error) {
console.error(
`Error occurred in uploading file ${file.name}`,
error.message
)
}
}
return value
},
fetchAndDecrypt: async (value, encryptionKey) => {
if (!encryptionKey) {
throw new Error('Signature requires an encryption key')
}
const encryptedArrayBuffer = await axios.get(value, {
responseType: 'arraybuffer'
})
// Verify hash
const parts = value.split('/')
const urlHash = parts[parts.length - 1]
const hash = await getHash(encryptedArrayBuffer.data)
if (hash !== urlHash) {
// TODO: handle hash verification failing
throw new Error('Unable to verify signature')
}
const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer.data,
encryptionKey
).catch((err) => {
console.log('err in decryption:>> ', err)
return null
})
if (arrayBuffer) {
// decode json
const decoder = new TextDecoder()
return decoder.decode(arrayBuffer)
}
return value
}
}

@ -0,0 +1,19 @@
import { MarkInputProps } from '../MarkStrategy'
import styles from '../../MarkFormField/style.module.scss'
export const MarkInputText = ({
value,
handler,
placeholder
}: MarkInputProps) => {
return (
<input
className={styles.input}
placeholder={placeholder}
onChange={(e) => {
handler(e.currentTarget.value)
}}
value={value}
/>
)
}

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputText } from './Input'
export const TextStrategy: MarkStrategy = {
input: MarkInputText,
render: ({ value }) => <>{value}</>
}

@ -36,6 +36,8 @@ const PdfItem = ({
return file.pages?.map((page, i) => {
return (
<PdfPageItem
fileName={file.name}
pageIndex={i}
page={page}
key={i}
currentUserMarks={filterByPage(currentUserMarks, i)}

@ -2,6 +2,9 @@ import { CurrentUserMark } from '../../types/mark.ts'
import styles from '../DrawPDFFields/style.module.scss'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
import { useScale } from '../../hooks/useScale.tsx'
import { forwardRef } from 'react'
import { npubToHex } from '../../utils/nostr.ts'
import { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
interface PdfMarkItemProps {
userMark: CurrentUserMark
@ -14,35 +17,46 @@ interface PdfMarkItemProps {
/**
* Responsible for display an individual Pdf Mark.
*/
const PdfMarkItem = ({
selectedMark,
handleMarkClick,
selectedMarkValue,
userMark,
pageWidth
}: PdfMarkItemProps) => {
const { location } = userMark.mark
const handleClick = () => handleMarkClick(userMark.mark.id)
const isEdited = () => selectedMark?.mark.id === userMark.mark.id
const getMarkValue = () =>
isEdited() ? selectedMarkValue : userMark.currentValue
const { from } = useScale()
return (
<div
onClick={handleClick}
className={`file-mark ${styles.drawingRectangle} ${isEdited() && styles.edited}`}
style={{
left: inPx(from(pageWidth, location.left)),
top: inPx(from(pageWidth, location.top)),
width: inPx(from(pageWidth, location.width)),
height: inPx(from(pageWidth, location.height)),
fontFamily: FONT_TYPE,
fontSize: inPx(from(pageWidth, FONT_SIZE))
}}
>
{getMarkValue()}
</div>
)
}
const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
(
{ selectedMark, handleMarkClick, selectedMarkValue, userMark, pageWidth },
ref
) => {
const { location } = userMark.mark
const handleClick = () => handleMarkClick(userMark.mark.id)
const isEdited = () => selectedMark?.mark.id === userMark.mark.id
const getMarkValue = () =>
isEdited() ? selectedMarkValue : userMark.currentValue
const { from } = useScale()
return (
<div
ref={ref}
onClick={handleClick}
className={`file-mark ${styles.signingRectangle} ${isEdited() && styles.edited}`}
style={{
backgroundColor: selectedMark?.mark.npub
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
: undefined,
outlineColor: selectedMark?.mark.npub
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}`
: undefined,
left: inPx(from(pageWidth, location.left)),
top: inPx(from(pageWidth, location.top)),
width: inPx(from(pageWidth, location.width)),
height: inPx(from(pageWidth, location.height)),
fontFamily: FONT_TYPE,
fontSize: inPx(from(pageWidth, FONT_SIZE))
}}
>
<MarkRender
key={getMarkValue()}
markType={userMark.mark.type}
value={getMarkValue()}
mark={userMark.mark}
/>
</div>
)
}
)
export default PdfMarkItem

@ -7,7 +7,7 @@ import {
getUpdatedMark,
updateCurrentUserMarks
} from '../../utils'
import { EMPTY } from '../../utils/const.ts'
import { EMPTY } from '../../utils'
import { Container } from '../Container'
import signPageStyles from '../../pages/sign/style.module.scss'
import { CurrentUserFile } from '../../types/file.ts'
@ -15,15 +15,28 @@ import FileList from '../FileList'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { UsersDetails } from '../UsersDetails.tsx'
import { Meta } from '../../types'
import {
faCircleInfo,
faFileDownload,
faPen
} from '@fortawesome/free-solid-svg-icons'
import { Typography } from '@mui/material'
import styles from '../UsersDetails.tsx/style.module.scss'
interface PdfMarkingProps {
currentUserMarks: CurrentUserMark[]
files: CurrentUserFile[]
handleDownload: () => void
/**
* Currently, loading spinner is present if `files` array is of length 0,
* Which means if no files are found, loading spinner will be spinning indefinitely
* For that reason `noFiles` is introduced to set the loading off when fetching is finished.
*/
noFiles?: boolean
handleSign: () => void
handleSignOffline: () => void
meta: Meta | null
otherUserMarks: Mark[]
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
setIsMarksCompleted: (isMarksCompleted: boolean) => void
setUpdatedMarks: (markToUpdate: Mark) => void
}
@ -33,17 +46,17 @@ interface PdfMarkingProps {
* @param props
* @constructor
*/
const PdfMarking = (props: PdfMarkingProps) => {
const {
files,
currentUserMarks,
setIsMarksCompleted,
setCurrentUserMarks,
setUpdatedMarks,
handleDownload,
meta,
otherUserMarks
} = props
const PdfMarking = ({
files,
noFiles,
currentUserMarks,
setCurrentUserMarks,
setUpdatedMarks,
handleSign,
handleSignOffline,
meta,
otherUserMarks
}: PdfMarkingProps) => {
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
@ -65,8 +78,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
const handleMarkClick = (id: number) => {
const nextMark = currentUserMarks.find((mark) => mark.mark.id === id)
setSelectedMark(nextMark!)
setSelectedMarkValue(nextMark?.mark.value ?? EMPTY)
if (nextMark) handleCurrentUserMarkChange(nextMark)
}
const handleCurrentUserMarkChange = (mark: CurrentUserMark) => {
@ -81,39 +94,57 @@ const PdfMarking = (props: PdfMarkingProps) => {
updatedSelectedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMarkValue(mark.currentValue ?? EMPTY)
setSelectedMark(mark)
// If clicking on the same mark, don't update the value, otherwise do update
if (mark.id !== selectedMark.id) {
setSelectedMarkValue(mark.currentValue ?? EMPTY)
setSelectedMark(mark)
}
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
/**
* Sign and Complete
*/
const handleSubmit = (
event: React.MouseEvent<HTMLButtonElement>,
type: 'online' | 'offline'
) => {
event.preventDefault()
if (!selectedMarkValue || !selectedMark) return
if (selectedMarkValue && selectedMark) {
const updatedMark: CurrentUserMark = getUpdatedMark(
selectedMark,
selectedMarkValue
)
const updatedMark: CurrentUserMark = getUpdatedMark(
selectedMark,
selectedMarkValue
)
setSelectedMarkValue(EMPTY)
const updatedCurrentUserMarks = updateCurrentUserMarks(
currentUserMarks,
updatedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(null)
setUpdatedMarks(updatedMark.mark)
}
setSelectedMarkValue(EMPTY)
const updatedCurrentUserMarks = updateCurrentUserMarks(
currentUserMarks,
updatedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(null)
setIsMarksCompleted(true)
setUpdatedMarks(updatedMark.mark)
if (type === 'online') handleSign()
else if (type === 'offline') handleSignOffline()
}
// const updateCurrentUserMarkValues = () => {
// const updatedMark: CurrentUserMark = getUpdatedMark(selectedMark!, selectedMarkValue)
// const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark)
// setSelectedMarkValue(EMPTY)
// setCurrentUserMarks(updatedCurrentUserMarks)
// }
const handleChange = (value: string) => {
setSelectedMarkValue(value)
}
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
setSelectedMarkValue(event.target.value)
const renderRightColumn = () => {
if (meta !== null) return <UsersDetails meta={meta} />
return (
<div className={styles.container}>
<div className={styles.section}>
<Typography>No meta found</Typography>
</div>
</div>
)
}
return (
<>
@ -126,26 +157,33 @@ const PdfMarking = (props: PdfMarkingProps) => {
files={files}
currentFile={currentFile}
setCurrentFile={setCurrentFile}
handleDownload={handleDownload}
/>
)}
</div>
}
right={meta !== null && <UsersDetails meta={meta} />}
right={renderRightColumn()}
leftIcon={faFileDownload}
centerIcon={faPen}
rightIcon={faCircleInfo}
>
{currentUserMarks?.length > 0 && (
<PdfView
currentFile={currentFile}
files={files}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
selectedMark={selectedMark}
currentUserMarks={currentUserMarks}
otherUserMarks={otherUserMarks}
/>
<PdfView
currentFile={currentFile}
files={files}
noFiles={noFiles}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
selectedMark={selectedMark}
currentUserMarks={currentUserMarks}
otherUserMarks={otherUserMarks}
/>
{noFiles && (
<Typography textAlign="center">
We were not able to retrieve the files.
</Typography>
)}
</StickySideColumns>
{selectedMark !== null && (
{!noFiles && (
<MarkFormField
handleSubmit={handleSubmit}
handleSelectedMarkValueChange={handleChange}

@ -6,7 +6,10 @@ import { useEffect, useRef } from 'react'
import pdfViewStyles from './style.module.scss'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
import { useScale } from '../../hooks/useScale.tsx'
import { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
interface PdfPageProps {
fileName: string
pageIndex: number
currentUserMarks: CurrentUserMark[]
handleMarkClick: (id: number) => void
otherUserMarks: Mark[]
@ -19,6 +22,8 @@ interface PdfPageProps {
* Responsible for rendering a single Pdf Page and its Marks
*/
const PdfPageItem = ({
fileName,
pageIndex,
page,
currentUserMarks,
handleMarkClick,
@ -29,7 +34,8 @@ const PdfPageItem = ({
useEffect(() => {
if (selectedMark !== null && !!markRefs.current[selectedMark.id]) {
markRefs.current[selectedMark.id]?.scrollIntoView({
behavior: 'smooth'
behavior: 'smooth',
block: 'center'
})
}
}, [selectedMark])
@ -38,18 +44,21 @@ const PdfPageItem = ({
return (
<div className={`image-wrapper ${styles.pdfImageWrapper}`}>
<img draggable="false" src={page.image} />
<img
draggable="false"
src={page.image}
alt={`page ${pageIndex + 1} of ${fileName}`}
/>
{currentUserMarks.map((m, i) => (
<div key={i} ref={(el) => (markRefs.current[m.id] = el)}>
<PdfMarkItem
key={i}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
userMark={m}
selectedMark={selectedMark}
pageWidth={page.width}
/>
</div>
<PdfMarkItem
key={i}
ref={(el) => (markRefs.current[m.id] = el)}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
userMark={m}
selectedMark={selectedMark}
pageWidth={page.width}
/>
))}
{otherUserMarks.map((m, i) => {
return (
@ -65,7 +74,7 @@ const PdfPageItem = ({
fontSize: inPx(from(page.width, FONT_SIZE))
}}
>
{m.value}
<MarkRender value={m.value} mark={m} markType={m.type} />
</div>
)
})}

@ -4,11 +4,18 @@ import { CurrentUserFile } from '../../types/file.ts'
import { useEffect, useRef } from 'react'
import { FileDivider } from '../FileDivider.tsx'
import React from 'react'
import { LoadingSpinner } from '../LoadingSpinner'
interface PdfViewProps {
currentFile: CurrentUserFile | null
currentUserMarks: CurrentUserMark[]
files: CurrentUserFile[]
/**
* Currently, loading spinner is present if `files` array is of length 0,
* Which means if no files are found, loading spinner will be spinning indefinitely
* For that reason `noFiles` is introduced to set the loading off when fetching is finished.
*/
noFiles?: boolean
handleMarkClick: (id: number) => void
otherUserMarks: Mark[]
selectedMark: CurrentUserMark | null
@ -20,6 +27,7 @@ interface PdfViewProps {
*/
const PdfView = ({
files,
noFiles,
currentUserMarks,
handleMarkClick,
selectedMarkValue,
@ -37,41 +45,53 @@ const PdfView = ({
currentUserMarks: CurrentUserMark[],
hash: string
): CurrentUserMark[] => {
return currentUserMarks.filter(
(currentUserMark) => currentUserMark.mark.pdfFileHash === hash
return currentUserMarks.filter((currentUserMark) =>
currentUserMark.mark.pdfFileHash
? currentUserMark.mark.pdfFileHash === hash
: currentUserMark.mark.fileHash === hash
)
}
const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => {
return marks.filter((mark) => mark.pdfFileHash === hash)
return marks.filter((mark) =>
mark.pdfFileHash ? mark.pdfFileHash === hash : mark.fileHash === hash
)
}
const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean =>
index !== files.length - 1
return (
<div className="files-wrapper">
{files.map((currentUserFile, index, arr) => {
const { hash, file, id } = currentUserFile
{files.length > 0 ? (
files
.map<React.ReactNode>((currentUserFile) => {
const { hash, file, id } = currentUserFile
if (!hash) return
return (
<React.Fragment key={index}>
<div
id={file.name}
className="file-wrapper"
ref={(el) => (pdfRefs.current[id] = el)}
>
<PdfItem
file={file}
currentUserMarks={filterByFile(currentUserMarks, hash)}
selectedMark={selectedMark}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
/>
</div>
{isNotLastPdfFile(index, arr) && <FileDivider />}
</React.Fragment>
)
})}
if (!hash) return
return (
<div
key={`file-${file.name}`}
id={file.name}
className="file-wrapper"
ref={(el) => (pdfRefs.current[id] = el)}
>
<PdfItem
file={file}
currentUserMarks={filterByFile(currentUserMarks, hash)}
selectedMark={selectedMark}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
/>
</div>
)
})
.reduce((prev, curr, i) => [
prev,
<FileDivider key={`separator-${i}`} />,
curr
])
) : noFiles ? (
''
) : (
<LoadingSpinner variant="small" />
)}
</div>
)
}

@ -6,8 +6,6 @@
.otherUserMarksDisplay {
position: absolute;
z-index: 50;
z-index: 40;
display: flex;
justify-content: center;
align-items: center;
}

@ -3,34 +3,56 @@ import { getProfileRoute } from '../../routes'
import styles from './styles.module.scss'
import { AvatarIconButton } from '../UserAvatarIconButton'
import { Link } from 'react-router-dom'
import { useProfileMetadata } from '../../hooks/useProfileMetadata'
import { Tooltip } from '@mui/material'
import { getProfileUsername } from '../../utils'
import { TooltipChild } from '../TooltipChild'
interface UserAvatarProps {
name?: string
pubkey: string
image?: string
isNameVisible?: boolean
}
/**
* This component will be used for the displaying username and profile picture.
* Clicking will navigate to the user's profile.
*/
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
export const UserAvatar = ({
pubkey,
isNameVisible = false
}: UserAvatarProps) => {
const profile = useProfileMetadata(pubkey)
const name = getProfileUsername(pubkey, profile)
const image = profile?.image
return (
<Link
to={getProfileRoute(pubkey)}
className={styles.container}
tabIndex={-1}
>
<AvatarIconButton
src={image}
hexKey={pubkey}
aria-label={`account of user ${name || pubkey}`}
color="inherit"
sx={{
padding: 0
}}
/>
{name ? <span className={styles.username}>{name}</span> : null}
<Tooltip
key={pubkey}
title={name}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<AvatarIconButton
src={image}
hexKey={pubkey}
aria-label={`account of user ${name}`}
color="inherit"
sx={{
padding: 0
}}
/>
</TooltipChild>
</Tooltip>
{isNameVisible && name ? (
<span className={styles.username}>{name}</span>
) : null}
</Link>
)
}

@ -1,11 +1,10 @@
import { Divider, Tooltip } from '@mui/material'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import {
formatTimestamp,
fromUnixTimestamp,
hexToNpub,
npubToHex,
shorten,
SigitStatus,
SignStatus
} from '../../utils'
import { useSigitMeta } from '../../hooks/useSigitMeta'
@ -17,17 +16,19 @@ import {
faCalendar,
faCalendarCheck,
faCalendarPlus,
faCheck,
faClock,
faEye,
faServer,
faFile,
faFileCircleExclamation
} from '@fortawesome/free-solid-svg-icons'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSelector } from 'react-redux'
import { State } from '../../store/rootReducer'
import { TooltipChild } from '../TooltipChild'
import { useAppSelector } from '../../hooks/store'
import { DisplaySigner } from '../DisplaySigner'
import { Meta } from '../../types'
import { Meta, OpenTimestamp } from '../../types'
import { extractFileExtensions } from '../../utils/file'
import { UserAvatar } from '../UserAvatar'
interface UsersDetailsProps {
meta: Meta
@ -36,58 +37,94 @@ interface UsersDetailsProps {
export const UsersDetails = ({ meta }: UsersDetailsProps) => {
const {
submittedBy,
exportedBy,
signers,
viewers,
fileHashes,
zipUrls,
signersStatus,
createdAt,
completedAt,
parsedSignatureEvents,
signedStatus,
isValid
isValid,
id,
timestamps
} = useSigitMeta(meta)
const { usersPubkey } = useSelector((state: State) => state.auth)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers,
...viewers
])
const { usersPubkey } = useAppSelector((state) => state.auth)
const userCanSign =
typeof usersPubkey !== 'undefined' &&
signers.includes(hexToNpub(usersPubkey))
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
const isTimestampVerified = (
timestamps: OpenTimestamp[],
nostrId: string
): boolean => {
const matched = timestamps.find((t) => t.nostrId === nostrId)
return !!(matched && matched.verification)
}
const getOpenTimestampsInfo = (
timestamps: OpenTimestamp[],
nostrId: string
) => {
if (isTimestampVerified(timestamps, nostrId)) {
return <FontAwesomeIcon className={styles.ticket} icon={faCheck} />
} else {
return <FontAwesomeIcon className={styles.ticket} icon={faClock} />
}
}
const getCompletedOpenTimestampsInfo = (timestamp: OpenTimestamp) => {
if (timestamp.verification) {
return <FontAwesomeIcon className={styles.ticket} icon={faCheck} />
} else {
return <FontAwesomeIcon className={styles.ticket} icon={faClock} />
}
}
const getTimestampTooltipTitle = (label: string, isVerified: boolean) => {
return `${label} / Open Timestamp ${isVerified ? 'Verified' : 'Pending'}`
}
const isUserSignatureTimestampVerified = () => {
if (
userCanSign &&
hexToNpub(usersPubkey) in parsedSignatureEvents &&
timestamps &&
timestamps.length > 0
) {
const nostrId = parsedSignatureEvents[hexToNpub(usersPubkey)].id
return isTimestampVerified(timestamps, nostrId)
}
return false
}
/**
* Used to parse the base URL from Blossom server full path
*/
const getBaseUrl = (url: string): string => {
try {
const parsedUrl = new URL(url)
return `${parsedUrl.protocol}//${parsedUrl.host}`
} catch (error) {
return 'Invalid URL'
}
}
return submittedBy ? (
<div className={styles.container}>
<div className={styles.section}>
<p>Signers</p>
<div className={styles.users}>
{submittedBy &&
(function () {
const profile = profiles[submittedBy]
return (
<Tooltip
key={submittedBy}
title={
profile?.display_name ||
profile?.name ||
shorten(hexToNpub(submittedBy))
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
profile={profile}
pubkey={submittedBy}
/>
</TooltipChild>
</Tooltip>
)
})()}
{submittedBy && (
<DisplaySigner
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
pubkey={submittedBy}
/>
)}
{submittedBy && signers.length ? (
<Divider orientation="vertical" flexItem />
@ -96,72 +133,80 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<UserAvatarGroup max={20}>
{signers.map((signer) => {
const pubkey = npubToHex(signer)!
const profile = profiles[pubkey]
return (
<Tooltip
key={signer}
title={
profile?.display_name || profile?.name || shorten(pubkey)
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={signersStatus[signer]}
profile={profile}
pubkey={pubkey}
/>
</TooltipChild>
</Tooltip>
)
})}
{viewers.map((signer) => {
const pubkey = npubToHex(signer)!
const profile = profiles[pubkey]
return (
<Tooltip
key={signer}
title={
profile?.display_name || profile?.name || shorten(pubkey)
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={SignStatus.Viewer}
profile={profile}
pubkey={pubkey}
/>
</TooltipChild>
</Tooltip>
<DisplaySigner
key={pubkey}
status={signersStatus[signer]}
pubkey={pubkey}
/>
)
})}
</UserAvatarGroup>
</div>
{viewers.length > 0 && (
<>
<p>Viewers</p>
<div className={styles.users}>
<UserAvatarGroup max={20}>
{viewers.map((signer) => {
const pubkey = npubToHex(signer)!
return (
<DisplaySigner
key={pubkey}
status={SignStatus.Viewer}
pubkey={pubkey}
/>
)
})}
</UserAvatarGroup>
</div>
</>
)}
{exportedBy && (
<>
<p>Exported By</p>
<div className={styles.users}>
<UserAvatar pubkey={exportedBy} />
</div>
</>
)}
</div>
<div className={styles.section}>
<p>Details</p>
<Tooltip
title={'Publication date'}
title={getTimestampTooltipTitle(
'Publication date',
!!(timestamps && id && isTimestampVerified(timestamps, id))
)}
placement="top"
arrow
disableInteractive
>
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
{createdAt ? formatTimestamp(createdAt) : <>&mdash;</>}
{createdAt ? formatTimestamp(createdAt) : <>&mdash;</>}{' '}
{timestamps &&
timestamps.length > 0 &&
id &&
getOpenTimestampsInfo(timestamps, id)}
</span>
</Tooltip>
<Tooltip
title={'Completion date'}
title={getTimestampTooltipTitle(
'Completion date',
!!(
signedStatus === SigitStatus.Complete &&
completedAt &&
timestamps &&
timestamps.length > 0 &&
timestamps[timestamps.length - 1].verification
)
)}
placement="top"
arrow
disableInteractive
@ -169,13 +214,26 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
{completedAt ? formatTimestamp(completedAt) : <>&mdash;</>}
{signedStatus === SigitStatus.Complete &&
completedAt &&
timestamps &&
timestamps.length > 0 && (
<span className={styles.ticket}>
{getCompletedOpenTimestampsInfo(
timestamps[timestamps.length - 1]
)}
</span>
)}
</span>
</Tooltip>
{/* User signed date */}
{userCanSign ? (
<Tooltip
title={'Your signature date'}
title={getTimestampTooltipTitle(
'Your signature date',
isUserSignatureTimestampVerified()
)}
placement="top"
arrow
disableInteractive
@ -195,6 +253,16 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
) : (
<>&mdash;</>
)}
{hexToNpub(usersPubkey) in parsedSignatureEvents &&
timestamps &&
timestamps.length > 0 && (
<span className={styles.ticket}>
{getOpenTimestampsInfo(
timestamps,
parsedSignatureEvents[hexToNpub(usersPubkey)].id
)}
</span>
)}
</span>
</Tooltip>
) : null}
@ -216,6 +284,20 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<FontAwesomeIcon icon={faFileCircleExclamation} /> &mdash;
</>
)}
<span className={styles.detailsItem}>
<FontAwesomeIcon icon={faEye} /> {signedStatus}
</span>
</div>
<div className={styles.section}>
<p>File Servers</p>
{zipUrls &&
zipUrls.map((url) => (
<span className={styles.detailsItem} key={url}>
<FontAwesomeIcon icon={faServer} /> {getBaseUrl(url)}
</span>
))}
</div>
</div>
) : undefined

@ -31,8 +31,6 @@
padding: 5px;
display: flex;
align-items: center;
justify-content: start;
> :first-child {
padding: 5px;
@ -44,3 +42,7 @@
color: white;
}
}
.ticket {
margin-left: auto;
}

@ -1,6 +1,5 @@
import { Typography } from '@mui/material'
import { useSelector } from 'react-redux'
import { State } from '../store/rootReducer'
import { useAppSelector } from '../hooks/store'
import styles from './username.module.scss'
import { AvatarIconButton } from './UserAvatarIconButton'
@ -16,7 +15,7 @@ type Props = {
* Clicking will open the menu.
*/
const Username = ({ username, avatarContent, handleClick }: Props) => {
const hexKey = useSelector((state: State) => state.auth.usersPubkey)
const hexKey = useAppSelector((state) => state.auth.usersPubkey)
return (
<div className={styles.container}>

299
src/contexts/NDKContext.tsx Normal file

@ -0,0 +1,299 @@
import NDK, {
getRelayListForUser,
Hexpubkey,
NDKEvent,
NDKFilter,
NDKRelayList,
NDKRelaySet,
NDKSubscriptionCacheUsage,
NDKSubscriptionOptions,
NDKUser,
NDKUserProfile
} from '@nostr-dev-kit/ndk'
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
import { Dexie } from 'dexie'
import { createContext, ReactNode, useEffect, useMemo } from 'react'
import { toast } from 'react-toastify'
import { UserRelaysType } from '../types'
import {
DEFAULT_LOOK_UP_RELAY_LIST,
hexToNpub,
orderEventsChronologically,
SIGIT_RELAY,
timeout
} from '../utils'
export interface NDKContextType {
ndk: NDK
fetchEvents: (
filter: NDKFilter,
opts?: NDKSubscriptionOptions
) => Promise<NDKEvent[]>
fetchEvent: (
filter: NDKFilter,
opts?: NDKSubscriptionOptions
) => Promise<NDKEvent | null>
fetchEventsFromUserRelays: (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType,
opts?: NDKSubscriptionOptions
) => Promise<NDKEvent[]>
fetchEventFromUserRelays: (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType,
opts?: NDKSubscriptionOptions
) => Promise<NDKEvent | null>
findMetadata: (
pubkey: string,
opts?: NDKSubscriptionOptions
) => Promise<NDKUserProfile | null>
getNDKRelayList: (pubkey: Hexpubkey) => Promise<NDKRelayList>
publish: (event: NDKEvent, explicitRelayUrls?: string[]) => Promise<string[]>
}
// Create the context with an initial value of `null`
export const NDKContext = createContext<NDKContextType | null>(null)
// Create a provider component to wrap around parts of your app
export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
window.onunhandledrejection = async (event: PromiseRejectionEvent) => {
event.preventDefault()
if (event.reason?.name === Dexie.errnames.DatabaseClosed) {
console.log(
'Could not open Dexie DB, probably version change. Deleting old DB and reloading...'
)
await Dexie.delete('degmod-db')
// Must reload to open a brand new DB
window.location.reload()
}
}
}, [])
const ndk = useMemo(() => {
if (import.meta.env.MODE === 'development') {
localStorage.setItem('debug', '*')
}
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'sigit-db' })
dexieAdapter.locking = true
const ndk = new NDK({
enableOutboxModel: true,
autoConnectUserRelays: true,
autoFetchUserMutelist: true,
explicitRelayUrls: [...DEFAULT_LOOK_UP_RELAY_LIST],
cacheAdapter: dexieAdapter
})
ndk.connect()
return ndk
}, [])
/**
* Asynchronously retrieves multiple event based on a provided filter.
*
* @param filter - The filter criteria to find the event.
* @returns Returns a promise that resolves to the found event or null if not found.
*/
const fetchEvents = async (
filter: NDKFilter,
opts?: NDKSubscriptionOptions
): Promise<NDKEvent[]> => {
return ndk
.fetchEvents(filter, {
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
...opts
})
.then((ndkEventSet) => {
const ndkEvents = Array.from(ndkEventSet)
return orderEventsChronologically(ndkEvents)
})
.catch((err) => {
// Log the error and show a notification if fetching fails
console.error('An error occurred in fetching events', err)
toast.error('An error occurred in fetching events') // Show error notification
return [] // Return an empty array in case of an error
})
}
/**
* Asynchronously retrieves an event based on a provided filter.
*
* @param filter - The filter criteria to find the event.
* @returns Returns a promise that resolves to the found event or null if not found.
*/
const fetchEvent = async (
filter: NDKFilter,
opts?: NDKSubscriptionOptions
) => {
const events = await fetchEvents(filter, opts)
if (events.length === 0) return null
return events[0]
}
/**
* Asynchronously retrieves multiple events from the user's relays based on a specified filter.
* The function first retrieves the user's relays, and then fetches the events using the provided filter.
*
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
* @param hexKey - The hexadecimal representation of the user's public key.
* @param userRelaysType - The type of relays to search (e.g., write, read).
* @returns A promise that resolves with an array of events.
*/
const fetchEventsFromUserRelays = async (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType,
opts?: NDKSubscriptionOptions
): Promise<NDKEvent[]> => {
// Find the user's relays (10s timeout).
const relayUrls = await Promise.race([
getRelayListForUser(hexKey, ndk),
timeout(3000)
])
.then((ndkRelayList) => {
if (ndkRelayList) return ndkRelayList[userRelaysType]
return [] // Return an empty array if ndkRelayList is undefined
})
.catch((err) => {
console.error(
`An error occurred in fetching user's (${hexKey}) ${userRelaysType}`,
err
)
return [] as string[]
})
if (!relayUrls.includes(SIGIT_RELAY)) {
relayUrls.push(SIGIT_RELAY)
}
return ndk
.fetchEvents(
filter,
{
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
...opts
},
relayUrls.length
? NDKRelaySet.fromRelayUrls(relayUrls, ndk, true)
: undefined
)
.then((ndkEventSet) => {
const ndkEvents = Array.from(ndkEventSet)
return orderEventsChronologically(ndkEvents)
})
.catch((err) => {
// Log the error and show a notification if fetching fails
console.error('An error occurred in fetching events', err)
toast.error('An error occurred in fetching events') // Show error notification
return [] // Return an empty array in case of an error
})
}
/**
* Fetches an event from the user's relays based on a specified filter.
* The function first retrieves the user's relays, and then fetches the event using the provided filter.
*
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
* @param hexKey - The hexadecimal representation of the user's public key.
* @param userRelaysType - The type of relays to search (e.g., write, read).
* @returns A promise that resolves to the fetched event or null if the operation fails.
*/
const fetchEventFromUserRelays = async (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType,
opts?: NDKSubscriptionOptions
) => {
const events = await fetchEventsFromUserRelays(
filter,
hexKey,
userRelaysType,
opts
)
if (events.length === 0) return null
return events[0]
}
/**
* Finds metadata for a given pubkey.
*
* @param hexKey - The pubkey to search for metadata.
* @returns A promise that resolves to the metadata event.
*/
const findMetadata = async (
pubkey: string,
opts?: NDKSubscriptionOptions
): Promise<NDKUserProfile | null> => {
const npub = hexToNpub(pubkey)
const user = new NDKUser({ npub })
user.ndk = ndk
return await user.fetchProfile({
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
...(opts || {})
})
}
const getNDKRelayList = async (pubkey: Hexpubkey) => {
const ndkRelayList = await Promise.race([
getRelayListForUser(pubkey, ndk),
timeout(10000)
]).catch(() => {
const relayList = new NDKRelayList(ndk)
relayList.bothRelayUrls = [SIGIT_RELAY]
return relayList
})
return ndkRelayList
}
const publish = async (
event: NDKEvent,
explicitRelayUrls?: string[]
): Promise<string[]> => {
if (!event.sig) throw new Error('Before publishing first sign the event!')
let ndkRelaySet: NDKRelaySet | undefined
if (explicitRelayUrls && explicitRelayUrls.length > 0) {
if (!explicitRelayUrls.includes(SIGIT_RELAY)) {
explicitRelayUrls = [...explicitRelayUrls, SIGIT_RELAY]
}
ndkRelaySet = NDKRelaySet.fromRelayUrls(explicitRelayUrls, ndk)
}
return await Promise.race([event.publish(ndkRelaySet), timeout(3000)])
.then((res) => {
const relaysPublishedOn = Array.from(res)
return relaysPublishedOn.map((relay) => relay.url)
})
.catch((err) => {
console.error(`An error occurred in publishing event`, err)
return []
})
}
return (
<NDKContext.Provider
value={{
ndk,
fetchEvents,
fetchEvent,
fetchEventsFromUserRelays,
fetchEventFromUserRelays,
findMetadata,
getNDKRelayList,
publish
}}
>
{children}
</NDKContext.Provider>
)
}

@ -1,139 +0,0 @@
import { EventTemplate } from 'nostr-tools'
import { MetadataController, NostrController } from '.'
import { appPrivateRoutes } from '../routes'
import {
setAuthState,
setMetadataEvent,
setRelayMapAction
} from '../store/actions'
import store from '../store/store'
import { SignedEvent } from '../types'
import {
base64DecodeAuthToken,
base64EncodeSignedEvent,
compareObjects,
getAuthToken,
getRelayMap,
getVisitedLink,
saveAuthToken,
unixNow
} from '../utils'
export class AuthController {
private nostrController: NostrController
private metadataController: MetadataController
constructor() {
this.nostrController = NostrController.getInstance()
this.metadataController = new MetadataController()
}
/**
* Function will authenticate user by signing an auth event
* which is done by calling the sign() function, where appropriate
* method will be chosen (extension, nsecbunker or keys)
*
* @param pubkey of the user trying to login
* @returns url to redirect if authentication successfull
* or error if otherwise
*/
async authAndGetMetadataAndRelaysMap(pubkey: string) {
const emptyMetadata = this.metadataController.getEmptyMetadataEvent()
this.metadataController
.findMetadata(pubkey)
.then((event) => {
if (event) {
store.dispatch(setMetadataEvent(event))
} else {
store.dispatch(setMetadataEvent(emptyMetadata))
}
})
.catch((err) => {
console.warn('Error occurred while finding metadata', err)
store.dispatch(setMetadataEvent(emptyMetadata))
})
// Nostr uses unix timestamps
const timestamp = unixNow()
const { hostname } = window.location
const authEvent: EventTemplate = {
kind: 27235,
tags: [],
content: `${hostname}-${timestamp}`,
created_at: timestamp
}
const signedAuthEvent = await this.nostrController.signEvent(authEvent)
this.createAndSaveAuthToken(signedAuthEvent)
store.dispatch(
setAuthState({
loggedIn: true,
usersPubkey: pubkey
})
)
const relayMap = await getRelayMap(pubkey)
if (Object.keys(relayMap).length < 1) {
// Navigate user to relays page if relay map is empty
return Promise.resolve(appPrivateRoutes.relays)
}
if (store.getState().auth?.loggedIn) {
if (!compareObjects(store.getState().relays?.map, relayMap.map))
store.dispatch(setRelayMapAction(relayMap.map))
}
const currentLocation = window.location.hash.replace('#', '')
if (!Object.values(appPrivateRoutes).includes(currentLocation)) {
// User did change the location to one of the private routes
const visitedLink = getVisitedLink()
if (visitedLink) {
const { pathname, search } = visitedLink
return Promise.resolve(`${pathname}${search}`)
} else {
// Navigate user in
return Promise.resolve(appPrivateRoutes.homePage)
}
}
}
checkSession() {
const savedAuthToken = getAuthToken()
if (savedAuthToken) {
const signedEvent = base64DecodeAuthToken(savedAuthToken)
store.dispatch(
setAuthState({
loggedIn: true,
usersPubkey: signedEvent.pubkey
})
)
return
}
store.dispatch(
setAuthState({
loggedIn: false,
usersPubkey: undefined
})
)
}
private createAndSaveAuthToken(signedAuthEvent: SignedEvent) {
const base64Encoded = base64EncodeSignedEvent(signedAuthEvent)
// save newly created auth token (base64 nostr singed event) in local storage along with expiry time
saveAuthToken(base64Encoded)
return base64Encoded
}
}

@ -1,212 +0,0 @@
import {
Event,
Filter,
VerifiedEvent,
kinds,
validateEvent,
verifyEvent
} from 'nostr-tools'
import { toast } from 'react-toastify'
import { EventEmitter } from 'tseep'
import { NostrController, relayController } from '.'
import { localCache } from '../services'
import { ProfileMetadata, RelaySet } from '../types'
import {
findRelayListAndUpdateCache,
findRelayListInCache,
getDefaultRelaySet,
getUserRelaySet,
isOlderThanOneDay,
unixNow
} from '../utils'
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'
export class MetadataController extends EventEmitter {
private nostrController: NostrController
private specialMetadataRelay = 'wss://purplepag.es'
private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches
constructor() {
super()
this.nostrController = NostrController.getInstance()
}
/**
* Asynchronously checks for more recent metadata events authored by a specific key.
* If a more recent metadata event is found, it is handled and returned.
* If no more recent event is found, the current event is returned.
* @param hexKey The hexadecimal key of the author to filter metadata events.
* @param currentEvent The current metadata event, if any, to compare with newer events.
* @returns A promise resolving to the most recent metadata event found, or null if none is found.
*/
private async checkForMoreRecentMetadata(
hexKey: string,
currentEvent: Event | null
): Promise<Event | null> {
// Return the ongoing fetch promise if one exists for the same hexKey
if (this.pendingFetches.has(hexKey)) {
return this.pendingFetches.get(hexKey)!
}
// Define the event filter to only include metadata events authored by the given key
const eventFilter: Filter = {
kinds: [kinds.Metadata],
authors: [hexKey]
}
const fetchPromise = relayController
.fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST)
.catch((err) => {
console.error(err)
return null
})
.finally(() => {
this.pendingFetches.delete(hexKey)
})
this.pendingFetches.set(hexKey, fetchPromise)
const metadataEvent = await fetchPromise
if (
metadataEvent &&
validateEvent(metadataEvent) &&
verifyEvent(metadataEvent)
) {
if (
!currentEvent ||
metadataEvent.created_at >= currentEvent.created_at
) {
this.handleNewMetadataEvent(metadataEvent)
}
return metadataEvent
}
// todo/implement: if no valid metadata event is found in DEFAULT_LOOK_UP_RELAY_LIST
// try to query user relay list
// if current event is null we should cache empty metadata event for provided hexKey
if (!currentEvent) {
const emptyMetadata = this.getEmptyMetadataEvent(hexKey)
this.handleNewMetadataEvent(emptyMetadata as VerifiedEvent)
}
return currentEvent
}
/**
* Handle new metadata events and emit them to subscribers
*/
private async handleNewMetadataEvent(event: VerifiedEvent) {
// update the event in local cache
localCache.addUserMetadata(event)
// Emit the event to subscribers.
this.emit(event.pubkey, event.kind, event)
}
/**
* Finds metadata for a given hexadecimal key.
*
* @param hexKey - The hexadecimal key to search for metadata.
* @returns A promise that resolves to the metadata event.
*/
public findMetadata = async (hexKey: string): Promise<Event | null> => {
// Attempt to retrieve the metadata event from the local cache
const cachedMetadataEvent = await localCache.getUserMetadata(hexKey)
// If cached metadata is found, check its validity
if (cachedMetadataEvent) {
// Check if the cached metadata is older than one day
if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) {
// If older than one week, find the metadata from relays in background
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
}
// Return the cached metadata event
return cachedMetadataEvent.event
}
// If no cached metadata is found, retrieve it from relays
return this.checkForMoreRecentMetadata(hexKey, null)
}
/**
* Based on the hexKey of the current user, this method attempts to retrieve a relay set.
* @func findRelayListInCache first checks if there is already an up-to-date
* relay list available in cache; if not -
* @func findRelayListAndUpdateCache checks if the relevant relay event is available from
* the purple pages relay;
* @func findRelayListAndUpdateCache will run again if the previous two calls return null and
* check if the relevant relay event can be obtained from 'most popular relays'
* If relay event is found, it will be saved in cache for future use
* @param hexKey of the current user
* @return RelaySet which will contain either relays extracted from the user Relay Event
* or a fallback RelaySet with Sigit's Relay
*/
public findRelayListMetadata = async (hexKey: string): Promise<RelaySet> => {
const relayEvent =
(await findRelayListInCache(hexKey)) ||
(await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, hexKey))
return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet()
}
public extractProfileMetadataContent = (event: Event) => {
try {
if (!event.content) return {}
return JSON.parse(event.content) as ProfileMetadata
} catch (error) {
console.log('error in parsing metadata event content :>> ', error)
return null
}
}
/**
* Function will not sign provided event if the SIG exists
*/
public publishMetadataEvent = async (event: Event) => {
let signedMetadataEvent = event
if (event.sig.length < 1) {
const timestamp = unixNow()
// Metadata event to publish to the wss://purplepag.es relay
const newMetadataEvent: Event = {
...event,
created_at: timestamp
}
signedMetadataEvent =
await this.nostrController.signEvent(newMetadataEvent)
}
await relayController
.publish(signedMetadataEvent, [this.specialMetadataRelay])
.then((relays) => {
if (relays.length) {
toast.success(`Metadata event published on: ${relays.join('\n')}`)
this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent)
} else {
toast.error('Could not publish metadata event to any relay!')
}
})
.catch((err) => {
toast.error(err.message)
})
}
public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
public getEmptyMetadataEvent = (pubkey?: string): Event => {
return {
content: '',
created_at: new Date().valueOf(),
id: '',
kind: 0,
pubkey: pubkey || '',
sig: '',
tags: []
}
}
}

@ -1,194 +1,20 @@
import NDK, {
NDKEvent,
NDKNip46Signer,
NDKPrivateKeySigner,
NDKUser,
NostrEvent
} from '@nostr-dev-kit/ndk'
import {
Event,
EventTemplate,
UnsignedEvent,
finalizeEvent,
nip04,
nip19,
nip44
} from 'nostr-tools'
import { EventTemplate, UnsignedEvent } from 'nostr-tools'
import { EventEmitter } from 'tseep'
import { updateNsecbunkerPubkey } from '../store/actions'
import { AuthState, LoginMethods } from '../store/auth/types'
import store from '../store/store'
import { SignedEvent } from '../types'
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
import { LoginMethodContext } from '../services/LoginMethodStrategy/loginMethodContext'
import { clear, unixNow } from '../utils'
import { LoginMethod } from '../store/auth/types'
import { logout as nostrLogout } from 'nostr-login'
import { userLogOutAction } from '../store/actions'
export class NostrController extends EventEmitter {
private static instance: NostrController
private bunkerNDK: NDK | undefined
private remoteSigner: NDKNip46Signer | undefined
private constructor() {
super()
}
private getNostrObject = () => {
// fix: this is not picking up type declaration from src/system/index.d.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (window.nostr) return window.nostr as any
throw new Error(
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
)
}
public nsecBunkerInit = async (relays: string[]) => {
// Don't reinstantiate bunker NDK if exists with same relays
if (
this.bunkerNDK &&
this.bunkerNDK.explicitRelayUrls?.length === relays.length &&
this.bunkerNDK.explicitRelayUrls?.every((relay) => relays.includes(relay))
)
return
this.bunkerNDK = new NDK({
explicitRelayUrls: relays
})
try {
await this.bunkerNDK
.connect(2000)
.then(() => {
console.log(
`Successfully connected to the nsecBunker relays: ${relays.join(
','
)}`
)
})
.catch((err) => {
console.error(
`Error connecting to the nsecBunker relays: ${relays.join(
','
)} ${err}`
)
})
} catch (err) {
console.error(err)
}
}
/**
* Creates nSecBunker signer instance for the given npub
* Or if npub omitted it will return existing signer
* If neither, error will be thrown
* @param npub nPub / public key in hex format
* @returns nsecBunker Signer instance
*/
public createNsecBunkerSigner = async (
npub: string | undefined
): Promise<NDKNip46Signer> => {
const nsecBunkerDelegatedKey = getNsecBunkerDelegatedKey()
return new Promise((resolve, reject) => {
if (!nsecBunkerDelegatedKey) {
reject('nsecBunker delegated key is not found in the browser.')
return
}
const localSigner = new NDKPrivateKeySigner(nsecBunkerDelegatedKey)
if (!npub) {
if (this.remoteSigner) resolve(this.remoteSigner)
const npubFromStorage = (store.getState().auth as AuthState)
.nsecBunkerPubkey
if (npubFromStorage) {
npub = npubFromStorage
} else {
reject(
'No signer instance present, no npub provided by user or found in the browser.'
)
return
}
} else {
store.dispatch(updateNsecbunkerPubkey(npub))
}
// Pubkey of a key pair stored in nsecbunker that will be used to sign event with
const appPubkeyOrToken = npub.includes('npub')
? npub
: nip19.npubEncode(npub)
/**
* When creating and NDK instance we create new connection to the relay
* To prevent too much connections and hitting rate limits, if npub against which we sign
* we will reuse existing instance. Otherwise we will create new NDK and signer instance.
*/
if (!this.remoteSigner || this.remoteSigner?.remotePubkey !== npub) {
this.remoteSigner = new NDKNip46Signer(
this.bunkerNDK!,
appPubkeyOrToken,
localSigner
)
}
/**
* when nsecbunker-delegated-key is regenerated we have to reinitialize the remote signer
*/
if (this.remoteSigner.localSigner !== localSigner) {
this.remoteSigner = new NDKNip46Signer(
this.bunkerNDK!,
appPubkeyOrToken,
localSigner
)
}
resolve(this.remoteSigner)
})
}
/**
* Signs the nostr event and returns the sig and id or full raw nostr event
* @param npub stored in nsecBunker to sign with
* @param event to be signed
* @param returnFullEvent whether to return full raw nostr event or just SIG and ID values
*/
public signWithNsecBunker = async (
npub: string | undefined,
event: NostrEvent,
returnFullEvent = true
): Promise<{ id: string; sig: string } | NostrEvent> => {
return new Promise((resolve, reject) => {
this.createNsecBunkerSigner(npub)
.then(async (signer) => {
const ndkEvent = new NDKEvent(undefined, event)
const timeout = setTimeout(() => {
reject('Timeout occurred while waiting for event signing')
}, 60000) // 60000 ms (1 min) = 1000 * 60
await ndkEvent.sign(signer).catch((err) => {
clearTimeout(timeout)
reject(err)
return
})
clearTimeout(timeout)
if (returnFullEvent) {
resolve(ndkEvent.rawEvent())
} else {
resolve({
id: ndkEvent.id,
sig: ndkEvent.sig!
})
}
})
.catch((err) => {
reject(err)
})
})
}
public static getInstance(): NostrController {
if (!NostrController.instance) {
NostrController.instance = new NostrController()
@ -206,60 +32,11 @@ export class NostrController extends EventEmitter {
*/
nip44Encrypt = async (receiver: string, content: string) => {
// Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod
const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
// Handle encryption when the login method is via an extension.
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 encryption.
if (!nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Encrypt the content using NIP-44 provided by the nostr extension.
const encrypted = await nostr.nip44.encrypt(receiver, content)
return encrypted as string
}
// Handle encryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
receiver
)
// Encrypt the content using the generated conversation key.
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
return encrypted
}
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) {
throw new Error(
`nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
)
}
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined')
return await context.nip44Encrypt(receiver, content)
}
/**
@ -272,180 +49,48 @@ export class NostrController extends EventEmitter {
*/
nip44Decrypt = async (sender: string, content: string) => {
// Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod
const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
// Handle decryption when the login method is via an extension.
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 decryption.
if (!nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Decrypt the content using NIP-44 provided by the nostr extension.
const decrypted = await nostr.nip44.decrypt(sender, content)
return decrypted as string
}
// Handle decryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
sender
)
// Decrypt the content using the generated conversation key.
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
return decrypted
}
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) {
throw new Error(
`nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
)
}
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined')
// Handle decryption
return await context.nip44Decrypt(sender, content)
}
/**
* Signs an event with private key (if it is present in local storage) or
* with browser extension (if it is present) or
* with nSecBunker instance.
* with browser extension (if it is present)
* @param event - unsigned nostr event.
* @returns - a promised that is resolved with signed nostr event.
*/
signEvent = async (
event: UnsignedEvent | EventTemplate
): Promise<SignedEvent> => {
const loginMethod = (store.getState().auth as AuthState).loginMethod
const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
if (!loginMethod) {
return Promise.reject('No login method found in the browser storage')
const authkey = store.getState().auth.usersPubkey
const signedEvent = await context.signEvent(event)
const pubkey = signedEvent.pubkey
// Forcefully log out the user if we detect missmatch between pubkeys
// Allow undefined authkey, intial log in
if (authkey && authkey !== pubkey) {
if (loginMethod === LoginMethod.nostrLogin) {
nostrLogout()
}
store.dispatch(userLogOutAction())
clear()
throw new Error('User missmatch.\n\nPlease log in again.')
}
if (loginMethod === LoginMethods.nsecBunker) {
// Check if nsecBunker is available
if (!this.bunkerNDK) {
return Promise.reject(
`Login method is ${loginMethod} but bunkerNDK is not created`
)
}
if (!this.remoteSigner) {
return Promise.reject(
`Login method is ${loginMethod} but bunkerNDK is not created`
)
}
const signedEvent = await this.signWithNsecBunker(
'',
event as NostrEvent
).catch((err) => {
throw err
})
return Promise.resolve(signedEvent as SignedEvent)
} else if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
return Promise.reject(
`Login method is ${loginMethod}, but keys are not found`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const signedEvent = finalizeEvent(event, privateKey)
verifySignedEvent(signedEvent)
return Promise.resolve(signedEvent)
} else if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
return (await nostr
.signEvent(event as NostrEvent)
.catch((err: unknown) => {
console.log('Error while signing event: ', err)
throw err
})) as Event
} else {
return Promise.reject(
`We could not sign the event, none of the signing methods are available`
)
}
return signedEvent
}
nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
const loginMethod = (store.getState().auth as AuthState).loginMethod
const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
if (!nostr.nip04) {
throw new Error(
`Your nostr extension does not support nip04 encryption & decryption`
)
}
const encrypted = await nostr.nip04.encrypt(receiver, content)
return encrypted
}
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const encrypted = await nip04.encrypt(privateKey, receiver, content)
return encrypted
}
if (loginMethod === LoginMethods.nsecBunker) {
const user = new NDKUser({ pubkey: receiver })
this.remoteSigner?.on('authUrl', (authUrl) => {
this.emit('nsecbunker-auth', authUrl)
})
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
const encrypted = await this.remoteSigner.encrypt(user, content)
return encrypted
}
throw new Error('Login method is undefined')
return await context.nip04Encrypt(receiver, content)
}
/**
@ -456,79 +101,44 @@ export class NostrController extends EventEmitter {
* @returns A promise that resolves to the decrypted content.
*/
nip04Decrypt = async (sender: string, content: string): Promise<string> => {
const loginMethod = (store.getState().auth as AuthState).loginMethod
const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod)
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
if (!nostr.nip04) {
throw new Error(
`Your nostr extension does not support nip04 encryption & decryption`
)
}
const decrypted = await nostr.nip04.decrypt(sender, content)
return decrypted
}
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const decrypted = await nip04.decrypt(privateKey, sender, content)
return decrypted
}
if (loginMethod === LoginMethods.nsecBunker) {
const user = new NDKUser({ pubkey: sender })
this.remoteSigner?.on('authUrl', (authUrl) => {
this.emit('nsecbunker-auth', authUrl)
})
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
const decrypted = await this.remoteSigner.decrypt(user, content)
return decrypted
}
throw new Error('Login method is undefined')
return await context.nip04Decrypt(sender, content)
}
/**
* Function will capture the public key from the nostr extension or if no extension present
* function wil capture the public key from the local storage
* Function will capture the public key from signedEvent
*/
capturePublicKey = async (): Promise<string> => {
const nostr = this.getNostrObject()
const pubKey = await nostr.getPublicKey().catch((err: unknown) => {
if (err instanceof Error) {
return Promise.reject(err.message)
} else {
return Promise.reject(JSON.stringify(err))
try {
const timestamp = unixNow()
const { href } = window.location
const authEvent: EventTemplate = {
kind: 27235,
tags: [
['u', href],
['method', 'GET']
],
content: '',
created_at: timestamp
}
})
if (!pubKey) {
return Promise.reject('Error getting public key, user canceled')
const signedAuthEvent = await this.signEvent(authEvent)
const pubkey = signedAuthEvent.pubkey
if (!pubkey) {
return Promise.reject('Error getting public key, user canceled')
}
return Promise.resolve(pubkey)
} catch (error) {
if (error instanceof Error) {
return Promise.reject(error.message)
} else {
return Promise.reject(JSON.stringify(error))
}
}
return Promise.resolve(pubKey)
}
/**
* Generates NDK Private Signer
* @returns nSecBunker delegated key
*/
generateDelegatedKey = (): string => {
return NDKPrivateKeySigner.generate().privateKey!
}
}

@ -1,306 +0,0 @@
import { Event, Filter, Relay } from 'nostr-tools'
import {
settleAllFullfilfedPromises,
normalizeWebSocketURL,
timeout
} from '../utils'
import { SIGIT_RELAY } from '../utils/const'
/**
* Singleton class to manage relay operations.
*/
export class RelayController {
private static instance: RelayController
private pendingConnections = new Map<string, Promise<Relay | null>>() // Track pending connections
public connectedRelays = new Map<string, Relay>()
private constructor() {}
/**
* Provides the singleton instance of RelayController.
*
* @returns The singleton instance of RelayController.
*/
public static getInstance(): RelayController {
if (!RelayController.instance) {
RelayController.instance = new RelayController()
}
return RelayController.instance
}
/**
* Connects to a relay server if not already connected.
*
* This method checks if a relay with the given URL is already in the list of connected relays.
* If it is not connected, it attempts to establish a new connection.
* On successful connection, the relay is added to the list of connected relays and returned.
* If the connection fails, an error is logged and `null` is returned.
*
* @param relayUrl - The URL of the relay server to connect to.
* @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails.
*/
public connectRelay = async (relayUrl: string): Promise<Relay | null> => {
const normalizedWebSocketURL = normalizeWebSocketURL(relayUrl)
const relay = this.connectedRelays.get(normalizedWebSocketURL)
if (relay) {
if (relay.connected) return relay
// If relay is found in connectedRelay map but not connected,
// remove it from map and call connectRelay method again
this.connectedRelays.delete(relayUrl)
return this.connectRelay(relayUrl)
}
// Check if there's already a pending connection for this relay URL
if (this.pendingConnections.has(relayUrl)) {
// Return the existing promise to avoid making another connection
return this.pendingConnections.get(relayUrl)!
}
// Create a new connection promise and store it in pendingConnections
const connectionPromise = Relay.connect(relayUrl)
.then((relay) => {
if (relay.connected) {
// Add the newly connected relay to the connected relays map
this.connectedRelays.set(relayUrl, relay)
// Return the newly connected relay
return relay
}
return null
})
.catch((err) => {
// Log an error message if the connection fails
console.error(`Relay connection failed: ${relayUrl}`, err)
// Return null to indicate connection failure
return null
})
.finally(() => {
// Remove the connection from pendingConnections once it settles
this.pendingConnections.delete(relayUrl)
})
this.pendingConnections.set(relayUrl, connectionPromise)
return connectionPromise
}
/**
* Asynchronously retrieves multiple event from a set of relays based on a provided filter.
* If no relays are specified, it defaults to using connected relays.
*
* @param filter - The filter criteria to find the event.
* @param relays - An optional array of relay URLs to search for the event.
* @returns Returns a promise that resolves with an array of events.
*/
fetchEvents = async (
filter: Filter,
relayUrls: string[] = []
): Promise<Event[]> => {
if (!relayUrls.includes(SIGIT_RELAY)) {
/**
* NOTE: To avoid side-effects on external relayUrls array passed as argument
* re-assigned relayUrls with added sigit relay instead of just appending to same array
*/
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
}
// connect to all specified relays
const relays = await settleAllFullfilfedPromises(
relayUrls,
this.connectRelay
)
// Check if any relays are connected
if (relays.length === 0) {
throw new Error('No relay is connected to fetch events!')
}
const events: Event[] = []
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
// Create a promise for each relay subscription
const subPromises = relays.map((relay) => {
return new Promise<void>((resolve) => {
if (!relay.connected) {
console.log(`${relay.url} : Not connected!`, 'Skipping subscription')
return resolve()
}
// Subscribe to the relay with the specified filter
const sub = relay.subscribe([filter], {
// Handle incoming events
onevent: (e) => {
// Add the event to the array if it's not a duplicate
if (!eventIds.has(e.id)) {
eventIds.add(e.id) // Record the event ID
events.push(e) // Add the event to the array
}
},
// Handle the End-Of-Stream (EOSE) message
oneose: () => {
sub.close() // Close the subscription
resolve() // Resolve the promise when EOSE is received
}
})
// add a 30 sec of timeout to subscription
setTimeout(() => {
if (!sub.closed) {
sub.close()
resolve()
}
}, 30 * 1000)
})
})
// Wait for all subscriptions to complete
await Promise.allSettled(subPromises)
// It is possible that different relays will send different events and events array may contain more events then specified limit in filter
// To fix this issue we'll first sort these events and then return only limited events
if (filter.limit) {
// Sort events by creation date in descending order
events.sort((a, b) => b.created_at - a.created_at)
return events.slice(0, filter.limit)
}
return events
}
/**
* Asynchronously retrieves an event from a set of relays based on a provided filter.
* If no relays are specified, it defaults to using connected relays.
*
* @param filter - The filter criteria to find the event.
* @param relays - An optional array of relay URLs to search for the event.
* @returns Returns a promise that resolves to the found event or null if not found.
*/
fetchEvent = async (
filter: Filter,
relays: string[] = []
): Promise<Event | null> => {
const events = await this.fetchEvents(filter, relays)
// Sort events by creation date in descending order
events.sort((a, b) => b.created_at - a.created_at)
// Return the most recent event, or null if no events were received
return events[0] || null
}
/**
* Subscribes to events from multiple relays.
*
* This method connects to the specified relay URLs and subscribes to events
* using the provided filter. It handles incoming events through the given
* `eventHandler` callback and manages the subscription lifecycle.
*
* @param filter - The filter criteria to apply when subscribing to events.
* @param relayUrls - An optional array of relay URLs to connect to. The default relay URL (`SIGIT_RELAY`) is added automatically.
* @param eventHandler - A callback function to handle incoming events. It receives an `Event` object.
*
*/
subscribeForEvents = async (
filter: Filter,
relayUrls: string[] = [],
eventHandler: (event: Event) => void
) => {
if (!relayUrls.includes(SIGIT_RELAY)) {
/**
* NOTE: To avoid side-effects on external relayUrls array passed as argument
* re-assigned relayUrls with added sigit relay instead of just appending to same array
*/
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
}
// connect to all specified relays
const relays = await settleAllFullfilfedPromises(
relayUrls,
this.connectRelay
)
// Check if any relays are connected
if (relays.length === 0) {
throw new Error('No relay is connected to fetch events!')
}
const processedEvents: string[] = [] // To keep track of processed events
// Create a promise for each relay subscription
const subPromises = relays.map((relay) => {
return new Promise<void>((resolve) => {
// Subscribe to the relay with the specified filter
const sub = relay.subscribe([filter], {
// Handle incoming events
onevent: (e) => {
// Process event only if it hasn't been processed before
if (!processedEvents.includes(e.id)) {
processedEvents.push(e.id)
eventHandler(e) // Call the event handler with the event
}
},
// Handle the End-Of-Stream (EOSE) message
oneose: () => {
sub.close() // Close the subscription
resolve() // Resolve the promise when EOSE is received
}
})
})
})
// Wait for all subscriptions to complete
await Promise.allSettled(subPromises)
}
publish = async (
event: Event,
relayUrls: string[] = []
): Promise<string[]> => {
if (!relayUrls.includes(SIGIT_RELAY)) {
/**
* NOTE: To avoid side-effects on external relayUrls array passed as argument
* re-assigned relayUrls with added sigit relay instead of just appending to same array
*/
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
}
// connect to all specified relays
const relays = await settleAllFullfilfedPromises(
relayUrls,
this.connectRelay
)
// Check if any relays are connected
if (relays.length === 0) {
throw new Error('No relay is connected to publish event!')
}
const publishedOnRelays: string[] = [] // List to track which relays successfully published the event
// Create a promise for publishing the event to each connected relay
const publishPromises = relays.map(async (relay) => {
try {
await Promise.race([
relay.publish(event), // Publish the event to the relay
timeout(20 * 1000) // Set a timeout to handle cases where publishing takes too long
])
publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays
} catch (err) {
console.error(`Failed to publish event on relay: ${relay.url}`, err)
}
})
// Wait for all publish operations to complete (either fulfilled or rejected)
await Promise.allSettled(publishPromises)
// Return the list of relay URLs where the event was published
return publishedOnRelays
}
}
export const relayController = RelayController.getInstance()

@ -1,4 +1 @@
export * from './AuthController'
export * from './MetadataController'
export * from './NostrController'
export * from './RelayController'

@ -19,7 +19,7 @@
"page": 1
},
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
}
],
"da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05/2.png": [
@ -34,7 +34,7 @@
"page": 2
},
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
}
]
}
@ -54,7 +54,7 @@
"page": 1
},
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
"value": "Pera Peric"
},
{
@ -68,7 +68,7 @@
"page": 2
},
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
"value": "Pera Peric"
}
]

@ -1,2 +1,7 @@
export * from './store'
export * from './useAuth'
export * from './useDidMount'
export * from './useDvm'
export * from './useLogout'
export * from './useNDK'
export * from './useNDKContext'

142
src/hooks/useAuth.ts Normal file

@ -0,0 +1,142 @@
import { EventTemplate } from 'nostr-tools'
import { useCallback } from 'react'
import { NostrController } from '../controllers'
import { appPrivateRoutes } from '../routes'
import {
setAuthState,
setRelayMapAction,
setUserProfile
} from '../store/actions'
import {
base64DecodeAuthToken,
compareObjects,
createAndSaveAuthToken,
getAuthToken,
getRelayMapFromNDKRelayList,
unixNow
} from '../utils'
import { useAppDispatch, useAppSelector } from './store'
import { useNDKContext } from './useNDKContext'
import { useDvm } from './useDvm'
import { getFileServerMap } from '../utils/file-servers.ts'
import store from '../store/store.ts'
import { setServerMapAction } from '../store/servers/action.ts'
export const useAuth = () => {
const dispatch = useAppDispatch()
const { getRelayInfo } = useDvm()
const { findMetadata, getNDKRelayList, fetchEvent } = useNDKContext()
const authState = useAppSelector((state) => state.auth)
const relaysState = useAppSelector((state) => state.relays)
const checkSession = useCallback(() => {
const savedAuthToken = getAuthToken()
if (savedAuthToken) {
const signedEvent = base64DecodeAuthToken(savedAuthToken)
dispatch(
setAuthState({
loggedIn: true,
usersPubkey: signedEvent.pubkey
})
)
return
}
dispatch(
setAuthState({
loggedIn: false,
usersPubkey: undefined
})
)
}, [dispatch])
/**
* Function will authenticate user by signing an auth event
* which is done by calling the sign() function, where appropriate
* method will be chosen (extension or keys)
*
* @param pubkey of the user trying to login
* @returns url to redirect if user has no relays set
*/
const authAndGetMetadataAndRelaysMap = useCallback(
async (pubkey: string) => {
try {
const profile = await findMetadata(pubkey)
dispatch(setUserProfile(profile))
} catch (err) {
console.warn('Error occurred while finding metadata', err)
}
const timestamp = unixNow()
const { href } = window.location
const authEvent: EventTemplate = {
kind: 27235,
tags: [
['u', href],
['method', 'GET']
],
content: '',
created_at: timestamp
}
const nostrController = NostrController.getInstance()
const signedAuthEvent = await nostrController.signEvent(authEvent)
createAndSaveAuthToken(signedAuthEvent)
dispatch(
setAuthState({
loggedIn: true,
usersPubkey: pubkey
})
)
const [ndkRelayList, serverMap] = await Promise.all([
getNDKRelayList(pubkey),
getFileServerMap(pubkey, fetchEvent)
])
const relays = ndkRelayList.relays
if (relays.length < 1) {
// Navigate user to relays page if a relay map is empty
return appPrivateRoutes.relays
}
if (Object.keys(serverMap).length < 1) {
// Navigate user to servers page if a server map is empty
return appPrivateRoutes.servers
}
getRelayInfo(relays)
const relayMap = getRelayMapFromNDKRelayList(ndkRelayList)
if (authState.loggedIn) {
if (!compareObjects(relaysState?.map, relayMap))
dispatch(setRelayMapAction(relayMap))
if (!compareObjects(store.getState().servers?.map, serverMap.map))
dispatch(setServerMapAction(serverMap.map))
}
return
},
[
dispatch,
getNDKRelayList,
fetchEvent,
getRelayInfo,
authState.loggedIn,
findMetadata,
relaysState?.map
]
)
return {
authAndGetMetadataAndRelaysMap,
checkSession
}
}

98
src/hooks/useDvm.ts Normal file

@ -0,0 +1,98 @@
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
import { EventTemplate } from 'nostr-tools'
import { NostrController } from '../controllers'
import { setRelayInfoAction } from '../store/actions'
import { RelayInfoObject } from '../types'
import { compareObjects, unixNow } from '../utils'
import { useAppDispatch, useAppSelector } from './store'
import { useNDKContext } from './useNDKContext'
export const useDvm = () => {
const dvmRelays = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://relayable.org'
]
const relayInfo = useAppSelector((state) => state.relays.info)
const { ndk, publish } = useNDKContext()
const dispatch = useAppDispatch()
/**
* Sets information about relays into relays.info app state.
* @param relayURIs - relay URIs to get information about
*/
const getRelayInfo = async (relayURIs: string[]) => {
// initialize job request
const jobEventTemplate: EventTemplate = {
content: '',
created_at: unixNow(),
kind: 68001,
tags: [
['i', `${JSON.stringify(relayURIs)}`],
['j', 'relay-info']
]
}
const nostrController = NostrController.getInstance()
// sign job request event
const jobSignedEvent = await nostrController.signEvent(jobEventTemplate)
// publish job request
const ndkEvent = new NDKEvent(ndk, jobSignedEvent)
await publish(ndkEvent, dvmRelays)
const subscribeWithTimeout = (
subscription: NDKSubscription,
timeoutMs: number
): Promise<string> => {
return new Promise((resolve, reject) => {
const eventHandler = (event: NDKEvent) => {
subscription.stop()
resolve(event.content)
}
subscription.on('event', eventHandler)
// Set up a timeout to stop the subscription after a specified time
const timeout = setTimeout(() => {
subscription.stop() // Stop the subscription
reject(new Error('Subscription timed out')) // Reject the promise with a timeout error
}, timeoutMs)
// Handle subscription close event
subscription.on('close', () => clearTimeout(timeout))
})
}
// filter for getting DVM job's result
const sub = ndk.subscribe({
kinds: [68002 as number],
'#e': [jobSignedEvent.id],
'#p': [jobSignedEvent.pubkey]
})
// asynchronously get relay info from dvm job with 20 seconds timeout
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
if (!dvmJobResult) {
return Promise.reject(`Relay(s) information wasn't received`)
}
let newRelaysInfo: RelayInfoObject
try {
newRelaysInfo = JSON.parse(dvmJobResult)
} catch (error) {
return Promise.reject(`Invalid relay(s) information.`)
}
if (newRelaysInfo && !compareObjects(relayInfo, newRelaysInfo)) {
dispatch(setRelayInfoAction(newRelaysInfo))
}
}
return { getRelayInfo }
}

@ -0,0 +1,72 @@
import React, { useMemo } from 'react'
import {
getLocalStorageItem,
mergeWithInitialValue,
removeLocalStorageItem,
setLocalStorageItem
} from '../utils'
/**
* Subscribe to the Browser's storage event. Get the new value if any of the tabs changes it.
* @param callback - function to be called when the storage event is triggered
* @returns clean up function
*/
const useLocalStorageSubscribe = (callback: () => void) => {
window.addEventListener('storage', callback)
return () => window.removeEventListener('storage', callback)
}
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const getSnapshot = () => {
// Get the stored value
const storedValue = getLocalStorageItem(key, initialValue)
// Parse the value
const parsedStoredValue = JSON.parse(storedValue)
// Merge the default and the stored in case some of the required fields are missing
return JSON.stringify(
mergeWithInitialValue(parsedStoredValue, initialValue)
)
}
// https://react.dev/reference/react/useSyncExternalStore
// Returns the snapshot of the data and subscribes to the storage event
const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot)
// Takes the value or a function that returns the value and updates the local storage
const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
(v: React.SetStateAction<T>) => {
try {
const nextState =
typeof v === 'function'
? (v as (prevState: T) => T)(JSON.parse(data))
: v
if (nextState === undefined || nextState === null) {
removeLocalStorageItem(key)
} else {
setLocalStorageItem(key, JSON.stringify(nextState))
}
} catch (e) {
console.warn(e)
}
},
[data, key]
)
React.useEffect(() => {
// Set local storage only when it's empty
const data = window.localStorage.getItem(key)
if (data === null) {
setLocalStorageItem(key, JSON.stringify(initialValue))
}
}, [key, initialValue])
const memoized = useMemo(() => JSON.parse(data) as T, [data])
return [memoized, setState]
}

26
src/hooks/useLogout.tsx Normal file

@ -0,0 +1,26 @@
import { logout as nostrLogout } from 'nostr-login'
import { clear } from '../utils/localStorage'
import { userLogOutAction } from '../store/actions'
import { LoginMethod } from '../store/auth/types'
import { useAppDispatch, useAppSelector } from './store'
import { useCallback } from 'react'
export const useLogout = () => {
const loginMethod = useAppSelector((state) => state.auth?.loginMethod)
const dispatch = useAppDispatch()
const logout = useCallback(() => {
// Log out of the nostr-login
if (loginMethod === LoginMethod.nostrLogin) {
nostrLogout()
}
// Reset redux state with the logout
dispatch(userLogOutAction())
// Clear the local storage states
clear()
}, [dispatch, loginMethod])
return logout
}

666
src/hooks/useNDK.ts Normal file

@ -0,0 +1,666 @@
import { useCallback } from 'react'
import { toast } from 'react-toastify'
import { bytesToHex } from '@noble/hashes/utils'
import {
NDKEvent,
NDKFilter,
NDKKind,
NDKRelaySet,
NDKSubscriptionCacheUsage
} from '@nostr-dev-kit/ndk'
import _ from 'lodash'
import {
Event,
finalizeEvent,
generateSecretKey,
getEventHash,
getPublicKey,
kinds,
UnsignedEvent
} from 'nostr-tools'
import { useAppDispatch, useAppSelector, useNDKContext } from '.'
import { NostrController } from '../controllers'
import {
updateProcessedGiftWraps,
updateUserAppData as updateUserAppDataAction
} from '../store/actions'
import { Keys } from '../store/auth/types'
import {
BlossomVersion,
isSigitNotification,
Meta,
SigitNotification,
UserAppData,
UserRelaysType
} from '../types'
import {
countLeadingZeroes,
createWrap,
deleteBlossomFile,
fetchMetaFromFileStorage,
getDTagForUserAppData,
getUserAppDataFromBlossom,
hexToNpub,
nip44Encrypt,
parseJson,
randomTimeUpTo2DaysInThePast,
SIGIT_RELAY,
unixNow,
uploadUserAppDataToBlossom
} from '../utils'
import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError'
export const useNDK = () => {
const dispatch = useAppDispatch()
const {
ndk,
fetchEvent,
fetchEventFromUserRelays,
fetchEventsFromUserRelays,
publish,
getNDKRelayList
} = useNDKContext()
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
const appData = useAppSelector((state) => state.userAppData)
const processedEvents = useAppSelector(
(state) => state.userAppData?.processedGiftWraps
)
/**
* Fetches user application data based on user's public key.
*
* @returns The user application data or null if an error occurs or no data is found.
*/
const getUsersAppData = useCallback(async (): Promise<UserAppData | null> => {
if (!usersPubkey) return null
// Get an instance of the NostrController
const nostrController = NostrController.getInstance()
// Decryption can fail down in the code if extension options changed
// Forcefully log out the user if we detect missmatch between pubkeys
if (usersPubkey !== (await nostrController.capturePublicKey())) {
return null
}
// Generate an identifier for the user's nip78
const dTag = await getDTagForUserAppData()
if (!dTag) return null
// Define a filter for fetching events
const filter: NDKFilter = {
kinds: [NDKKind.AppSpecificData],
authors: [usersPubkey],
'#d': [dTag]
}
const encryptedContent = await fetchEvent(filter, {
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY
})
.then((event) => {
if (event) return event.content
// If no event is found, return an empty stringified object
return '{}'
})
.catch((err) => {
// Log error and show a toast notification if fetching event fails
console.log(`An error occurred in finding kind 30078 event`, err)
toast.error(
'An error occurred in finding kind 30078 event for data storage'
)
return null
})
// Return null if encrypted content retrieval fails
if (!encryptedContent) return null
// Handle case where the encrypted content is an empty object
if (encryptedContent === '{}') {
// Generate ephemeral key pair
const secret = generateSecretKey()
const pubKey = getPublicKey(secret)
return {
sigits: {},
processedGiftWraps: [],
blossomVersions: [],
keyPair: {
private: bytesToHex(secret),
public: pubKey
}
}
}
// Decrypt the encrypted content
const decrypted = await nostrController
.nip04Decrypt(usersPubkey, encryptedContent)
.catch((err) => {
// Log error and show a toast notification if decryption fails
console.log('An error occurred while decrypting app data', err)
toast.error('An error occurred while decrypting app data')
return null
})
// Return null if decryption fails
if (!decrypted) return null
// Parse the decrypted content
const parsedContent = await parseJson<{
blossomVersions: BlossomVersion[]
blossomUrls: string[]
keyPair: Keys
}>(decrypted).catch((err) => {
// Log error and show a toast notification if parsing fails
console.log(
'An error occurred in parsing the content of kind 30078 event',
err
)
toast.error(
'An error occurred in parsing the content of kind 30078 event'
)
return null
})
// Return null if parsing fails
if (!parsedContent) return null
// If old property blossomUrls is found, convert it to new appraoch blossomVersions
if (parsedContent.blossomUrls) {
parsedContent.blossomVersions = parsedContent.blossomUrls.map((url) => {
return {
urls: [url]
}
})
}
const { blossomVersions, keyPair } = parsedContent
// Return null if no blossom URLs are found
if (blossomVersions.length === 0) return null
// Fetch additional user app data from the last blossom version urls
const dataFromBlossom = await getUserAppDataFromBlossom(
blossomVersions[0],
keyPair.private
)
// Return null if fetching data from blossom fails
if (!dataFromBlossom) return null
const { sigits, processedGiftWraps } = dataFromBlossom
// Return the final user application data
return {
blossomVersions,
keyPair,
sigits,
processedGiftWraps
}
}, [usersPubkey, fetchEvent])
const updateUsersAppData = useCallback(
async (metaArray: Meta[]) => {
if (!appData || !appData.keyPair || !usersPubkey) return null
const sigits = _.cloneDeep(appData.sigits)
let isUpdated = false
for (const meta of metaArray) {
const createSignatureEvent = await parseJson<Event>(
meta.createSignature
).catch((err) => {
console.log('Error in parsing the createSignature event:', err)
toast.error(
err.message ||
'Error occurred in parsing the create signature event'
)
return null
})
if (!createSignatureEvent) continue
const id = createSignatureEvent.id
// Check if sigit already exists
if (id in sigits) {
// Update meta only if incoming meta is more recent
const existingMeta = sigits[id]
if (existingMeta.modifiedAt < meta.modifiedAt) {
sigits[id] = meta
isUpdated = true
}
} else {
sigits[id] = meta
isUpdated = true
}
}
if (!isUpdated) return null
const blossomVersions = [...appData.blossomVersions]
const newBlossomUrls = await uploadUserAppDataToBlossom(
sigits,
appData.processedGiftWraps,
appData.keyPair.private
).catch((err) => {
console.log(
'Error uploading user app data file to Blossom server:',
err
)
toast.error(
'Error occurred in uploading user app data file to Blossom server'
)
return null
})
if (!newBlossomUrls) return null
// insert new server (blossom) urls at the start of the array
blossomVersions.unshift({
urls: newBlossomUrls
})
// only keep last 10 blossom versions (urls), delete older ones
// Every version can be uploaded to multiple servers
if (blossomVersions.length > 10) {
const versionsToDelete = blossomVersions.splice(10)
versionsToDelete.forEach((version) => {
for (const url of version.urls) {
deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
console.log(
`An error occurred while removing an old file of user app data from the file server: ${url}`,
err
)
})
}
})
}
// Encrypt content for storing in kind 30078 event
const nostrController = NostrController.getInstance()
const encryptedContent = await nostrController
.nip04Encrypt(
usersPubkey,
JSON.stringify({
blossomVersions: blossomVersions,
keyPair: appData.keyPair
})
)
.catch((err) => {
console.log('Error encrypting content for app data:', err)
toast.error(err.message || 'Error encrypting content for app data')
return null
})
if (!encryptedContent) return null
// Generate the identifier for user's appData event
const dTag = await getDTagForUserAppData()
if (!dTag) return null
const updatedEvent: UnsignedEvent = {
kind: kinds.Application,
pubkey: usersPubkey,
created_at: unixNow(),
tags: [['d', dTag]],
content: encryptedContent
}
const signedEvent = await nostrController
.signEvent(updatedEvent)
.catch((err) => {
console.log('Error signing event:', err)
toast.error(err.message || 'Error signing event')
return null
})
if (!signedEvent) return null
const ndkEvent = new NDKEvent(ndk, signedEvent)
const publishResult = await publish(ndkEvent)
if (publishResult.length === 0 || !publishResult) {
toast.error('Unexpected error occurred in publishing updated app data')
return null
}
console.count('updateUserAppData useNDK')
// Update Redux store
dispatch(
updateUserAppDataAction({
sigits,
blossomVersions: blossomVersions,
processedGiftWraps: [...appData.processedGiftWraps],
keyPair: {
...appData.keyPair
}
})
)
return signedEvent
},
[appData, dispatch, ndk, publish, usersPubkey]
)
const processReceivedEvents = useCallback(
async (events: NDKEvent[], difficulty: number = 5) => {
if (!processedEvents) return
const validMetaArray: Meta[] = [] // Array to store valid Meta objects
const updatedProcessedEvents = [...processedEvents] // Keep track of processed event IDs
for (const event of events) {
// Skip already processed events
if (processedEvents.includes(event.id)) continue
// Validate PoW
const leadingZeroes = countLeadingZeroes(event.id)
if (leadingZeroes < difficulty) continue
// Decrypt the content of the gift wrap event
const nostrController = NostrController.getInstance()
const decrypted = await nostrController
.nip44Decrypt(event.pubkey, event.content)
.catch((err) => {
console.log('An error occurred in decrypting event content', err)
return null
})
if (!decrypted) continue
const internalUnsignedEvent = await parseJson<UnsignedEvent>(
decrypted
).catch((err) => {
console.log(
'An error occurred in parsing the internal unsigned event',
err
)
return null
})
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938)
continue
const parsedContent = await parseJson<Meta | SigitNotification>(
internalUnsignedEvent.content
).catch((err) => {
console.log('An error occurred in parsing event content', err)
return null
})
if (!parsedContent) continue
let meta: Meta
if (isSigitNotification(parsedContent)) {
const notification = parsedContent
if (!notification.keys || !usersPubkey) continue
let encryptionKey: string | undefined
const { sender, keys } = notification.keys
const usersNpub = hexToNpub(usersPubkey)
if (usersNpub in keys) {
encryptionKey = await nostrController
.nip04Decrypt(sender, keys[usersNpub])
.catch((err) => {
console.log(
'An error occurred in decrypting encryption key',
err
)
return undefined
})
}
try {
meta = await fetchMetaFromFileStorage(
notification.metaUrls,
encryptionKey
)
} catch (error) {
console.error(
'An error occurred fetching meta file from storage',
error
)
continue
}
} else {
meta = parsedContent
}
validMetaArray.push(meta) // Add valid Meta to the array
updatedProcessedEvents.push(event.id) // Mark event as processed
}
// Update processed events in the Redux store
dispatch(updateProcessedGiftWraps(updatedProcessedEvents))
// Pass the array of Meta objects to updateUsersAppData
if (validMetaArray.length > 0) {
await updateUsersAppData(validMetaArray)
}
},
[dispatch, processedEvents, updateUsersAppData, usersPubkey]
)
const subscribeForSigits = useCallback(
async (pubkey: string) => {
// Define the filter for the subscription
const filter: NDKFilter = {
kinds: [1059 as NDKKind],
'#p': [pubkey]
}
// Process the received event synchronously
const events = await fetchEventsFromUserRelays(
filter,
pubkey,
UserRelaysType.Read,
{
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY
}
)
await processReceivedEvents(events)
},
[fetchEventsFromUserRelays, processReceivedEvents]
)
/**
* Function to send a notification to a specified receiver.
* @param receiver - The recipient's public key.
* @param notification - Url pointing to metadata associated with the notification on blossom and keys to decrypt.
*/
const sendNotification = useCallback(
async (receiver: string, notification: SigitNotification) => {
if (!usersPubkey) return
// Create an unsigned event object with the provided metadata
const unsignedEvent: UnsignedEvent = {
kind: 938,
pubkey: usersPubkey,
content: JSON.stringify(notification),
tags: [],
created_at: unixNow()
}
// Wrap the unsigned event with the receiver's information
const wrappedEvent = createWrap(unsignedEvent, receiver)
// Publish the notification event to the recipient's read relays
const ndkEvent = new NDKEvent(ndk, wrappedEvent)
const ndkRelayList = await getNDKRelayList(receiver)
const readRelayUrls: string[] = []
if (ndkRelayList?.readRelayUrls) {
readRelayUrls.push(...ndkRelayList.readRelayUrls)
}
if (!readRelayUrls.includes(SIGIT_RELAY)) {
readRelayUrls.push(SIGIT_RELAY)
}
await ndkEvent
.publish(NDKRelaySet.fromRelayUrls(readRelayUrls, ndk, true))
.then((publishedOnRelays) => {
if (publishedOnRelays.size === 0) {
throw new Error('Could not publish to any relay')
}
return publishedOnRelays
})
.catch((err) => {
// Log an error if publishing the notification event fails
console.log(
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
err
)
throw err
})
},
[ndk, usersPubkey, getNDKRelayList]
)
/**
* Modified {@link UnsignedEvent Unsigned Event} that includes an id
*
* Fields id and created_at are required.
* @see {@link UnsignedEvent}
* @see {@link https://github.com/nostr-protocol/nips/blob/master/17.md#direct-message-kind}
*/
type UnsignedEventWithId = UnsignedEvent & {
id?: string
}
const sendPrivateDirectMessage = useCallback(
async (message: string, receiver: string, subject?: string) => {
if (!receiver) throw new SendDMError(SendDMErrorType.MISSING_RECIEVER)
// Get the direct message preferred relays list
// https://github.com/nostr-protocol/nips/blob/master/17.md#publishing
const preferredRelaysListEvent = await fetchEventFromUserRelays(
{
kinds: [NDKKind.DirectMessageReceiveRelayList],
authors: [receiver]
},
receiver,
UserRelaysType.Read
)
const isRelayTag = (tag: string[]): boolean => tag[0] === 'relay'
const finalRelaysList: string[] = []
if (preferredRelaysListEvent) {
const preferredRelaysList = preferredRelaysListEvent.tags
.filter((t) => isRelayTag(t))
.map((t) => t[1])
finalRelaysList.push(...preferredRelaysList)
}
if (!finalRelaysList.length) {
// Get receiver's read relay list
const ndkRelayList = await getNDKRelayList(receiver).catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
err
)
return null
})
if (ndkRelayList?.readRelayUrls) {
finalRelaysList.push(...ndkRelayList.readRelayUrls)
}
}
if (!finalRelaysList.includes(SIGIT_RELAY)) {
finalRelaysList.push(SIGIT_RELAY)
}
// Generate "sender"
const senderSecret = generateSecretKey()
const senderPubkey = getPublicKey(senderSecret)
// Prepare tags for the message
const tags: string[][] = [['p', receiver]]
// Conversation title
if (subject) tags.push(['subject', subject])
// Create private DM event containing the message and relevant metadata
// TODO: kinds.PrivateDirectMessage (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const dm: UnsignedEventWithId = {
pubkey: senderPubkey,
created_at: unixNow(),
kind: 14,
tags,
content: message
}
// Calculate the hash based on the UnverifiedEvent
dm.id = getEventHash(dm)
// Encrypt the private dm using the sender secret and the receiver's public key
const encryptedDm = nip44Encrypt(dm, senderSecret, receiver)
if (!encryptedDm) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
message,
kind: dm.kind
}
})
}
// Seal the message
// TODO: kinds.Seal (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const sealedMessage: UnsignedEvent = {
kind: 13, // seal
pubkey: senderPubkey,
content: encryptedDm,
created_at: randomTimeUpTo2DaysInThePast(),
tags: [] // no tags
}
// Finalize and sign the sealed event
const finalizedSeal = finalizeEvent(sealedMessage, senderSecret)
// Encrypt the seal and gift wrap
const finalizedGiftWrap = createWrap(finalizedSeal, receiver)
const ndkEvent = new NDKEvent(ndk, finalizedGiftWrap)
// Publish the finalized gift wrap event (the encrypted DM) to the relays
const publishedOnRelays = await ndkEvent.publish(
NDKRelaySet.fromRelayUrls(finalRelaysList, ndk, true)
)
// Handle cases where publishing to the relays failed
if (publishedOnRelays.size === 0) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
count: publishedOnRelays.size
}
})
}
// Return true indicating that the DM was successfully sent
return true
},
[fetchEventFromUserRelays, getNDKRelayList, ndk]
)
return {
getUsersAppData,
subscribeForSigits,
updateUsersAppData,
sendNotification,
sendPrivateDirectMessage
}
}

@ -0,0 +1,13 @@
import { NDKContext, NDKContextType } from '../contexts/NDKContext'
import { useContext } from 'react'
export const useNDKContext = () => {
const ndkContext = useContext(NDKContext)
if (!ndkContext)
throw new Error(
'NDKContext should not be used in out component tree hierarchy'
)
return { ...ndkContext } as NDKContextType
}

@ -0,0 +1,27 @@
import { useEffect, useState } from 'react'
import { NDKUserProfile } from '@nostr-dev-kit/ndk'
import { useNDKContext } from './useNDKContext'
export const useProfileMetadata = (pubkey: string) => {
const { findMetadata } = useNDKContext()
const [userProfile, setUserProfile] = useState<NDKUserProfile>()
useEffect(() => {
if (pubkey) {
findMetadata(pubkey)
.then((profile) => {
if (profile) setUserProfile(profile)
})
.catch((err) => {
console.error(
`error occurred in finding metadata for: ${pubkey}`,
err
)
})
}
}, [pubkey, findMetadata])
return userProfile
}

@ -1,10 +1,5 @@
import { useEffect, useState } from 'react'
import {
CreateSignatureEventContent,
DocSignatureEvent,
Meta,
SignedEventContent
} from '../types'
import { DocSignatureEvent, Meta, SignedEventContent, FlatMeta } from '../types'
import { Mark } from '../types/mark'
import {
fromUnixTimestamp,
@ -16,46 +11,10 @@ import {
} from '../utils'
import { toast } from 'react-toastify'
import { verifyEvent } from 'nostr-tools'
import { Event } from 'nostr-tools'
import store from '../store/store'
import { AuthState } from '../store/auth/types'
import { NostrController } from '../controllers'
import { MetaParseError } from '../types/errors/MetaParseError'
/**
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
* and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions)
*/
export interface FlatMeta
extends Meta,
CreateSignatureEventContent,
Partial<Omit<Event, 'pubkey' | 'created_at'>> {
// Remove pubkey and use submittedBy as `npub1${string}`
submittedBy?: `npub1${string}`
// Remove created_at and replace with createdAt
createdAt?: number
// Validated create signature event
isValid: boolean
// Decryption
encryptionKey: string | null
// Parsed Document Signatures
parsedSignatureEvents: {
[signer: `npub1${string}`]: DocSignatureEvent
}
// Calculated completion time
completedAt?: number
// Calculated status fields
signedStatus: SigitStatus
signersStatus: {
[signer: `npub1${string}`]: SignStatus
}
}
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy'
/**
* Custom use hook for parsing the Sigit Meta
@ -67,7 +26,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
const [kind, setKind] = useState<number>()
const [tags, setTags] = useState<string[][]>()
const [createdAt, setCreatedAt] = useState<number>()
const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event
const [submittedBy, setSubmittedBy] = useState<string>() // submittedBy, pubkey from nostr event (hex)
const [exportedBy, setExportedBy] = useState<string>() // pubkey from export signature nostr event (hex)
const [id, setId] = useState<string>()
const [sig, setSig] = useState<string>()
@ -78,7 +38,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
}>({})
const [markConfig, setMarkConfig] = useState<Mark[]>([])
const [title, setTitle] = useState<string>('')
const [zipUrl, setZipUrl] = useState<string>('')
const [zipUrls, setZipUrls] = useState<string[]>([])
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
[signer: `npub1${string}`]: DocSignatureEvent
@ -93,13 +53,23 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
[signer: `npub1${string}`]: SignStatus
}>({})
const [encryptionKey, setEncryptionKey] = useState<string | null>(null)
const [encryptionKey, setEncryptionKey] = useState<string | undefined>()
useEffect(() => {
if (!meta) return
;(async function () {
try {
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
if (meta.exportSignature) {
const exportSignatureEvent = parseNostrEvent(meta.exportSignature)
if (
verifyEvent(exportSignatureEvent) &&
exportSignatureEvent.pubkey
) {
setExportedBy(exportSignatureEvent.pubkey)
}
}
const createSignatureEvent = parseNostrEvent(meta.createSignature)
const { kind, tags, created_at, pubkey, id, sig, content } =
createSignatureEvent
@ -109,11 +79,11 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setTags(tags)
// created_at in nostr events are stored in seconds
setCreatedAt(fromUnixTimestamp(created_at))
setSubmittedBy(pubkey as `npub1${string}`)
setSubmittedBy(pubkey)
setId(id)
setSig(sig)
const { title, signers, viewers, fileHashes, markConfig, zipUrl } =
const { title, signers, viewers, fileHashes, markConfig, zipUrls } =
await parseCreateSignatureEventContent(content)
setTitle(title)
@ -121,12 +91,13 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setViewers(viewers)
setFileHashes(fileHashes)
setMarkConfig(markConfig)
setZipUrl(zipUrl)
setZipUrls(zipUrls)
let encryptionKey: string | undefined
if (meta.keys) {
const { sender, keys } = meta.keys
// Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
const usersPubkey = store.getState().auth.usersPubkey!
const usersNpub = hexToNpub(usersPubkey)
// Check if the user's public key is in the keys object
@ -140,13 +111,13 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
'An error occurred in decrypting encryption key',
err
)
return null
return undefined
})
encryptionKey = decrypted
setEncryptionKey(decrypted)
}
}
// Temp. map to hold events and signers
const parsedSignatureEventsMap = new Map<
`npub1${string}`,
@ -188,13 +159,40 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
}
}
parsedSignatureEventsMap.forEach((event, npub) => {
for (const [npub, event] of parsedSignatureEventsMap) {
const isValidSignature = verifyEvent(event)
if (isValidSignature) {
// get the signature of prev signer from the content of current signers signedEvent
const prevSignersSig = getPrevSignerSig(npub)
try {
const obj: SignedEventContent = JSON.parse(event.content)
// Signature object can include values that need to be fetched and decrypted
for (let i = 0; i < obj.marks.length; i++) {
const m = obj.marks[i]
try {
const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {}
if (
typeof fetchAndDecrypt === 'function' &&
m.value &&
encryptionKey
) {
const decrypted = await fetchAndDecrypt(
m.value,
encryptionKey
)
obj.marks[i].value = decrypted
}
} catch (error) {
console.error(
`Error during mark fetchAndDecrypt phase`,
error
)
}
}
parsedSignatureEventsMap.set(npub, {
...event,
parsedContent: obj
@ -210,7 +208,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
}
}
})
}
signers
.filter((s) => !parsedSignatureEventsMap.has(s))
@ -260,11 +258,13 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
createSignature: meta?.createSignature,
docSignatures: meta?.docSignatures,
keys: meta?.keys,
timestamps: meta?.timestamps,
isValid,
kind,
tags,
createdAt,
submittedBy,
exportedBy,
id,
sig,
signers,
@ -272,7 +272,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
fileHashes,
markConfig,
title,
zipUrl,
zipUrls,
parsedSignatureEvents,
completedAt,
signedStatus,

@ -1,71 +0,0 @@
import { useEffect, useState } from 'react'
import { ProfileMetadata } from '../types'
import { MetadataController } from '../controllers'
import { npubToHex } from '../utils'
import { Event, kinds } from 'nostr-tools'
/**
* Extracts profiles from metadata events
* @param pubkeys Array of npubs to check
* @returns ProfileMetadata
*/
export const useSigitProfiles = (
pubkeys: `npub1${string}`[]
): { [key: string]: ProfileMetadata } => {
const [profileMetadata, setProfileMetadata] = useState<{
[key: string]: ProfileMetadata
}>({})
useEffect(() => {
if (pubkeys.length) {
const metadataController = new MetadataController()
// Remove duplicate keys
const users = new Set<string>([...pubkeys])
const handleMetadataEvent = (key: string) => (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent) {
setProfileMetadata((prev) => ({
...prev,
[key]: metadataContent
}))
}
}
users.forEach((user) => {
const hexKey = npubToHex(user)
if (hexKey && !(hexKey in profileMetadata)) {
metadataController.on(hexKey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(hexKey)(event)
}
})
metadataController
.findMetadata(hexKey)
.then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(hexKey)(metadataEvent)
})
.catch((err) => {
console.error(
`error occurred in finding metadata for: ${user}`,
err
)
})
}
})
return () => {
users.forEach((key) => {
metadataController.off(key, handleMetadataEvent(key))
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pubkeys])
return profileMetadata
}

@ -27,7 +27,7 @@ body {
position: fixed;
top: 80px;
right: 20px;
z-index: 100;
z-index: 40;
}
#root {

@ -1,100 +1,166 @@
import { Event, kinds } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Outlet } from 'react-router-dom'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
import { getPublicKey, nip19 } from 'nostr-tools'
import { init as initNostrLogin } from 'nostr-login'
import { NostrLoginAuthOptions } from 'nostr-login/dist/types'
import { AppBar } from '../components/AppBar/AppBar'
import { LoadingSpinner } from '../components/LoadingSpinner'
import { MetadataController, NostrController } from '../controllers'
import { NostrController } from '../controllers'
import {
useAppDispatch,
useAppSelector,
useAuth,
useLogout,
useNDK,
useNDKContext
} from '../hooks'
import {
restoreState,
setAuthState,
setMetadataEvent,
updateUserAppData
setUserProfile,
updateKeyPair,
updateLoginMethod,
updateNostrLoginAuthMethod,
updateUserAppData,
setUserRobotImage
} from '../store/actions'
import { LoginMethods } from '../store/auth/types'
import { State } from '../store/rootReducer'
import { Dispatch } from '../store/store'
import { setUserRobotImage } from '../store/userRobotImage/action'
import {
clearAuthToken,
clearState,
getRoboHashPicture,
getUsersAppData,
loadState,
saveNsecBunkerDelegatedKey,
subscribeForSigits
} from '../utils'
import { useAppSelector } from '../hooks'
import { LoginMethod } from '../store/auth/types'
import { getRoboHashPicture, loadState } from '../utils'
import styles from './style.module.scss'
import { Footer } from '../components/Footer/Footer'
export const MainLayout = () => {
const dispatch: Dispatch = useDispatch()
const [searchParams, setSearchParams] = useSearchParams()
const navigate = useNavigate()
const dispatch = useAppDispatch()
const logout = useLogout()
const { findMetadata } = useNDKContext()
const { authAndGetMetadataAndRelaysMap } = useAuth()
const { getUsersAppData, subscribeForSigits } = useNDK()
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
const authState = useSelector((state: State) => state.auth)
const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
const authState = useAppSelector((state) => state.auth)
const usersAppData = useAppSelector((state) => state.userAppData)
// Ref to track if `subscribeForSigits` has been called
const hasSubscribed = useRef(false)
useEffect(() => {
const metadataController = new MetadataController()
const logout = () => {
dispatch(
setAuthState({
keyPair: undefined,
loggedIn: false,
usersPubkey: undefined,
loginMethod: undefined,
nsecBunkerPubkey: undefined
const navigateAfterLogin = useCallback(
(path: string | undefined) => {
const isCallback = window.location.hash.startsWith('#/?callbackPath=')
if (isCallback) {
const path = atob(window.location.hash.replace('#/?callbackPath=', ''))
setSearchParams((prev) => {
prev.delete('callbackPath')
return prev
})
)
navigate(path)
return
}
if (path) navigate(path)
},
[navigate, setSearchParams]
)
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
const login = useCallback(async () => {
try {
dispatch(updateLoginMethod(LoginMethod.nostrLogin))
const nostrController = NostrController.getInstance()
const pubkey = await nostrController.capturePublicKey()
const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
navigateAfterLogin(redirectPath)
} catch (error) {
console.error(`Error occured during login`, error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch])
// clear authToken saved in local storage
clearAuthToken()
clearState()
useEffect(() => {
// Developer login with ?nsec= (not recommended)
const nsec = searchParams.get('nsec')
if (!nsec) return
// update nsecBunker delegated key
const newDelegatedKey =
NostrController.getInstance().generateDelegatedKey()
saveNsecBunkerDelegatedKey(newDelegatedKey)
// Clear nsec from the url immediately
searchParams.delete('nsec')
setSearchParams(searchParams)
if (!authState?.loggedIn) {
if (!nsec.startsWith('nsec')) {
console.error('Invalid format, use private key (nsec)')
return
}
try {
const privateKey = nip19.decode(nsec).data as Uint8Array
if (!privateKey) {
console.error('Failed to convert the private key.')
return
}
const publickey = getPublicKey(privateKey)
dispatch(
updateKeyPair({
private: nsec,
public: publickey
})
)
dispatch(updateLoginMethod(LoginMethod.privateKey))
authAndGetMetadataAndRelaysMap(publickey).catch((err) => {
console.error('Error occurred in authentication: ' + err)
return null
})
} catch (err) {
console.error(`Error decoding the nsec. ${err}`)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, searchParams])
useEffect(() => {
const handleNostrAuth = (_: string, opts: NostrLoginAuthOptions) => {
if (opts.type === 'login' || opts.type === 'signup') {
dispatch(updateNostrLoginAuthMethod(opts.method))
login()
} else if (opts.type === 'logout') {
// Clear `subscribeForSigits` as called after the logout
hasSubscribed.current = false
}
}
// Initialize the nostr-login
initNostrLogin({
methods: ['connect', 'extension', 'local'],
noBanner: true,
onAuth: handleNostrAuth,
outboxRelays: [
'wss://purplepag.es',
'wss://relay.nos.social',
'wss://user.kindpag.es',
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.sigit.io'
]
}).catch((error) => {
console.error('Failed to initialize Nostr-Login', error)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch])
useEffect(() => {
const restoredState = loadState()
if (restoredState) {
dispatch(restoreState(restoredState))
const { loggedIn, loginMethod, usersPubkey, nsecBunkerRelays } =
restoredState.auth
const { loggedIn, loginMethod, usersPubkey } = restoredState.auth
if (loggedIn) {
if (!loginMethod || !usersPubkey) return logout()
if (loginMethod === LoginMethods.nsecBunker) {
if (!nsecBunkerRelays) return logout()
const nostrController = NostrController.getInstance()
nostrController.nsecBunkerInit(nsecBunkerRelays).then(() => {
nostrController.createNsecBunkerSigner(usersPubkey)
})
}
const handleMetadataEvent = (event: Event) => {
dispatch(setMetadataEvent(event))
}
metadataController.on(usersPubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
metadataController.findMetadata(usersPubkey).then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(metadataEvent)
findMetadata(usersPubkey).then((profile) => {
dispatch(setUserProfile(profile))
})
} else {
setIsLoading(false)
@ -102,21 +168,26 @@ export const MainLayout = () => {
} else {
setIsLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch])
/**
* Subscribe for the sigits
*/
useEffect(() => {
if (authState.loggedIn && usersAppData) {
if (authState && isLoggedIn && usersAppData) {
const pubkey = authState.usersPubkey || authState.keyPair?.public
if (pubkey && !hasSubscribed.current) {
// Call `subscribeForSigits` only if it hasn't been called before
// #193 disabled websocket subscribtion, until #194 is done
subscribeForSigits(pubkey)
// Mark `subscribeForSigits` as called
hasSubscribed.current = true
}
}
}, [authState, usersAppData])
}, [authState, isLoggedIn, usersAppData, subscribeForSigits])
/**
* When authState change user logged in / or app reloaded
@ -124,7 +195,7 @@ export const MainLayout = () => {
* so that avatar will be consistent across the app when kind 0 is empty
*/
useEffect(() => {
if (authState && authState.loggedIn) {
if (authState && isLoggedIn) {
const pubkey = authState.usersPubkey || authState.keyPair?.public
if (pubkey) {
@ -141,7 +212,8 @@ export const MainLayout = () => {
})
.finally(() => setIsLoading(false))
}
}, [authState, dispatch])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, isLoggedIn])
if (isLoading) return <LoadingSpinner desc={loadingSpinnerDesc} />
@ -160,7 +232,6 @@ export const MainLayout = () => {
>
<Outlet />
</main>
<Footer />
</>
)
}

@ -3,9 +3,33 @@
.container {
display: grid;
grid-template-columns: 0.75fr 1.5fr 0.75fr;
grid-gap: 30px;
flex-grow: 1;
@media only screen and (max-width: 767px) {
gap: 20px;
grid-auto-flow: column;
grid-auto-columns: 100%;
// Hide Scrollbar and let's use tabs to navigate
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
overflow-x: auto;
overscroll-behavior-inline: contain;
scroll-snap-type: inline mandatory;
> * {
scroll-margin-top: $header-height + $body-vertical-padding;
scroll-snap-align: start;
scroll-snap-stop: always; // Touch devices will always stop on each element
}
}
@media only screen and (min-width: 768px) {
grid-template-columns: 0.75fr 1.5fr 0.75fr;
gap: 30px;
}
}
.sidesWrap {
@ -16,17 +40,65 @@
}
.sides {
position: sticky;
top: $header-height + $body-vertical-padding;
@media only screen and (min-width: 768px) {
position: sticky;
top: $body-vertical-padding;
}
> :first-child {
// We want to keep header on smaller devices at all times
max-height: calc(
100dvh - $header-height - $body-vertical-padding * 2 - $tabs-height
);
@media only screen and (min-width: 768px) {
max-height: calc(100dvh - $body-vertical-padding * 2);
}
}
}
.files {
display: flex;
flex-direction: column;
grid-gap: 15px;
// Adjust the content scroll on smaller screens
// Make sure only the inner tab is scrolling
.scrollAdjust {
@media only screen and (max-width: 767px) {
max-height: calc(
100svh - $header-height - $body-vertical-padding * 2 - $tabs-height
);
overflow-y: auto;
}
}
.content {
padding: 10px;
border: 10px solid $overlay-background-color;
border-radius: 4px;
@media only screen and (min-width: 768px) {
padding: 10px;
border: 10px solid $overlay-background-color;
border-radius: 4px;
}
}
.navTabs {
display: none;
position: fixed;
left: 0;
bottom: 0;
right: 0;
height: $tabs-height;
z-index: 2;
background: $overlay-background-color;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
padding: 5px;
gap: 5px;
@media only screen and (max-width: 767px) {
display: flex;
}
> li {
flex-grow: 1;
}
}
.active {
background-color: $primary-main !important;
color: white !important;
}

@ -1,30 +1,147 @@
import { PropsWithChildren, ReactNode } from 'react'
import {
PropsWithChildren,
ReactNode,
useEffect,
useRef,
useState
} from 'react'
import styles from './StickySideColumns.module.scss'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { Button } from '@mui/material'
interface StickySideColumnsProps {
left?: ReactNode
right?: ReactNode
left: ReactNode
right: ReactNode
leftIcon: IconDefinition
centerIcon: IconDefinition
rightIcon: IconDefinition
}
const DEFAULT_TAB = 'nav-content'
export const StickySideColumns = ({
left,
right,
leftIcon,
centerIcon,
rightIcon,
children
}: PropsWithChildren<StickySideColumnsProps>) => {
const [tab, setTab] = useState(DEFAULT_TAB)
const ref = useRef<HTMLDivElement>(null)
const tabsRefs = useRef<{ [id: string]: HTMLDivElement | null }>({})
const handleNavClick = (id: string) => {
if (ref.current && tabsRefs.current) {
const x = tabsRefs.current[id]?.offsetLeft
ref.current.scrollTo({
left: x,
behavior: 'smooth'
})
}
}
const isActive = (id: string) => id === tab
useEffect(() => {
setTab(DEFAULT_TAB)
handleNavClick(DEFAULT_TAB)
}, [])
useEffect(() => {
const tabs = tabsRefs.current
// Set up the observer
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setTab(entry.target.id)
}
})
},
{
root: ref.current,
threshold: 0.5,
rootMargin: '-20px'
}
)
if (tabs) {
Object.values(tabs).forEach((tab) => {
if (tab) observer.observe(tab)
})
}
return () => {
if (tabs) {
Object.values(tabs).forEach((tab) => {
if (tab) observer.unobserve(tab)
})
}
}
}, [])
return (
<div className={styles.container}>
<div className={`${styles.sidesWrap} ${styles.files}`}>
<div className={styles.sides}>{left}</div>
</div>
<div>
<div id="content-preview" className={styles.content}>
{children}
<>
<div className={styles.container} ref={ref}>
<div
id="nav-left"
className={styles.sidesWrap}
ref={(tab) => (tabsRefs.current['nav-left'] = tab)}
>
<div className={styles.sides}>{left}</div>
</div>
<div
id="nav-content"
className={styles.scrollAdjust}
ref={(tab) => (tabsRefs.current['nav-content'] = tab)}
>
<div id="content-preview" className={styles.content}>
{children}
</div>
</div>
<div
id="nav-right"
className={styles.sidesWrap}
ref={(tab) => (tabsRefs.current['nav-right'] = tab)}
>
<div className={styles.sides}>{right}</div>
</div>
</div>
<div className={styles.sidesWrap}>
<div className={styles.sides}>{right}</div>
</div>
</div>
<ul className={styles.navTabs}>
<li>
<Button
fullWidth
variant="text"
onClick={() => handleNavClick('nav-left')}
className={`${isActive('nav-left') && styles.active}`}
aria-label="nav left"
>
<FontAwesomeIcon icon={leftIcon} />
</Button>
</li>
<li>
<Button
fullWidth
variant="text"
onClick={() => handleNavClick('nav-content')}
className={`${isActive('nav-content') && styles.active}`}
aria-label="nav middle"
>
<FontAwesomeIcon icon={centerIcon} />
</Button>
</li>
<li>
<Button
fullWidth
variant="text"
onClick={() => handleNavClick('nav-right')}
className={`${isActive('nav-right') && styles.active}`}
aria-label="nav right"
>
<FontAwesomeIcon icon={rightIcon} />
</Button>
</li>
</ul>
</>
)
}

@ -33,7 +33,7 @@ export const Modal = () => {
{ to: appPublicRoutes.register, title: 'Register', label: 'Register' },
{
to: appPublicRoutes.nostr,
title: 'Login',
title: 'Nostr Login',
sx: { padding: '10px' },
label: <img src={nostrImage} width="25" alt="nostr logo" height="25" />
}

@ -3,6 +3,5 @@
.main {
flex-grow: 1;
padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0;
background-color: $body-background-color;
padding: $body-vertical-padding 0 $body-vertical-padding 0;
}

@ -11,14 +11,15 @@ import './index.css'
import store from './store/store.ts'
import { theme } from './theme'
import { saveState } from './utils'
import { NDKContextProvider } from './contexts/NDKContext'
store.subscribe(
_.throttle(() => {
saveState({
auth: store.getState().auth,
metadata: store.getState().metadata,
userRobotImage: store.getState().userRobotImage,
relays: store.getState().relays
user: store.getState().user,
relays: store.getState().relays,
servers: store.getState().servers
})
}, 1000)
)
@ -28,7 +29,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<CssVarsProvider theme={theme}>
<HashRouter>
<Provider store={store}>
<App />
<NDKContextProvider>
<App />
</NDKContextProvider>
<ToastContainer />
</Provider>
</HashRouter>

File diff suppressed because it is too large Load Diff

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