Compare commits

...

215 Commits

Author SHA1 Message Date
7e590a4ef8 chore: Update translations 2023-11-08 15:44:23 +00:00
7088bee14d
fix: logged out lang 2023-11-08 15:43:18 +00:00
732a895a58 chore: Update translations 2023-11-08 15:18:57 +00:00
2d4d9117bd
fix: events 2023-11-08 15:18:01 +00:00
0e202b12d9 chore: Update translations 2023-11-08 14:49:48 +00:00
d119a5f626
feat: sign up flow v2 2023-11-08 14:47:46 +00:00
a2bcb936ef
fix: trim invalid chars from NostrLink 2023-11-08 14:47:46 +00:00
b4697d1e04 chore: Update translations 2023-11-08 09:47:27 +00:00
c9935a275e
feat: system on-event 2023-11-08 09:45:47 +00:00
3326aedc52
feat: collect relay metrics 2023-11-08 09:45:47 +00:00
8dbbb24729 chore: Update translations 2023-11-07 14:23:15 +00:00
b234762f62
chore: fetch tags in ci 2023-11-07 14:21:44 +00:00
cf6aa6b134
feat: default zap pool amount 2023-11-07 14:21:44 +00:00
e627cddd24
feat: relays cleanup 2023-11-07 14:21:44 +00:00
fcd2c8a3a0
feat: event emitter 2023-11-07 14:21:44 +00:00
fc3d196f48
chore: formatting 2023-11-07 14:21:43 +00:00
b166427f28
fix: relays tab on profile 2023-11-07 14:21:43 +00:00
d60862da11
fix: render less on notifications page 2023-11-07 14:21:43 +00:00
758107fd50
bug: parse legacy tag refs 2023-11-07 14:21:43 +00:00
e248889170
feat: select forward type 2023-11-07 14:21:43 +00:00
c689bd39dc
feat: add leo to contributors 2023-11-07 14:21:43 +00:00
f2456e060e chore: Update translations 2023-11-06 14:17:23 +00:00
de0e58b657
feat: autoTranslate preferences 2023-11-06 14:16:19 +00:00
3d69762359
chore: update prod script 2023-11-06 14:04:03 +00:00
9d91cc6ec9
fix: no subs error 2023-11-06 14:02:40 +00:00
6ca8f8f339
chore: auto translate for pro 2023-11-06 13:40:18 +00:00
5fe8a5e3b6 chore: Update translations 2023-11-06 13:34:09 +00:00
6e349051a2
feat: auto translate 2023-11-06 13:32:02 +00:00
e0b68ae817
chore: more relay info 2023-11-06 12:46:17 +00:00
b3c8ee982d
feat: get country from timezone 2023-11-05 13:48:33 +09:00
d59c3ebdcb
chore: remove unused 2023-11-05 11:42:06 +09:00
58e6be94fa
chore: remove nostr.watch 2023-11-05 11:41:11 +09:00
4ccb052edb chore: Update translations 2023-11-05 02:31:26 +00:00
f482c004b3
chore: stop loading kind3 for relay info 2023-11-03 14:48:26 +09:00
0ebd2f167a
chore: formatting 2023-11-03 14:30:54 +09:00
930b493a12
fix: write relays only in relay metadata 2023-11-03 14:30:22 +09:00
9eb029e1dc
fix: zapper for zap goals 2023-11-03 10:07:06 +09:00
0cef163eb9
fix: zap goal embed 2023-11-03 09:56:17 +09:00
71a05dd13c
feat: about 2023-11-03 02:40:02 +09:00
2f1d48792a chore: Update translations 2023-11-02 16:46:51 +00:00
ed3b6c84cf
fix: remove relay tag from zap e/a tag 2023-11-03 01:43:34 +09:00
f252087f6b
fix: startup relay race condition 2023-11-02 07:24:57 +09:00
d4bf929e60
fix: upgrade existing connection to non-ephemeral 2023-11-02 06:48:07 +09:00
32c80ed1c5 chore: Update translations 2023-11-01 21:12:59 +00:00
83a085a343
feat: deepl translate 2023-11-02 06:11:00 +09:00
f994f8722d chore: Update translations 2023-10-31 15:43:19 +00:00
6f15580682
chore: formatting 2023-11-01 00:40:57 +09:00
c65bb7a992
chore: cleanup 2023-11-01 00:40:12 +09:00
8f90daa840 chore: Update translations 2023-10-22 13:12:00 +00:00
46d7c000ac
fix: sub renew 2023-10-22 14:10:11 +01:00
454f957653
chore: print notification payload 2023-10-21 23:05:06 +01:00
c2991b8e26
fix: note creator mobile 2023-10-21 22:26:04 +01:00
63950f1e6b
chore: formatting 2023-10-21 21:46:15 +01:00
0e3661afc6
fix: note broadcaster bug / createPortal for modal 2023-10-21 21:45:50 +01:00
c1ea68b296
fix: Remove default lang 2023-10-20 13:55:50 +01:00
22224cb4f2 chore: Update translations 2023-10-20 12:47:54 +00:00
548247f39c
fix: notification badge 2023-10-20 13:46:50 +01:00
083f8a9edb chore: Update translations 2023-10-20 12:34:54 +00:00
6f1c36d53e chore: Update translations 2023-10-20 12:33:57 +00:00
b379550827
fix: wrong url in notifications 2023-10-20 13:33:06 +01:00
7b29290420
fix: service worker build 2023-10-20 13:26:53 +01:00
d06a914e01 chore: Update translations 2023-10-20 12:23:54 +00:00
76ec08a251 chore: Update translations 2023-10-20 12:22:25 +00:00
938bc84fb3 chore: Update translations 2023-10-20 12:21:03 +00:00
74480af85b chore: Update translations 2023-10-20 12:19:51 +00:00
2dd84bd280
fix: open note1 links from notification 2023-10-20 13:18:28 +01:00
fd746440b6
fix: open correct url for notifications 2023-10-20 13:13:18 +01:00
a5ae474c8b
chore: cleanup notification body 2023-10-20 12:54:49 +01:00
c68565c484 chore: Update translations 2023-10-20 11:32:48 +00:00
9f763fccba
feat: render other notification types 2023-10-20 12:31:21 +01:00
12c678ca7a chore: Update translations 2023-10-20 10:59:31 +00:00
bee0cc1188
feat: add scope to push notifications 2023-10-20 11:58:07 +01:00
15795d442f chore: Update translations 2023-10-19 18:26:53 +00:00
824b6fdce4
feat: cache in settings 2023-10-19 19:25:33 +01:00
c96ea94bb3
chore: remove big E 2023-10-19 16:46:05 +01:00
234d354062
fix: compact push notifications 2023-10-19 16:38:14 +01:00
3f28c94a56
chore: cleanup 2023-10-19 16:07:37 +01:00
1a984a8075
fix: use notification tag 2023-10-19 16:06:44 +01:00
748fe22101
feat: click notification 2023-10-19 15:22:07 +01:00
b5e9203742
feat: push notifications 2023-10-19 13:28:23 +01:00
c823cd314d chore: Update translations 2023-10-18 21:28:17 +00:00
8ea5be1504
fix: docker nginx config 2023-10-18 22:26:36 +01:00
ccd98bef1b
fix: center images 2023-10-18 15:50:29 +01:00
3b3a920124
fix: tests 2023-10-18 15:36:39 +01:00
4d4106a3ff
fix: center avatars 2023-10-18 15:18:19 +01:00
3714867b98
chore: drop nostrplebs 2023-10-18 15:12:48 +01:00
4be93c8f51
fix: always encode as naddr for PRE 2023-10-18 15:06:35 +01:00
ce09b92518 chore: Update translations 2023-10-18 14:00:38 +00:00
c0bfe376ed
fix: various 2023-10-18 14:59:14 +01:00
98c3d901ae
feat: renew sub task 2023-10-18 14:47:50 +01:00
0ba1ba05ac
fix: spotlight media styles 2023-10-18 14:47:50 +01:00
09cdd501c3
fix: max-height images 2023-10-18 14:47:50 +01:00
d954b90bfd
fix: fast zap not working 2023-10-18 14:47:50 +01:00
c565dbc993
chore: css fixes / rename subscription to pro 2023-10-18 14:47:49 +01:00
51f0d2ed15 chore: Update translations 2023-10-18 13:01:37 +00:00
63d3645dda Merge pull request 'Search dropdown' (#655) from mmalmi/snort:main into main 2023-10-18 13:00:23 +00:00
190dd92f9a fix esc handler 2023-10-18 15:44:03 +03:00
a331e43b4e search dropdown 2023-10-18 15:44:03 +03:00
5535614455 SearchBox.css 2023-10-18 15:44:03 +03:00
8e9e75c5f0 fix NoteFooter reacted colors 2023-10-18 15:44:03 +03:00
e3d17254f8 move nav search to its own component 2023-10-18 15:44:03 +03:00
cf60dcb654 chore: Update translations 2023-10-18 10:40:17 +00:00
c4bbafb9d7
feat: seasonal features 2023-10-18 11:39:15 +01:00
ab50afe917
fix: note creator styles 2023-10-18 11:21:25 +01:00
2ff072f442 chore: Update translations 2023-10-18 10:15:02 +00:00
0e0d768eec
feat: toggle switch 2023-10-18 11:13:11 +01:00
a081f9655e
chore: always add prefix on encode 2023-10-18 09:50:26 +01:00
81df18ea4e Merge pull request 'Event & profile links' (#653) from mmalmi/snort:main into main
Reviewed-on: Kieran/snort#653
2023-10-18 07:06:41 +00:00
2e663dcb4c NostrLink.encode(prefix: NostrPrefix) 2023-10-18 10:01:25 +03:00
52f70056f8 chore: Update translations 2023-10-17 22:06:33 +00:00
7a1df4a178
feat: profile cards on mentions 2023-10-17 23:01:53 +01:00
d50979b8ae
fix: add default robohash image path 2023-10-17 22:32:06 +01:00
9b3cc94d18
chore: robohash set2 2023-10-17 22:31:18 +01:00
700db8f62c "share note" url 2023-10-17 22:02:41 +03:00
2c7878ac7f CONFIG.profileLinkSuffix 2023-10-17 21:50:11 +03:00
d9bd198e8d CONFIG.eventLinkPrefix 2023-10-17 21:36:22 +03:00
ca18cf25e3 rm /e/ and /p/ from event & profile links 2023-10-17 20:02:32 +03:00
089c40d816
fix: load more subscription events 2023-10-17 16:36:12 +01:00
3f82b31b6b
fix: flex fixes 2023-10-17 16:33:29 +01:00
0548e1a9e1
fix: flex styles 2023-10-17 16:33:29 +01:00
2b09da6959 chore: Update translations 2023-10-17 13:06:06 +00:00
a0d14b158b
chore: formatting 2023-10-17 14:04:48 +01:00
faaeb6af4a
refactor: flex styles / fixes / profile links 2023-10-17 14:04:48 +01:00
6479a18cb2
feat: sub renew months 2023-10-17 14:04:48 +01:00
9f6a030a11 chore: Update translations 2023-10-17 09:56:28 +00:00
88c146d729
fix: lock file 2023-10-17 10:55:37 +01:00
281785952d
feat: more feature flags 2023-10-17 10:54:34 +01:00
1a507679f3
chore: add all reactions 2023-10-16 21:37:15 +01:00
f34ccf72cb
fix: cleanup 2023-10-16 21:33:01 +01:00
5e42c5e70c
feat: add useEventReactions to system-react 2023-10-16 21:24:54 +01:00
3b427338f4
chore: bump packages 2023-10-16 20:31:12 +01:00
725d6d11f0 chore: Update translations 2023-10-16 16:10:42 +00:00
f24d9982f4
feat: upload stage 2023-10-16 17:09:17 +01:00
dec2b9ce2e
chore: cleanup AsyncIcon element 2023-10-16 17:09:16 +01:00
497ef7bf9a chore: Update translations 2023-10-16 15:15:03 +00:00
0f40b9a426
chore: remove "popular" accounts list 2023-10-16 16:14:08 +01:00
a168465bdb Merge pull request 'tailwind' (#651) from mmalmi/snort:main into main 2023-10-16 15:12:04 +00:00
28fa0b4bc8
feat: use imgproxy to generate video posters 2023-10-16 15:53:31 +01:00
2ce5bd153b
feat: classnames 2023-10-16 15:48:56 +01:00
7129ffa1c7
don't close publish modal automatically.
even if all relays responded with an OK the modal will still remain open so the
user can look at the list of relays the note was published to.
2023-10-16 14:44:56 +01:00
227b3b8dd7
add trailingComma to prettier settings.
because my system was still using the old defaults and that was bad.
2023-10-16 14:44:55 +01:00
f0110e9009
feat: note creator button on deck 2023-10-16 14:44:55 +01:00
65552e126b
fix: spotlight bugs 2023-10-16 14:44:55 +01:00
5162887807 chore: Update translations 2023-10-16 12:47:11 +00:00
e378a53b21
feat: long form deck modal 2023-10-16 13:46:09 +01:00
d9fc4f37b0 chore: Update translations 2023-10-16 10:09:08 +00:00
6448996529
feat: file upload progress / imeta 2023-10-16 11:07:13 +01:00
a29d82bd56
fix: memoize note inner to prevent video reloading 2023-10-15 23:19:22 +01:00
3a95689792 chore: Update translations 2023-10-15 20:42:11 +00:00
10e83a5f55
Big E tag
https://github.com/nostr-protocol/nips/issues/812
2023-10-15 21:40:20 +01:00
4bf868c05a
chore: default sig checks off 2023-10-14 08:29:36 +01:00
cf7d9b8883 chore: Update translations 2023-10-13 21:24:42 +00:00
70925e6f08
feat: auth file uploaders 2023-10-13 22:22:21 +01:00
94058efb60 chore: Update translations 2023-10-13 15:43:58 +00:00
55d089072d
chore: disable code blocks 2023-10-13 16:43:11 +01:00
ce550eb206 chore: Update translations 2023-10-13 15:35:46 +00:00
93e8e0bbae
feat: make sig checks optional 2023-10-13 16:34:31 +01:00
87bb9dafeb
fix: check all sigs 2023-10-13 14:39:37 +01:00
9251d5b90a chore: Update translations 2023-10-13 11:42:15 +00:00
a647fd09b3
feat: quote repost
closes #217
2023-10-13 12:40:39 +01:00
ddb8e623f4
feat: show replies count 2023-10-13 11:22:58 +01:00
9b66b7b1da
fix: hide blocked replies 2023-10-13 11:16:44 +01:00
3b363d988e chore: Update translations 2023-10-13 10:12:26 +00:00
430763478f
fix: use last e/a tag for reply context 2023-10-13 11:10:55 +01:00
85faf528c5
fix: hide muted dms 2023-10-12 22:07:18 +01:00
2e38ac0d4f
feat: respond to auth only when expected 2023-10-12 22:02:05 +01:00
a080f0bb0c chore: Update translations 2023-10-12 15:29:54 +00:00
f5aa898631
feat: list feed rendering 2023-10-12 16:28:42 +01:00
01af3a3a58
fix: timeline feed reactions 2023-10-12 16:28:42 +01:00
733fb6da30
fix: filter expired status 2023-10-12 16:28:42 +01:00
81642b906e chore: Update translations 2023-10-12 14:56:59 +00:00
8f401c07bc
fix: typo 2023-10-12 15:55:36 +01:00
6e73e51501
feat: render kind 3 2023-10-12 15:54:46 +01:00
6650c48c98
fix: remove extra hashtag space 2023-10-12 15:16:35 +01:00
95a715839d
fix: disable WASM when not available 2023-10-12 15:12:06 +01:00
7513d4cdd3
chore: add avatar gradients to iris.to domain 2023-10-12 15:02:08 +01:00
a0207e8874
fix: use correct hostname in analytics script 2023-10-12 14:45:30 +01:00
a8964a2248
ci: checkout app from tag 2023-10-11 21:36:54 +01:00
102134d47f chore: Update translations 2023-10-11 20:00:46 +00:00
5a67edaf0b
fix: typo 2023-10-11 20:59:35 +01:00
2155d00a07 chore: Update translations 2023-10-11 18:26:44 +00:00
5360c5ad3b Merge pull request 'feat: add keyboard shortcuts' (#649) from fernandoporazzi/snort:scroll-up-shortcut into main
Reviewed-on: Kieran/snort#649
2023-10-11 18:25:07 +00:00
a0c8012f8e
Add retry/delete to note broadcaster 2023-10-11 19:24:11 +01:00
88ac4063cd
validate events on receive 2023-10-11 19:24:10 +01:00
022296fa18 chore: Update translations 2023-10-11 14:43:31 +00:00
0e4a040750
feat: note publishing progress 2023-10-11 15:41:36 +01:00
c239fba3df
fix: Filter a tagged as replies on timeline 2023-10-11 11:47:58 +01:00
ece4219180 chore: Update translations 2023-10-11 10:46:33 +00:00
6eca5a632d
feat: long form rendering 2023-10-11 11:45:10 +01:00
3b505f6c3e
relax zaps validation 2023-10-11 11:45:10 +01:00
bcaaba3fd4 chore: Update translations 2023-10-11 08:42:34 +00:00
09a9364163
fix: follow button response 2023-10-11 09:40:54 +01:00
Fernando Porazzi
5d137f281f
solve conflicts 2023-10-10 23:37:37 +02:00
Fernando Porazzi
b0d84779c8
feat: add keyboard shortcuts 2023-10-10 23:27:27 +02:00
2c31a37b6a
fix: gallery skip empty text elements 2023-10-10 12:40:18 +01:00
50bfd9eaa0
setup stalker mode 2023-10-10 12:20:49 +01:00
672255187f chore: Update translations 2023-10-10 09:43:20 +00:00
9d33abbf1e
feature flags config / typed app config 2023-10-10 10:37:53 +01:00
c023a89271 Merge pull request 'Profile urls, scrollbar, ProfilePage refactoring' (#646) from mmalmi/snort:main into main
Reviewed-on: Kieran/snort#646
2023-10-09 18:41:22 +00:00
b59c5685f9 Merge remote-tracking branch 'kieran/main' 2023-10-09 19:12:43 +03:00
8d882a0844
Profile hover cards 2023-10-09 16:32:14 +01:00
b62b877f5a
fix: build command order 2023-10-09 14:55:04 +01:00
01234d7be7 chore: Update translations 2023-10-09 13:52:12 +00:00
b5499f516c
fix: build scripts 2023-10-09 14:51:07 +01:00
8f153b0428 chore: Update translations 2023-10-09 13:36:42 +00:00
b27bb47007
Notification summary 2023-10-09 14:35:21 +01:00
15fb4cabdf css selector based margin-top 2023-10-09 16:06:47 +03:00
dbaad8bbb3 make useIsVerified pubkey param optional 2023-10-09 15:58:01 +03:00
79ef147023 simpler profile url nip05 replacement 2023-10-09 15:44:47 +03:00
224960a11f add spacing before media and link embeds 2023-10-08 16:40:23 +03:00
091169ae7d return note or profile component directly from NostrLinkHandler 2023-10-08 16:21:56 +03:00
5ed096509a use DisplayName on profile page 2023-10-08 15:27:41 +03:00
3f7ac9e2d4 if username@{NIP05_DOMAIN} valid, change profile page url to /username 2023-10-08 11:04:23 +03:00
9f5d467745 body overflow-y: scroll to reduce layout shift 2023-10-08 11:04:23 +03:00
e949708cec extract ProfileTab from ProfilePage 2023-10-08 11:04:23 +03:00
328 changed files with 12002 additions and 7078 deletions

View File

@ -12,6 +12,10 @@ trigger:
metadata:
namespace: git
steps:
- name: Fetch tags
image: alpine/git
commands:
- git fetch --tags
- name: Build site
image: node:current-bullseye
volumes:
@ -120,6 +124,10 @@ trigger:
metadata:
namespace: git
steps:
- name: Fetch tags
image: alpine/git
commands:
- git fetch --tags
- name: Build site
image: node:current-bullseye
volumes:

View File

@ -73,7 +73,7 @@ jobs:
- name: Copy files
run: |-
git clone https://git.v0l.io/Kieran/snort_android.git
git clone --depth 1 --branch ${{ github.ref_name }} https://git.v0l.io/Kieran/snort_android.git
mkdir -p snort_android/app/src/main/assets/
cp packages/app/build/* snort_android/app/src/main/assets/

View File

@ -1,5 +1,5 @@
server {
listen 80 default_server;
listen 8080 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;

View File

@ -4,15 +4,17 @@
"packages/*"
],
"scripts": {
"build": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-react build && yarn workspace @snort/app build",
"start": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-react build && yarn workspace @snort/app start",
"test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test",
"pre:commit": "yarn workspace @snort/app intl-extract && yarn workspace @snort/app intl-compile && yarn prettier --write ."
"build": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-web build && yarn workspace @snort/system-react build && yarn workspace @snort/app build",
"start": "yarn build && yarn workspace @snort/app start",
"test": "yarn build && yarn workspace @snort/app test && yarn workspace @snort/system test",
"pre:commit": "yarn workspace @snort/app intl-extract && yarn workspace @snort/app intl-compile && yarn prettier --write .",
"push-prod": "git checkout snort-prod && git merge --ff-only main && git push && git checkout main"
},
"prettier": {
"printWidth": 120,
"bracketSameLine": true,
"arrowParens": "avoid"
"arrowParens": "avoid",
"trailingComma": "all"
},
"packageManager": "yarn@3.6.3",
"dependencies": {

View File

@ -0,0 +1,47 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEApyUVkYJVwV7XgluUnllgCtrsdq1ctRICm5gQy8nd+aEdDQjA
CKPOWh5miLl/fAQVZGZy/JxavzXulwXo8238E6n6bmNB1Us2nuw7a0aW4iUSQ1Pt
P4ZhPpcrqeqMf+hp7iBW0nAHFy/aa2UR84d7tBmSk5J3NNrfBsZdUex/7FqF1EVv
mEzlc8kepU9lRXWFQDtZCllEZ1kY3SBJPm10h0g9saI8YIVRxUuNII5GHDYAE3hb
EmoY6fuSEoiXA8u0Yt9soBQxgxIhQVKSRPPoIPjGFOxsGHY6h8R9nx1kxhHKFRuV
nwsn0uWl/7yjhwyHanogJu73/WgelPcgP/hMDQIDAQABAoIBAAru+xU0oGVwzcoi
MXuWPxkWrwcoWfsiPXduIBMklleg+WSD4QPvqyzr9isVb0huf/O8W+M4WxtM7NmG
MnHSDP5ATThxV7obHGyS6WQgDvimEibDU66nHK9adim8RQqM6nkANo23dE9I+xGx
X9Y9U5M5ZQQwPYoAkzw/N5WHUerk+cSEYWYV8jDtO7wJhYOMu5qliPeuNOaWZ1W6
1uwr8A4ih69WwzugPuBSgBrPAW1c84zWIFN+njAugqPF5x8xp2uM3tUO9s5UlHJC
FWEuU40KcDT2utSUY+2HXSHbycF4KLKT5jAKSa4sPziLfo+YifrlN0Y3rhofUlZT
jCaeZ8ECgYEA5/xpk8aVhCEvv5iCghv0p/IHcjdXjx5+PCWh3Adx0fF91UvU5oqn
okdyYZDShZMuLDfJ0lG+OMKZd01JapnbTtiVNceVRMnraIdoWEM2/4bTXTSZGtdA
8gh/Kc/PMbPf5ppVWwqTCbUkPOSyGHyGc7+DQquq1w6yZu04A3x9vHECgYEAuHJk
uz8YKY5ZUR7CZ3y7YFuwq5Lcpl43AfiiCasjRch0P8yLrITc/6fORsXyy64XW9fC
h3YmXvEPaM03W2dxw2aQDvXEvXiEITzmILs7SE3UmZR9m7OMy7Jeqr3+JOc0ckZe
Rz5FfuMt1IvNB6lrpfHVtoVrpCOXpzHgC/k/x10CgYA6lU18GfwL/+107uiWPsUL
3FzxBPTBmau7OK2lSOP/ZoKmaJ39Eiq/GlfSN6ZSQRa55+S5jhcBcnMa45OUrgHp
6VvU1u/lDTC7luZM07yBzuR1dyDq3Ez0Uhz6zBXAsXHrZDIF6ae0HeBm2EH5WQkD
Fevp3DwqTvXSdDle+AMwoQKBgQCBSlaH1rNmNc0wCsK07f8ejUcrDZgz2mjurc1P
v7HK8bdjHUtvE/ciEguLGqiV06O2EmjesZg2Bv4JNYivPrTFBrjGc8qEEd10uw6J
NRVaGoyDV04w/UwdYRvwzZs/XP4reF4PzHvEdRSkH5cJ3t2BhiKLfby1YumkHlbx
rbbiVQKBgB02jyZUiB6pPTCP8vXZCJbZELgqNyS04ALhBBpdfGMcU1+0hRLJFBaE
tClJPGARFXl+MPkY032vmJZOuH3LrcTCm8DmMLzM/hT1EWawQ8BJkkwiIokE4lqc
Bi8CrkvuQs2cuCStK6C3Nkyr1lTkDge46trsb7KTcfHdtLsS7EPj
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDWzCCAkOgAwIBAgIJDji8iiceMvQlMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMTCWxvY2FsaG9zdDAeFw0yMzEwMTYwOTI0MThaFw0yMzExMTUxMDI0MThaMBQx
EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAKclFZGCVcFe14JblJ5ZYAra7HatXLUSApuYEMvJ3fmhHQ0IwAijzloeZoi5
f3wEFWRmcvycWr817pcF6PNt/BOp+m5jQdVLNp7sO2tGluIlEkNT7T+GYT6XK6nq
jH/oae4gVtJwBxcv2mtlEfOHe7QZkpOSdzTa3wbGXVHsf+xahdRFb5hM5XPJHqVP
ZUV1hUA7WQpZRGdZGN0gST5tdIdIPbGiPGCFUcVLjSCORhw2ABN4WxJqGOn7khKI
lwPLtGLfbKAUMYMSIUFSkkTz6CD4xhTsbBh2OofEfZ8dZMYRyhUblZ8LJ9Llpf+8
o4cMh2p6ICbu9/1oHpT3ID/4TA0CAwEAAaOBrzCBrDAMBgNVHRMEBTADAQH/MAsG
A1UdDwQEAwIC9DAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUF
BwMDBggrBgEFBQcDCDBcBgNVHREEVTBTgglsb2NhbGhvc3SCFWxvY2FsaG9zdC5s
b2NhbGRvbWFpboIGbHZoLm1lgggqLmx2aC5tZYIFWzo6MV2HBH8AAAGHEP6AAAAA
AAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBABY0rgWuzLYvVtvoVvWKS9cg
8rVhBRIFvpYO814ocN1iaxYQ9t9uLRsJXj0K+z1BHWf0zBiw4mB3dD9VpiKpuliL
4tRT+vATA96OYCd9G5k7DFQascAau40H3jxckh9rimIWa45FUSd7FIcddo1jeciv
gdAdiNUuHBen82O8KHJb+1PCBdA8RYeO5EGKfJM2yrOovu7dAFilf1ZPkXWgXnfG
nN6YfDDo9rAVDbvNXImrkwmGqEcN3Pq909IHiM/VETlU5lP4AbTNgrDa/aaZ+I+b
1MC1p87MvnibyXs+rTlK5+j8E6noNcD7tsHNd4ufkVCqr+pvSpuA3OvnXjbbm54=
-----END CERTIFICATE-----

696
packages/app/CHANGELOG.md Normal file
View File

@ -0,0 +1,696 @@
# v0.1.22
## Fixes
- Note creator too wide on mobile
- Sending notes dialog duplicated when replying
---
# v0.1.21
## Added
- Add gradients to iris.to domain
- Render referenced kind-3 (ContactList) as pubkey list
- List feed page renders the posts of a given list `/list-feed/{naddr-of-nip51-list-or-nevent-of-kind3}`
- Respond to AUTH when expected (Requesting DM's/GiftWrap)
- Show reply counts on threads
- Quote Repost
- Signature checks can be enabled in preferences
- NIP-98 auth for void.cat / nostr.build file uploaders
- Add `E` tag for direct replies
- File upload progress bar (void.cat only)
- Long form modal for deck layout (WIP still)
- Video thumbnails using ImgProxy
- Renew subscriptions for X months
- Tailwind CSS migration @mmalmi
- Seasonal features
- Profile cards on hover for mentions
- Dropdown search results on search bar @mmalmi
- Renew subscription task on task list
## Changed
- Disable highligher.js code blocks (for now)
- Removed "Popular Accounts" from new user flow, replaced with "Snort Devs" only
- Moved "Show Preview" on note creator to preview toggle switch
- Premium subscription renamed to PRO
- Limit images in posts to 800px high
- Nostrplebs colors removed
## Fixed
- Use correct hostname when submitting analytics
- Disable WASM when not supported on device
- Typo on "Nostr Address" in account settings
- Hide expired user status on profiles
- Hide muted dm chats
- Hide blocked replies
---
# v0.1.20
## Added
- Highlight text in search results - @fernandoporazzi
- Iris/Snort build configuration - @mmalmi
- Iris free NIP-05 on Profile page - @mmalmi
- Image galleries on posts - @fernandoporazzi
- Close modal with ESC - @mmalmi
- Navigate image spotlight with LR direction keys - @mmalmi
- Spotlight preview profile/banner on click - @mmalmi
- Fetch profiles from HTTP cache (Iris) - @mmalmi
- Animal names for empty profile accounts (Iris) - @mmalmi
- Redirect to NIP-05 short links for iris/snort accounts - @mmalmi
- Code block highlighting - @fernandoporazzi
- Notification summary graph - @Kieran
- Profile hover cards - @Kieran
- Keyboard shortcuts for new post/focus/search - @fernandoporazzi
- Markdown rendering for long form content - @Kieran
- Show relay response when publishing - @Kieran
## Fixed
- Copy buttons on insecure context - @Kieran
---
# v0.1.19
## Added
- Highlight search results
## Fixes
- Copy to clipboard on insecure context (Umbrel)
---
# v0.1.16
## Fixes
- Login bugs
---
# v0.1.15
## Added
- User status on profile pages (Music only [NIP-38])
- Following mark on avatars, if you follow the pubkey you will see a green tick on their avatar
- Pin encryption, encrypted private key storage for nsec login
- Pubkey (readonly) logins hide buttons which cannot be used (reactions, reply, save profiles, dms etc)
- Muted words feature (phase 1)
- NIP-28 public chats
## Changed
- Styles changes for Content warnings
- Live stream embed styles
- Cashu token embed styles
- Snort Deck thread navigation in modal from timeline
- PoW miner moved to WASM module for faster hashing
## Fixed
- Profile link to dms
- Long form content loading and replies
- Search function restored
---
# v0.1.14
## Added
- Timeline cache: faster page loads and much lower data usage
- WASM module: Some code moved to Rust WASM module for faster execution
- Zap Splits: NIP-57.G
- New Languages:
- Finnish
- Dutch
- Portuguese Brazilian
## Changed
- Count polls by pubkey
---
# v0.1.13
# Added
- Snort V2 Design
- NIP-24 Encrypted secret chats (nsec login only)
- NIP-13 Proof of Work (POW)
- NIP-31 Alt tag spec for unknown event kinds
- Render mentioned zap goals (Kind 9041)
- Embed fonts in src (No more google fonts requests)
- Native key storage for Android app (`Nip7os` interface)
- Swahili translations
- Thai translations
# Changed
- PWA pre-cache setup (Faster PWA loading)
- Show note creator button on profile pages
# Fixed
- Umlauts in urls
- Reject events which don't match request filter
---
# v0.1.12
# Added
- nsecBunker support (connection string `bunker://<pubkey>?relay=wss://realy.com[#token]`)
# Changed
- New snort logo by Bitko
- Infinite scroll changed to manual action (temperarily to fix performance issues)
# Fixed
- Note to self containing all DMS
- Media spotlight disabled for poll options containing images
- Badge image sizes oversize when bypassing imgproxy due to loading error
---
# v0.1.11
## Added
- `@snort/system` package
- `@snort/system-react` package
- Live streaming page (NIP-102)
- Chat system refactor (adding new chat systems much easier now, NIP-29 first candidate)
- NIP-29 simple group chat support
## Fixed
- Profile links with incorrect hrp fixed in some places
- `naddr` event loading fixed
- Relay specific requests fixed (Global tab / Search page)
- NWC connection responds to AUTH requests now
https://git.v0l.io/Kieran/snort/compare/v0.1.10...v0.1.11
---
# v0.1.10
## Added
- Gossip model, query follows write relays for events
- @snort/system NPM package containing Snort core nostr code
- NIP-44 Encryption scheme support
- NIP-59 Gift Wrap support
## Fixed
- Unmarked thread events replies out of order
https://git.v0l.io/Kieran/snort/compare/v0.1.9...v0.1.10
---
# v0.1.9
## Added
- Discover tab, shows trending users/posts from nostr.build
- New DM styles
- Mentioned Zapstr tracks are previewed on Snort with player
- Custom emoji rendering in posts (NIP-30)
- Lanaguage selector on new user flow
- ZapPool, support nostr ecosystem by donating a percentage of your zaps
- Alby NWC link added to NWC connect page
- SemisolDev follow recommendations on Discover tab
- Pubkey lists (NIP-51) render inline when mentioned in notes
- Persian language
- OpenGraph Image/Video media rendered inside link preview box
- Option to zap everybody on mentioned pubkey list
- L402 support for inline media (paywall content)
## Changed
- Error page shows actual error message now, also a button to reset app cache
- Massivly improved profile loading
- Improved JS bundle size by ejecting CRA and using dynamic modules
- Switched to `@void-cat/api` package for void.cat uploads
---
# v0.1.8
## Added
- Tamil Language support
- Quoted notes are rendered embedded
- Multi-account support for subscribers
- Zapper key loading processing in background to speed up profile loading
- Export keys page added to settings
- NIP-94 support for rendering quoted file metadata events
- Interactions cache (zaps/likes/reports) for better UX
- Full screen image/video previews in modal
- Re-broadcast own events dialog
- Nostr wallet connect support
- Cashu token parsing preview with redeem link
- Trending notes/people tabs added to search page
## Changed
- Profile page loads only 200 latest notes, improving profile load times for accounts with less activity
- New user flow has been tweaked to be shorter with NIP5 & Twitter import steps removed
## Fixed
- Thread navigation without page reload
- NIP-42 functionality restored
- `a` tagged kind 1 replies render properly under root event
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.7...v0.1.8
---
# v0.1.7
## Added
- Per event zap targets by @v0l
- Content warning (NIP-36) support by @v0l
- Polls (NIP-69) by @v0l
- Snort subscriptions by @v0l
- NIP-94 File header support by @v0l
- Link previews by @ghobs91 & @v0l
- Cmd+Enter to post note by @v0l
- `nostr:` links (NIP-27) by @v0l
- Tending users on Search page by @ghobs91 & @v0l
## Changed
- Paste image upload by @vivganes
- Note creator note preview by @v0l
- Login private key input masking by @vivganes
## Fixed
- Fix note creator closing on thread when new replies load by @SamSamskies
- Follow hashtag tab highlighting by @SamSamskies
- Language dropdown defaults to Arabic by @vivganes
- Bookmarks showing reactions by @vivganes
- Single zapper on note only shows name by @vivganes
- Broken link previews show empty box by @vivganes
- Render jfif images by @v0l
## PR List
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/476
- `nostr` package: implement NIP-05 by @sistemd in https://github.com/v0l/snort/pull/474
- `nostr` package: NIP-09 event deletion by @sistemd in https://github.com/v0l/snort/pull/478
- fix #484 by @vivganes in https://github.com/v0l/snort/pull/486
- fix #485 by @vivganes in https://github.com/v0l/snort/pull/487
- Per event zap targets by @v0l in https://github.com/v0l/snort/pull/466
- feat: nip-36 by @v0l in https://github.com/v0l/snort/pull/497
- fix #496 by @vivganes in https://github.com/v0l/snort/pull/498
- use redux for NoteCreator state management by @SamSamskies in https://github.com/v0l/snort/pull/494
- fix #495 by @vivganes in https://github.com/v0l/snort/pull/499
- Polls (NIP-69) by @v0l in https://github.com/v0l/snort/pull/489
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/483
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/508
- add ability to paste image from clipboard by @vivganes in https://github.com/v0l/snort/pull/510
- Subscriptions by @v0l in https://github.com/v0l/snort/pull/506
- feat: multi-account system by @v0l in https://github.com/v0l/snort/pull/514
- fix followed tag active tab highlighting by @SamSamskies in https://github.com/v0l/snort/pull/516
- NIP-94 file headers by @v0l in https://github.com/v0l/snort/pull/488
- fix #517 by @vivganes in https://github.com/v0l/snort/pull/518
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/511
- `nostr` package: get tests passing in the browser by @sistemd in https://github.com/v0l/snort/pull/490
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/519
- Subscription handle by @v0l in https://github.com/v0l/snort/pull/522
## New Contributors
- @vivganes made their first contribution in https://github.com/v0l/snort/pull/486
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.6...v0.1.7
---
# v0.1.6
## 🏷️ Summary
- Snort NIP5 management page (for transfers to new pubkeys)
- Short links for Snort NIP5 owners (ie. https://snort.social/kieran)
## Other Changes
- Update Wavlake embed to support .com links by @blastshielddown in https://github.com/v0l/snort/pull/469
- Bug fixes for save profile & relay connection on clean browser
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.5...v0.1.6
---
# v0.1.5
## 🏷️ Short Summary
- Completely rebuilt "core" subscription management system
- Option to rewrite Twitter links to Nitter links
- Tarui app setup, Mac/Windows/Linux desktop apps (coming soon)
- OpenGraph tagging for profiles and events (Only for https://snort.social)
- NIP-27 `nostr:` link parsing
- Global tab full relay names
## What's Changed
- `nostr` package: add direct messages by @sistemd in https://github.com/v0l/snort/pull/399
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/445
- Display search property alongside host in relay name by @h3y6e in https://github.com/v0l/snort/pull/452
- Shorten long relay name by @h3y6e in https://github.com/v0l/snort/pull/455
- Nostr links by @v0l in https://github.com/v0l/snort/pull/461
- `nostr` package: vastly simplify the API by @sistemd in https://github.com/v0l/snort/pull/412
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/453
- Fix: invisible <option> text in dark theme by @jiftechnify in https://github.com/v0l/snort/pull/454
- add setting for rewriting twitter links to nitter by @w3irdrobot in https://github.com/v0l/snort/pull/459
- RequestBuilder / Core Refactor by @v0l in https://github.com/v0l/snort/pull/326
- Tauri setup by @v0l in https://github.com/v0l/snort/pull/462
- Prevents adding ws relay when over https by @ivanacostarubio in https://github.com/v0l/snort/pull/463
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/465
- OpenGraph tag injection by @v0l in https://github.com/v0l/snort/pull/470
## New Contributors
- @jiftechnify made their first contribution in https://github.com/v0l/snort/pull/454
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.4...v0.1.5
---
# v0.1.4
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.3...v0.1.4
---
# v0.1.3
## What's Changed
- only replace note ID when note ID starts with `@` character by @SamSamskies in https://github.com/v0l/snort/pull/441
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.2...v0.1.3
---
# v0.1.2
## What's Changed
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/309
- UI bugs by @verbiricha in https://github.com/v0l/snort/pull/301
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/311
- Add build command to readme by @joshr4 in https://github.com/v0l/snort/pull/300
- fix(fotter-actions): add highlighting and min-width by @fernandolguevara in https://github.com/v0l/snort/pull/312
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/313
- `nostr` package part 1 by @fcked in https://github.com/v0l/snort/pull/315
- Reduce space between the texts for selecting relays by @h3y6e in https://github.com/v0l/snort/pull/316
- fix(profile): convert page id to npub bech32 by @fernandolguevara in https://github.com/v0l/snort/pull/322
- Improve overflow menu button by @joshr4 in https://github.com/v0l/snort/pull/304
- German translations for snort by @gandlafbtc in https://github.com/v0l/snort/pull/323
- fix: send all relays when zapping by @verbiricha in https://github.com/v0l/snort/pull/324
- Add default page selector by @jacany in https://github.com/v0l/snort/pull/321
- UI fixes by @verbiricha in https://github.com/v0l/snort/pull/318
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/319
- feat: render kind 1 reposts by @kphrx in https://github.com/v0l/snort/pull/314
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/330
- Use inner note content as comment by @Semisol in https://github.com/v0l/snort/pull/333
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/331
- Fix stale relays by @SamSamskies in https://github.com/v0l/snort/pull/337
- Feed cache rework by @v0l in https://github.com/v0l/snort/pull/339
- fix long zap comment text overflow by @SamSamskies in https://github.com/v0l/snort/pull/344
- fix links in parentheses by @SamSamskies in https://github.com/v0l/snort/pull/347
- Revert "Merge pull request #347 from v0l/fix-links-in-parentheses" by @SamSamskies in https://github.com/v0l/snort/pull/350
- Update thread detection to not include mentions by @w3irdrobot in https://github.com/v0l/snort/pull/351
- Small settings page stuff by @w3irdrobot in https://github.com/v0l/snort/pull/353
- Change message unread color to purple by @w3irdrobot in https://github.com/v0l/snort/pull/354
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/356
- Remove unread message dot when messages all read by @w3irdrobot in https://github.com/v0l/snort/pull/355
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/359
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/362
- `nostr` package part 2 by @fcked in https://github.com/v0l/snort/pull/346
- feat: add search page field autofocus by @lujakob in https://github.com/v0l/snort/pull/363
- fix URL parsing edge cases by @SamSamskies in https://github.com/v0l/snort/pull/360
- Fast Zaps ⚡ by @v0l in https://github.com/v0l/snort/pull/370
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/364
- Feat/add spinner to button by @lujakob in https://github.com/v0l/snort/pull/368
- Update mark all read dm button to be disabled when no unreads by @w3irdrobot in https://github.com/v0l/snort/pull/373
- `nostr` package part 3 by @fcked in https://github.com/v0l/snort/pull/365
- LNDHub/LNC wallet by @v0l in https://github.com/v0l/snort/pull/219
- Proposal: Remove SVGs from JSX by @enjikaka in https://github.com/v0l/snort/pull/382
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/374
- add Nostr Nests embed by @SamSamskies in https://github.com/v0l/snort/pull/377
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/387
- fix icons by @h3y6e in https://github.com/v0l/snort/pull/392
- Fix broken note links by @SamSamskies in https://github.com/v0l/snort/pull/380
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/391
- fix(BackButton): vertical align styles by @lujakob in https://github.com/v0l/snort/pull/397
- feat(note): open note in new tab on cmd press by @lujakob in https://github.com/v0l/snort/pull/395
- fix(skeleton): dark theme styles by @lujakob in https://github.com/v0l/snort/pull/393
- fix HyperText matching by @mattn in https://github.com/v0l/snort/pull/405
- Makes entire note clickable by @d-r-w in https://github.com/v0l/snort/pull/371
- render webm links as inline videos by @SamSamskies in https://github.com/v0l/snort/pull/410
- render embed for youtube live links by @SamSamskies in https://github.com/v0l/snort/pull/407
- do not render reposts of badge award events in timelines by @SamSamskies in https://github.com/v0l/snort/pull/406
- `nostr` package: use `EventEmitter` by @fcked in https://github.com/v0l/snort/pull/384
- `nostr` pacakge: implement basic NIP-20 `OK` functionality by @fcked in https://github.com/v0l/snort/pull/385
- feat: read nip-58 badges by @verbiricha in https://github.com/v0l/snort/pull/394
- Add Wavlake embed by @blastshielddown in https://github.com/v0l/snort/pull/416
- display search results on page load if query in url by @SamSamskies in https://github.com/v0l/snort/pull/415
- Fix event mention bug by @SamSamskies in https://github.com/v0l/snort/pull/421
- fix NaN when parsing empty string by @SamSamskies in https://github.com/v0l/snort/pull/422
- NIP06 support by @w3irdrobot in https://github.com/v0l/snort/pull/425
- Added key attr to Tabs to remove React warning by @w3irdrobot in https://github.com/v0l/snort/pull/424
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/426
- New Crowdin updates by @v0l in https://github.com/v0l/snort/pull/436
- Update Wavlake embed URL, add support for album & artist links by @blastshielddown in https://github.com/v0l/snort/pull/439
- build(deps): bump webpack from 5.75.0 to 5.76.1 by @dependabot in https://github.com/v0l/snort/pull/442
## New Contributors
- @joshr4 made their first contribution in https://github.com/v0l/snort/pull/300
- @gandlafbtc made their first contribution in https://github.com/v0l/snort/pull/323
- @jacany made their first contribution in https://github.com/v0l/snort/pull/321
- @kphrx made their first contribution in https://github.com/v0l/snort/pull/314
- @lujakob made their first contribution in https://github.com/v0l/snort/pull/363
- @mattn made their first contribution in https://github.com/v0l/snort/pull/405
- @d-r-w made their first contribution in https://github.com/v0l/snort/pull/371
- @blastshielddown made their first contribution in https://github.com/v0l/snort/pull/416
- @dependabot made their first contribution in https://github.com/v0l/snort/pull/442
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.1...v0.1.2
---
# v0.1.1
## What's Changed
- React Map Optimization, [missing map keys ] by @ahmedrowaihi in https://github.com/v0l/snort/pull/283
- Translate '/src/lang.json' in 'ar' by @transifex-integration in https://github.com/v0l/snort/pull/287
- HTML auto direction for specific textual content by @verbiricha in https://github.com/v0l/snort/pull/286
- Translate '/src/lang.json' in 'hu' by @transifex-integration in https://github.com/v0l/snort/pull/288
- feat: twitch embed by @v0l in https://github.com/v0l/snort/pull/289
- fix: don't show 0 if there is no description by @verbiricha in https://github.com/v0l/snort/pull/290
- feat: pinned notes and bookmarks by @verbiricha in https://github.com/v0l/snort/pull/255
- Translate '/src/lang.json' in 'ja' by @transifex-integration in https://github.com/v0l/snort/pull/293
- fix: set auto to whole text content instead of individual paragraphs by @verbiricha in https://github.com/v0l/snort/pull/292
- protocol handler by @v0l in https://github.com/v0l/snort/pull/164
- feat: read global from specific (paid) relays by @v0l in https://github.com/v0l/snort/pull/249
- SUPPORT RTL/LTR ON LOGIN PAGE by @ahmedrowaihi in https://github.com/v0l/snort/pull/291
- Add Apple Music embed by @SamSamskies in https://github.com/v0l/snort/pull/294
- Workspace with decoupled `nostr` package by @ennmichael in https://github.com/v0l/snort/pull/274
- Translate '/src/lang.json' in 'ar' by @transifex-integration in https://github.com/v0l/snort/pull/296
- UI fixes + counts on profile page tabs by @verbiricha in https://github.com/v0l/snort/pull/282
- Fix blackout when selecting global tab by @h3y6e in https://github.com/v0l/snort/pull/297
- Prevent profile text from overflowing flex container when it is too long by @h3y6e in https://github.com/v0l/snort/pull/298
## New Contributors
- @ahmedrowaihi made their first contribution in https://github.com/v0l/snort/pull/283
**Full Changelog**: https://github.com/v0l/snort/compare/v0.1.0...v0.1.1
---
# v0.1.0
## What's Changed
- Add global tab to Root by @p2pseed in https://github.com/v0l/snort/pull/3
- UI improvements by @verbiricha in https://github.com/v0l/snort/pull/4
- fix: dedupe thread pubkeys by @verbiricha in https://github.com/v0l/snort/pull/7
- Note creator improvement by @verbiricha in https://github.com/v0l/snort/pull/6
- fix: correctly follow user mention links by @verbiricha in https://github.com/v0l/snort/pull/5
- fix: force timeline rerender on tab change by @verbiricha in https://github.com/v0l/snort/pull/8
- feat: add mov to video files by @verbiricha in https://github.com/v0l/snort/pull/9
- feat: copy npub on profile by @verbiricha in https://github.com/v0l/snort/pull/10
- fix: display full lightning address, is trimmed if too long by @verbiricha in https://github.com/v0l/snort/pull/12
- feat: embed youtube videos by @verbiricha in https://github.com/v0l/snort/pull/13
- feat: add support for positive and negative reactions by @verbiricha in https://github.com/v0l/snort/pull/11
- feat: nip05 on profile page by @verbiricha in https://github.com/v0l/snort/pull/21
- UI improvements by @verbiricha in https://github.com/v0l/snort/pull/24
- Home tabs by @v0l in https://github.com/v0l/snort/pull/25
- fix: support m.youtube.com subdomain links by @verbiricha in https://github.com/v0l/snort/pull/27
- fix: use all available width for note creator text area by @verbiricha in https://github.com/v0l/snort/pull/28
- highlight hashtags by @verbiricha in https://github.com/v0l/snort/pull/29
- UI tweaks by @verbiricha in https://github.com/v0l/snort/pull/30
- Improve regexes by @verbiricha in https://github.com/v0l/snort/pull/32
- Shows QR code first by @ivanacostarubio in https://github.com/v0l/snort/pull/23
- feat: embed tweets by @verbiricha in https://github.com/v0l/snort/pull/33
- Nip5 shop by @v0l in https://github.com/v0l/snort/pull/50
- Activate snort.social NIP-5 service by @v0l in https://github.com/v0l/snort/pull/51
- feat: add avatar borders with color gradients to partner nip05 providers by @verbiricha in https://github.com/v0l/snort/pull/52
- DM's by @v0l in https://github.com/v0l/snort/pull/54
- feat: display banner in profile by @verbiricha in https://github.com/v0l/snort/pull/53
- add max width to details by @verbiricha in https://github.com/v0l/snort/pull/59
- Markdown by @verbiricha in https://github.com/v0l/snort/pull/55
- feat: mentions by @verbiricha in https://github.com/v0l/snort/pull/56
- Minor UI fixes by @verbiricha in https://github.com/v0l/snort/pull/63
- add user DB and cache nip-05 verifications by @verbiricha in https://github.com/v0l/snort/pull/65
- fix: adjust nip05 size by @verbiricha in https://github.com/v0l/snort/pull/66
- fix: dont display display_name as nip user when username is default by @verbiricha in https://github.com/v0l/snort/pull/67
- fix: don't retry errored verifications by @verbiricha in https://github.com/v0l/snort/pull/71
- UI improvements by @verbiricha in https://github.com/v0l/snort/pull/70
- refactor: TS by @v0l in https://github.com/v0l/snort/pull/69
- More TSX by @v0l in https://github.com/v0l/snort/pull/74
- Moar UI fixes by @verbiricha in https://github.com/v0l/snort/pull/73
- fix: autocomplete colors by @verbiricha in https://github.com/v0l/snort/pull/75
- feat: query for autocompletion using local db by @verbiricha in https://github.com/v0l/snort/pull/76
- fix: rerender user timeline on pubkey change by @verbiricha in https://github.com/v0l/snort/pull/77
- feat: follows you on profile page by @ivanacostarubio in https://github.com/v0l/snort/pull/64
- autocomplete improvements by @verbiricha in https://github.com/v0l/snort/pull/83
- filter for self dms by @LiranCohen in https://github.com/v0l/snort/pull/86
- Notifications by @v0l in https://github.com/v0l/snort/pull/88
- Theme by @verbiricha in https://github.com/v0l/snort/pull/87
- Modified self-dm to be a "Note to Self" by @LiranCohen in https://github.com/v0l/snort/pull/89
- note footer ordering by @verbiricha in https://github.com/v0l/snort/pull/91
- Hashtags by @v0l in https://github.com/v0l/snort/pull/92
- Make logo cursor a pointer by @w3irdrobot in https://github.com/v0l/snort/pull/99
- fix: active note colors by @verbiricha in https://github.com/v0l/snort/pull/102
- Tidal embeds by @v0l in https://github.com/v0l/snort/pull/95
- UI improvements by @verbiricha in https://github.com/v0l/snort/pull/103
- User preferences by @v0l in https://github.com/v0l/snort/pull/104
- Add note context menu by @v0l in https://github.com/v0l/snort/pull/105
- feat: soundcloud embed by @ivanacostarubio in https://github.com/v0l/snort/pull/112
- feat: Show latest by @v0l in https://github.com/v0l/snort/pull/113
- light theme fixes by @verbiricha in https://github.com/v0l/snort/pull/116
- add Karnage to contributors by @verbiricha in https://github.com/v0l/snort/pull/117
- feat: note mentions by @verbiricha in https://github.com/v0l/snort/pull/125
- Preferences & Profile changes by @FlannelDipole in https://github.com/v0l/snort/pull/126
- sort bug in the event that your pubkey is the 2nd item in the list by @LiranCohen in https://github.com/v0l/snort/pull/137
- UI updates by @verbiricha in https://github.com/v0l/snort/pull/135
- fix: hide note creator on send by @verbiricha in https://github.com/v0l/snort/pull/139
- fix: add bottom margin to thread by @verbiricha in https://github.com/v0l/snort/pull/140
- adds mixcloud by @ivanacostarubio in https://github.com/v0l/snort/pull/136
- feat: audio player by @verbiricha in https://github.com/v0l/snort/pull/146
- feat: in-memory fallback for storing user profiles by @verbiricha in https://github.com/v0l/snort/pull/110
- Make Markdown more interoperable by @fiatjaf in https://github.com/v0l/snort/pull/153
- fix: default to in-memory db only on db read fail by @verbiricha in https://github.com/v0l/snort/pull/155
- bug: logout reply by @ivanacostarubio in https://github.com/v0l/snort/pull/154
- Search by @v0l in https://github.com/v0l/snort/pull/143
- Nip42 (AUTH) by @v0l in https://github.com/v0l/snort/pull/144
- Muted list via NIP-51 by @verbiricha in https://github.com/v0l/snort/pull/151
- Add more relays (high performance) by @Semisol in https://github.com/v0l/snort/pull/149
- Show absolute time on hover by @wanacode in https://github.com/v0l/snort/pull/166
- nostr.build file uploads by @v0l in https://github.com/v0l/snort/pull/162
- New UI by @v0l in https://github.com/v0l/snort/pull/161
- fix: send d tags as list by @verbiricha in https://github.com/v0l/snort/pull/169
- Image proxy service by @v0l in https://github.com/v0l/snort/pull/174
- Translate notes by @v0l in https://github.com/v0l/snort/pull/179
- Use standard imgproxy by @v0l in https://github.com/v0l/snort/pull/180
- Fix races where Socket is closed before Websocket is created by @brugeman in https://github.com/v0l/snort/pull/186
- feat: nostrimg.com by @v0l in https://github.com/v0l/snort/pull/181
- Add Spotify embed by @SamSamskies in https://github.com/v0l/snort/pull/188
- Ln invoice styling by @verbiricha in https://github.com/v0l/snort/pull/187
- feed cache by @v0l in https://github.com/v0l/snort/pull/184
- bug: prepends https when missing from website by @ivanacostarubio in https://github.com/v0l/snort/pull/194
- Use the current embed player via TIDALs OEmbed API. by @enjikaka in https://github.com/v0l/snort/pull/191
- feat: zaps by @verbiricha in https://github.com/v0l/snort/pull/78
- display note zaps succintly by @verbiricha in https://github.com/v0l/snort/pull/196
- nostr-pub.semisol.dev is now atlas.nostr.land by @Semisol in https://github.com/v0l/snort/pull/198
- Zaps fixes by @verbiricha in https://github.com/v0l/snort/pull/199
- Note creator improvements by @verbiricha in https://github.com/v0l/snort/pull/193
- Settings page and UI tweaks by @verbiricha in https://github.com/v0l/snort/pull/200
- fix avatars by @verbiricha in https://github.com/v0l/snort/pull/203
- Skeleton component on timeline loading for better user experience by @leotuna in https://github.com/v0l/snort/pull/190
- Threads by @verbiricha in https://github.com/v0l/snort/pull/170
- fix: don't stream global feed in notifications tab by @verbiricha in https://github.com/v0l/snort/pull/207
- Zap modal by @verbiricha in https://github.com/v0l/snort/pull/209
- Add prettier formatting by @ennmichael in https://github.com/v0l/snort/pull/214
- react-intl spike by @SamSamskies in https://github.com/v0l/snort/pull/216
- Add support for zh and ja locales by @SamSamskies in https://github.com/v0l/snort/pull/218
- feat: reactions modal by @verbiricha in https://github.com/v0l/snort/pull/215
- Translate '/src/translations/en.json' in 'es' by @transifex-integration in https://github.com/v0l/snort/pull/224
- Translate '/src/translations/en.json' in 'ja' by @transifex-integration in https://github.com/v0l/snort/pull/227
- fix: allow zap comments by @verbiricha in https://github.com/v0l/snort/pull/229
- Eslint by @v0l in https://github.com/v0l/snort/pull/223
- Translate '/src/translations/en.json' in 'fr' by @transifex-integration in https://github.com/v0l/snort/pull/230
- add ability to use babel plugins without ejecting by @SamSamskies in https://github.com/v0l/snort/pull/225
- add prettier pre-commit hook by @SamSamskies in https://github.com/v0l/snort/pull/234
- oversight of intl by @h3y6e in https://github.com/v0l/snort/pull/231
- feat: new login page by @v0l in https://github.com/v0l/snort/pull/235
- feat: onboarding by @verbiricha in https://github.com/v0l/snort/pull/233
- Translate '/src/translations/en.json' in 'ja' by @transifex-integration in https://github.com/v0l/snort/pull/243
- Translate '/src/translations/en.json' in 'fr' by @transifex-integration in https://github.com/v0l/snort/pull/242
- Translate '/src/translations/en.json' in 'es' by @transifex-integration in https://github.com/v0l/snort/pull/241
- feat: break lang by @v0l in https://github.com/v0l/snort/pull/244
- fix(missing-event): avoid redirect by @fernandolguevara in https://github.com/v0l/snort/pull/246
- fix(content): render media content for current pubkey by @fernandolguevara in https://github.com/v0l/snort/pull/240
- remove follow button from reactions modal by @SamSamskies in https://github.com/v0l/snort/pull/247
- NIP-65: Relay list metada by @verbiricha in https://github.com/v0l/snort/pull/238
- Fix DM page UI by @SamSamskies in https://github.com/v0l/snort/pull/250
- Translate '/src/lang.json' in 'es' by @transifex-integration in https://github.com/v0l/snort/pull/252
- Translate '/src/lang.json' in 'es' [manual sync] by @transifex-integration in https://github.com/v0l/snort/pull/258
- Translate '/src/lang.json' in 'fr' [manual sync] by @transifex-integration in https://github.com/v0l/snort/pull/259
- Translate '/src/lang.json' in 'hu' [manual sync] by @transifex-integration in https://github.com/v0l/snort/pull/260
- Translate '/src/lang.json' in 'ja' [manual sync] by @transifex-integration in https://github.com/v0l/snort/pull/261
- Translate '/src/lang.json' in 'zh' [manual sync] by @transifex-integration in https://github.com/v0l/snort/pull/262
- Translate '/src/lang.json' in 'ja' by @transifex-integration in https://github.com/v0l/snort/pull/275
- Translate '/src/lang.json' in 'id' by @transifex-integration in https://github.com/v0l/snort/pull/277
- Translate '/src/lang.json' in 'zh' by @transifex-integration in https://github.com/v0l/snort/pull/278
- Translate '/src/lang.json' in 'es' by @transifex-integration in https://github.com/v0l/snort/pull/279
- Translate '/src/lang.json' in 'hu' by @transifex-integration in https://github.com/v0l/snort/pull/280
- Translate '/src/lang.json' in 'fr' by @transifex-integration in https://github.com/v0l/snort/pull/281
## New Contributors
- @p2pseed made their first contribution in https://github.com/v0l/snort/pull/3
- @v0l made their first contribution in https://github.com/v0l/snort/pull/25
- @ivanacostarubio made their first contribution in https://github.com/v0l/snort/pull/23
- @w3irdrobot made their first contribution in https://github.com/v0l/snort/pull/99
- @FlannelDipole made their first contribution in https://github.com/v0l/snort/pull/126
- @fiatjaf made their first contribution in https://github.com/v0l/snort/pull/153
- @Semisol made their first contribution in https://github.com/v0l/snort/pull/149
- @wanacode made their first contribution in https://github.com/v0l/snort/pull/166
- @SamSamskies made their first contribution in https://github.com/v0l/snort/pull/188
- @enjikaka made their first contribution in https://github.com/v0l/snort/pull/191
- @leotuna made their first contribution in https://github.com/v0l/snort/pull/190
- @transifex-integration made their first contribution in https://github.com/v0l/snort/pull/224
- @h3y6e made their first contribution in https://github.com/v0l/snort/pull/231
- @fernandolguevara made their first contribution in https://github.com/v0l/snort/pull/246
**Full Changelog**: https://github.com/v0l/snort/commits/v0.1.0

View File

@ -2,9 +2,24 @@
"appName": "Snort",
"appNameCapitalized": "Snort",
"appTitle": "Snort - Nostr",
"hostname": "snort.social",
"nip05Domain": "snort.social",
"favicon": "public/favicon.ico",
"appleTouchIconUrl": "/nostrich_512.png",
"httpCache": "",
"animalNamePlaceholders": false
"animalNamePlaceholders": false,
"defaultZapPoolFee": 0.5,
"features": {
"analytics": true,
"subscriptions": true,
"deck": true,
"zapPool": true
},
"eventLinkPrefix": "nevent",
"profileLinkPrefix": "nprofile",
"defaultRelays": {
"wss://relay.snort.social/": { "read": true, "write": true },
"wss://nostr.wine/": { "read": true, "write": false },
"wss://nos.lol/": { "read": true, "write": true }
}
}

View File

@ -2,9 +2,23 @@
"appName": "iris",
"appNameCapitalized": "Iris",
"appTitle": "iris",
"hostname": "iris.to",
"nip05Domain": "iris.to",
"favicon": "public/iris/favicon.ico",
"appleTouchIconUrl": "/img/apple-touch-icon.png",
"httpCache": "https://api.iris.to",
"animalNamePlaceholders": true
"animalNamePlaceholders": true,
"features": {
"analytics": true,
"subscriptions": false,
"deck": true,
"zapPool": true
},
"eventLinkPrefix": "note",
"profileLinkPrefix": "npub",
"defaultRelays": {
"wss://relay.snort.social/": { "read": true, "write": true },
"wss://nostr.wine/": { "read": true, "write": false },
"wss://nos.lol/": { "read": true, "write": true }
}
}

View File

@ -30,7 +30,44 @@ declare module "translations/*.json" {
export default value;
}
declare module "*.md" {
const value: string;
export default value;
}
declare module "emojilib" {
const value: Record<string, string>;
export default value;
}
declare const CONFIG: {
appName: string;
appNameCapitalized: string;
appTitle: string;
hostname: string;
nip05Domain: string;
favicon: string;
appleTouchIconUrl: string;
httpCache: string;
animalNamePlaceholders: boolean;
defaultZapPoolFee?: number;
features: {
analytics: boolean;
subscriptions: boolean;
deck: boolean;
zapPool: boolean;
};
eventLinkPrefix: NostrPrefix;
profileLinkPrefix: NostrPrefix;
defaultRelays: Record<string, RelaySettings>;
};
/**
* Single relay (Debug)
*/
declare const SINGLE_RELAY: string | undefined;
/**
* Build git hash
*/
declare const GIT_VERSION: string;

View File

@ -13,14 +13,19 @@
"@snort/system": "workspace:*",
"@snort/system-react": "workspace:*",
"@snort/system-wasm": "workspace:*",
"@snort/system-web": "workspace:*",
"@szhsin/react-menu": "^3.3.1",
"@types/use-sync-external-store": "^0.0.4",
"@void-cat/api": "^1.0.4",
"@uidotdev/usehooks": "^2.3.1",
"@void-cat/api": "^1.0.10",
"classnames": "^2.3.2",
"debug": "^4.3.4",
"dexie": "^3.2.4",
"emojilib": "^3.0.10",
"highlight.js": "^11.8.0",
"light-bolt11-decoder": "^2.1.0",
"marked": "^9.1.0",
"marked-footnote": "^1.0.0",
"match-sorter": "^6.3.1",
"qr-code-styling": "^1.6.0-rc.1",
"react": "^18.2.0",
@ -30,6 +35,7 @@
"react-router-dom": "^6.5.0",
"react-textarea-autosize": "^8.4.0",
"react-twitter-embed": "^4.0.4",
"recharts": "^2.8.0",
"use-long-press": "^3.2.0",
"use-sync-external-store": "^1.2.0",
"uuid": "^9.0.0",
@ -87,6 +93,7 @@
"@webbtc/webln-types": "^1.0.10",
"@webpack-cli/generators": "^3.0.4",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.3",
"config": "^3.3.9",
"copy-webpack-plugin": "^11.0.0",
@ -98,9 +105,13 @@
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"mini-css-extract-plugin": "^2.7.5",
"postcss": "^8.4.31",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.2.0",
"prettier": "2.8.3",
"prop-types": "^15.8.1",
"source-map-loader": "^4.0.1",
"tailwindcss": "^3.3.3",
"terser-webpack-plugin": "^5.3.9",
"tinybench": "^2.5.1",
"ts-jest": "^29.1.1",

View File

@ -0,0 +1,3 @@
module.exports = {
plugins: [require("tailwindcss"), require("autoprefixer")],
};

View File

@ -24,9 +24,9 @@
<symbol id="bookmark" viewBox="0 0 12 14" fill="none">
<path d="M1.3335 4.2C1.3335 3.0799 1.3335 2.51984 1.55148 2.09202C1.74323 1.71569 2.04919 1.40973 2.42552 1.21799C2.85334 1 3.41339 1 4.5335 1H7.46683C8.58693 1 9.14699 1 9.57481 1.21799C9.95114 1.40973 10.2571 1.71569 10.4488 2.09202C10.6668 2.51984 10.6668 3.0799 10.6668 4.2V13L6.00016 10.3333L1.3335 13V4.2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<symbol id="check" viewBox="0 0 18 13" fill="none">
<path d="M17 1L6 12L1 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<svg id="check" viewBox="0 0 24 25" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7071 5.79289C21.0976 6.18342 21.0976 6.81658 20.7071 7.20711L9.70711 18.2071C9.31658 18.5976 8.68342 18.5976 8.29289 18.2071L3.29289 13.2071C2.90237 12.8166 2.90237 12.1834 3.29289 11.7929C3.68342 11.4024 4.31658 11.4024 4.70711 11.7929L9 16.0858L19.2929 5.79289C19.6834 5.40237 20.3166 5.40237 20.7071 5.79289Z" fill="currentColor"/>
</svg>
<symbol id="chevronDown" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z" fill="currentColor" />
</symbol>
@ -130,8 +130,8 @@
<symbol id="pie-chart" viewBox="0 0 22 22" fill="none">
<path d="M20.2104 14.8901C19.5742 16.3946 18.5792 17.7203 17.3123 18.7514C16.0454 19.7825 14.5452 20.4875 12.9428 20.8048C11.3405 21.1222 9.68483 21.0422 8.12055 20.5719C6.55627 20.1015 5.13103 19.2551 3.96942 18.1067C2.80782 16.9583 1.94522 15.5428 1.45704 13.984C0.968859 12.4252 0.869965 10.7706 1.169 9.1647C1.46804 7.55885 2.1559 6.0507 3.17245 4.7721C4.189 3.4935 5.50329 2.48339 7.0004 1.83007M20.2392 7.17323C20.6395 8.1397 20.8851 9.16143 20.9684 10.2009C20.989 10.4577 20.9993 10.5861 20.9483 10.7018C20.9057 10.7984 20.8213 10.8898 20.7284 10.94C20.6172 11.0001 20.4783 11.0001 20.2004 11.0001H11.8004C11.5204 11.0001 11.3804 11.0001 11.2734 10.9456C11.1793 10.8976 11.1028 10.8211 11.0549 10.7271C11.0004 10.6201 11.0004 10.4801 11.0004 10.2001V1.80007C11.0004 1.5222 11.0004 1.38327 11.0605 1.27205C11.1107 1.17915 11.2021 1.09476 11.2987 1.05216C11.4144 1.00117 11.5428 1.01146 11.7996 1.03205C12.839 1.11539 13.8608 1.36095 14.8272 1.76127C16.0405 2.26382 17.1429 3.00042 18.0715 3.929C19.0001 4.85759 19.7367 5.95998 20.2392 7.17323Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<symbol id="diamond" viewBox="0 0 22 20" fill="none">
<path d="M1.49954 7H20.4995M8.99954 1L6.99954 7L10.9995 18.5L14.9995 7L12.9995 1M11.6141 18.2625L20.5727 7.51215C20.7246 7.32995 20.8005 7.23885 20.8295 7.13717C20.8551 7.04751 20.8551 6.95249 20.8295 6.86283C20.8005 6.76114 20.7246 6.67005 20.5727 6.48785L16.2394 1.28785C16.1512 1.18204 16.1072 1.12914 16.0531 1.09111C16.0052 1.05741 15.9518 1.03238 15.8953 1.01717C15.8314 1 15.7626 1 15.6248 1H6.37424C6.2365 1 6.16764 1 6.10382 1.01717C6.04728 1.03238 5.99385 1.05741 5.94596 1.09111C5.89192 1.12914 5.84783 1.18204 5.75966 1.28785L1.42633 6.48785C1.2745 6.67004 1.19858 6.76114 1.16957 6.86283C1.144 6.95249 1.144 7.04751 1.16957 7.13716C1.19858 7.23885 1.2745 7.32995 1.42633 7.51215L10.385 18.2625C10.596 18.5158 10.7015 18.6424 10.8279 18.6886C10.9387 18.7291 11.0603 18.7291 11.1712 18.6886C11.2975 18.6424 11.4031 18.5158 11.6141 18.2625Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<symbol id="diamond" viewBox="0 0 20 20" fill="none">
<path d="M2.08295 7.5H17.9163M8.33295 2.5L6.66628 7.5L9.99961 17.0833L13.3329 7.5L11.6663 2.5M10.5118 16.8854L17.9773 7.92679C18.1038 7.77496 18.1671 7.69905 18.1912 7.6143C18.2126 7.53959 18.2126 7.46041 18.1912 7.38569C18.1671 7.30095 18.1038 7.22504 17.9773 7.07321L14.3662 2.73988C14.2927 2.6517 14.256 2.60762 14.2109 2.57592C14.171 2.54784 14.1265 2.52698 14.0794 2.51431C14.0262 2.5 13.9688 2.5 13.854 2.5H6.1452C6.03042 2.5 5.97303 2.5 5.91985 2.51431C5.87273 2.52698 5.82821 2.54784 5.7883 2.57592C5.74327 2.60762 5.70653 2.6517 5.63305 2.73988L2.02194 7.07321C1.89541 7.22504 1.83215 7.30095 1.80798 7.38569C1.78666 7.46041 1.78666 7.53959 1.80798 7.6143C1.83215 7.69904 1.89541 7.77496 2.02194 7.92679L9.48747 16.8854C9.66335 17.0965 9.75129 17.202 9.85657 17.2405C9.94894 17.2743 10.0503 17.2743 10.1427 17.2405C10.2479 17.202 10.3359 17.0965 10.5118 16.8854Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="wallet" viewBox="0 0 22 18" fill="none">
<path d="M19 6.5V4.2C19 3.0799 19 2.51984 18.782 2.09202C18.5903 1.7157 18.2843 1.40974 17.908 1.21799C17.4802 1 16.9201 1 15.8 1H4.2C3.0799 1 2.51984 1 2.09202 1.21799C1.7157 1.40973 1.40973 1.71569 1.21799 2.09202C1 2.51984 1 3.0799 1 4.2V13.8C1 14.9201 1 15.4802 1.21799 15.908C1.40973 16.2843 1.71569 16.5903 2.09202 16.782C2.51984 17 3.07989 17 4.2 17L15.8 17C16.9201 17 17.4802 17 17.908 16.782C18.2843 16.5903 18.5903 16.2843 18.782 15.908C19 15.4802 19 14.9201 19 13.8V11.5M14 9C14 8.53535 14 8.30302 14.0384 8.10982C14.1962 7.31644 14.8164 6.69624 15.6098 6.53843C15.803 6.5 16.0353 6.5 16.5 6.5H18.5C18.9647 6.5 19.197 6.5 19.3902 6.53843C20.1836 6.69624 20.8038 7.31644 20.9616 8.10982C21 8.30302 21 8.53535 21 9C21 9.46466 21 9.69698 20.9616 9.89018C20.8038 10.6836 20.1836 11.3038 19.3902 11.4616C19.197 11.5 18.9647 11.5 18.5 11.5H16.5C16.0353 11.5 15.803 11.5 15.6098 11.4616C14.8164 11.3038 14.1962 10.6836 14.0384 9.89018C14 9.69698 14 9.46465 14 9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@ -339,9 +339,48 @@
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12.5008C9 10.8439 10.3431 9.50076 12 9.50076C13.6569 9.50076 15 10.8439 15 12.5008C15 14.1576 13.6569 15.5008 12 15.5008C10.3431 15.5008 9 14.1576 9 12.5008Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="shield-tick" viewBox="0 0 24 24" fill="none">
<path d="M9 11.5L11 13.5L15.5 8.99999M20 12C20 16.9084 14.646 20.4784 12.698 21.6149C12.4766 21.744 12.3659 21.8086 12.2097 21.8421C12.0884 21.8681 11.9116 21.8681 11.7903 21.8421C11.6341 21.8086 11.5234 21.744 11.302 21.6149C9.35396 20.4784 4 16.9084 4 12V7.21759C4 6.41808 4 6.01833 4.13076 5.6747C4.24627 5.37113 4.43398 5.10027 4.67766 4.88552C4.9535 4.64243 5.3278 4.50207 6.0764 4.22134L11.4382 2.21067C11.6461 2.13271 11.75 2.09373 11.857 2.07827C11.9518 2.06457 12.0482 2.06457 12.143 2.07827C12.25 2.09373 12.3539 2.13271 12.5618 2.21067L17.9236 4.22134C18.6722 4.50207 19.0465 4.64243 19.3223 4.88552C19.566 5.10027 19.7537 5.37113 19.8692 5.6747C20 6.01833 20 6.41808 20 7.21759V12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="shield-tick" viewBox="0 0 24 24" fill="none">
<path d="M9 11.5L11 13.5L15.5 8.99999M20 12C20 16.9084 14.646 20.4784 12.698 21.6149C12.4766 21.744 12.3659 21.8086 12.2097 21.8421C12.0884 21.8681 11.9116 21.8681 11.7903 21.8421C11.6341 21.8086 11.5234 21.744 11.302 21.6149C9.35396 20.4784 4 16.9084 4 12V7.21759C4 6.41808 4 6.01833 4.13076 5.6747C4.24627 5.37113 4.43398 5.10027 4.67766 4.88552C4.9535 4.64243 5.3278 4.50207 6.0764 4.22134L11.4382 2.21067C11.6461 2.13271 11.75 2.09373 11.857 2.07827C11.9518 2.06457 12.0482 2.06457 12.143 2.07827C12.25 2.09373 12.3539 2.13271 12.5618 2.21067L17.9236 4.22134C18.6722 4.50207 19.0465 4.64243 19.3223 4.88552C19.566 5.10027 19.7537 5.37113 19.8692 5.6747C20 6.01833 20 6.41808 20 7.21759V12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="at-sign" viewBox="0 0 21 20" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0453 1.07385C11.0306 0.603671 8.91602 0.82887 7.04549 1.71284C5.17495 2.5968 3.65845 4.08754 2.74257 5.94266C1.82669 7.79778 1.5653 9.90817 2.00091 11.9307C2.43651 13.9532 3.54348 15.7689 5.14183 17.0825C6.74018 18.3961 8.7359 19.1304 10.8045 19.166C12.8731 19.2015 14.8929 18.5363 16.5354 17.2784C16.9008 16.9986 16.9702 16.4755 16.6904 16.1101C16.4105 15.7447 15.8875 15.6754 15.5221 15.9552C14.1782 16.9844 12.5256 17.5287 10.8331 17.4995C9.14066 17.4704 7.5078 16.8697 6.20006 15.7949C4.89232 14.7201 3.98661 13.2346 3.63021 11.5798C3.27381 9.92499 3.48767 8.1983 4.23703 6.68048C4.98638 5.16265 6.22716 3.94296 7.7576 3.21971C9.28804 2.49647 11.0181 2.31221 12.6666 2.69691C14.315 3.08161 15.7848 4.01263 16.837 5.33858C17.8892 6.66453 18.462 8.30748 18.4621 10.0002V10.8335C18.4621 11.2755 18.2865 11.6994 17.9739 12.012C17.6614 12.3245 17.2374 12.5001 16.7954 12.5001C16.3534 12.5001 15.9295 12.3245 15.6169 12.012C15.3043 11.6994 15.1288 11.2755 15.1288 10.8335V6.6668C15.1288 6.20656 14.7557 5.83347 14.2954 5.83347C13.8353 5.83347 13.4622 6.2064 13.4621 6.66651C12.7657 6.14343 11.9001 5.83348 10.9621 5.83348C8.6609 5.83348 6.79542 7.69896 6.79542 10.0001C6.79542 12.3013 8.6609 14.1668 10.9621 14.1668C12.2022 14.1668 13.3157 13.6251 14.079 12.7654C14.186 12.9159 14.3061 13.0582 14.4384 13.1905C15.0635 13.8156 15.9114 14.1668 16.7954 14.1668C17.6795 14.1668 18.5273 13.8156 19.1524 13.1905C19.7776 12.5654 20.1288 11.7175 20.1288 10.8335V10.0001C20.1286 7.93119 19.4286 5.92319 18.1426 4.30257C16.8565 2.68195 15.0601 1.54404 13.0453 1.07385ZM13.4621 9.99665V10.0036C13.4602 11.3827 12.3416 12.5001 10.9621 12.5001C9.58138 12.5001 8.46209 11.3809 8.46209 10.0001C8.46209 8.61943 9.58138 7.50014 10.9621 7.50014C12.3416 7.50014 13.4602 8.61754 13.4621 9.99665Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="trash-01" viewBox="0 0 20 21" fill="none">
<g>
<path d="M13.3333 5.5013V4.83464C13.3333 3.90121 13.3333 3.4345 13.1517 3.07798C12.9919 2.76438 12.7369 2.50941 12.4233 2.34962C12.0668 2.16797 11.6001 2.16797 10.6667 2.16797H9.33333C8.39991 2.16797 7.9332 2.16797 7.57668 2.34962C7.26308 2.50941 7.00811 2.76438 6.84832 3.07798C6.66667 3.4345 6.66667 3.90121 6.66667 4.83464V5.5013M8.33333 10.0846V14.2513M11.6667 10.0846V14.2513M2.5 5.5013H17.5M15.8333 5.5013V14.8346C15.8333 16.2348 15.8333 16.9348 15.5608 17.4696C15.3212 17.94 14.9387 18.3225 14.4683 18.5622C13.9335 18.8346 13.2335 18.8346 11.8333 18.8346H8.16667C6.76654 18.8346 6.06647 18.8346 5.53169 18.5622C5.06129 18.3225 4.67883 17.94 4.43915 17.4696C4.16667 16.9348 4.16667 16.2348 4.16667 14.8346V5.5013" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</symbol>
<symbol id="refresh-ccw-01" viewBox="0 0 20 21" fill="none">
<g>
<path d="M5.28415 5.78889C6.49163 4.58059 8.15782 3.83464 9.99983 3.83464C13.6817 3.83464 16.6665 6.8194 16.6665 10.5013C16.6665 14.1832 13.6817 17.168 9.99983 17.168C6.96172 17.168 4.39626 15.135 3.59364 12.3536C3.46603 11.9114 3.00412 11.6564 2.56193 11.784C2.11973 11.9116 1.86471 12.3735 1.99231 12.8157C2.99526 16.2914 6.19946 18.8346 9.99983 18.8346C14.6022 18.8346 18.3332 15.1037 18.3332 10.5013C18.3332 5.89893 14.6022 2.16797 9.99983 2.16797C7.69784 2.16797 5.61252 3.10246 4.10524 4.61079C3.57518 5.14121 3.00475 5.7994 2.49992 6.41325V3.83464C2.49992 3.3744 2.12682 3.0013 1.66659 3.0013C1.20635 3.0013 0.833252 3.3744 0.833252 3.83464V8.83464C0.833252 9.29487 1.20635 9.66797 1.66659 9.66797H6.66659C7.12682 9.66797 7.49992 9.29487 7.49992 8.83464C7.49992 8.3744 7.12682 8.0013 6.66659 8.0013H3.35886C3.92894 7.28583 4.64729 6.4262 5.28415 5.78889Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="x" viewBox="0 0 24 25" fill="none">
<path d="M17.7071 8.20711C18.0976 7.81658 18.0976 7.18342 17.7071 6.79289C17.3166 6.40237 16.6834 6.40237 16.2929 6.79289L12 11.0858L7.70711 6.79289C7.31658 6.40237 6.68342 6.40237 6.29289 6.79289C5.90237 7.18342 5.90237 7.81658 6.29289 8.20711L10.5858 12.5L6.29289 16.7929C5.90237 17.1834 5.90237 17.8166 6.29289 18.2071C6.68342 18.5976 7.31658 18.5976 7.70711 18.2071L12 13.9142L16.2929 18.2071C16.6834 18.5976 17.3166 18.5976 17.7071 18.2071C18.0976 17.8166 18.0976 17.1834 17.7071 16.7929L13.4142 12.5L17.7071 8.20711Z" fill="currentColor"/>
</symbol>
<symbol id="settings-04" viewBox="0 0 20 20" fill="none">
<path d="M11.772 7.4987L2.50033 7.4987C2.04009 7.4987 1.66699 7.1256 1.66699 6.66536C1.66699 6.20513 2.04009 5.83203 2.50033 5.83203L11.772 5.83203C12.142 4.39434 13.4471 3.33203 15.0003 3.33203C16.8413 3.33203 18.3337 4.82442 18.3337 6.66536C18.3337 8.50631 16.8413 9.9987 15.0003 9.9987C13.4471 9.9987 12.142 8.93639 11.772 7.4987Z" fill="currentColor"/>
<path d="M5.00033 9.9987C3.15938 9.9987 1.66699 11.4911 1.66699 13.332C1.66699 15.173 3.15938 16.6654 5.00033 16.6654C6.55352 16.6654 7.85861 15.6031 8.22864 14.1654L17.5003 14.1654C17.9606 14.1654 18.3337 13.7923 18.3337 13.332C18.3337 12.8718 17.9606 12.4987 17.5003 12.4987L8.22864 12.4987C7.85861 11.061 6.55352 9.9987 5.00033 9.9987Z" fill="currentColor"/>
</symbol>
<symbol id="list" viewBox="0 0 20 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66699 9.9987C6.66699 9.53846 7.04009 9.16536 7.50033 9.16536L17.5003 9.16537C17.9606 9.16537 18.3337 9.53846 18.3337 9.9987C18.3337 10.4589 17.9606 10.832 17.5003 10.832L7.50033 10.832C7.04009 10.832 6.66699 10.4589 6.66699 9.9987Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66699 4.9987C6.66699 4.53846 7.04009 4.16536 7.50033 4.16536L17.5003 4.16537C17.9606 4.16537 18.3337 4.53846 18.3337 4.9987C18.3337 5.45894 17.9606 5.83203 17.5003 5.83203L7.50033 5.83203C7.04009 5.83203 6.66699 5.45894 6.66699 4.9987Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66699 14.9987C6.66699 14.5385 7.04009 14.1654 7.50033 14.1654L17.5003 14.1654C17.9606 14.1654 18.3337 14.5385 18.3337 14.9987C18.3337 15.4589 17.9606 15.832 17.5003 15.832L7.50033 15.832C7.04009 15.832 6.66699 15.4589 6.66699 14.9987Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.66699 9.9987C1.66699 9.07822 2.41318 8.33203 3.33366 8.33203C4.25413 8.33203 5.00033 9.07822 5.00033 9.9987C5.00033 10.9192 4.25413 11.6654 3.33366 11.6654C2.41318 11.6654 1.66699 10.9192 1.66699 9.9987Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.66699 4.9987C1.66699 4.07822 2.41318 3.33203 3.33366 3.33203C4.25413 3.33203 5.00033 4.07822 5.00033 4.9987C5.00033 5.91917 4.25413 6.66536 3.33366 6.66536C2.41318 6.66536 1.66699 5.91917 1.66699 4.9987Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.66699 14.9987C1.66699 14.0782 2.41318 13.332 3.33366 13.332C4.25413 13.332 5.00033 14.0782 5.00033 14.9987C5.00033 15.9192 4.25413 16.6654 3.33366 16.6654C2.41318 16.6654 1.66699 15.9192 1.66699 14.9987Z" fill="currentColor"/>
</symbol>
<symbol id="hard-drive" viewBox="0 0 24 24" fill="none">
<path d="M8.81413 2.99982C7.88643 2.99919 7.18706 2.99872 6.54986 3.21851C5.98936 3.41184 5.47885 3.72735 5.05527 4.14222C4.57372 4.61386 4.26137 5.23961 3.84705 6.06964C3.21363 7.33653 2.55478 8.59437 1.92983 9.86687C1.74859 10.2359 1.65797 10.4204 1.68168 10.5755C1.70218 10.7097 1.77888 10.8328 1.89025 10.9103C2.01902 11 2.22697 11 2.64287 11H21.3572C21.7731 11 21.9811 11 22.1098 10.9103C22.2212 10.8328 22.2979 10.7097 22.3184 10.5755C22.3421 10.4204 22.2515 10.2359 22.0702 9.86686C21.4453 8.59437 20.7865 7.33653 20.153 6.06966C19.7387 5.23961 19.4264 4.61386 18.9448 4.14222C18.5212 3.72735 18.0107 3.41184 17.4502 3.21851C16.813 2.99872 16.1136 2.99919 15.1859 2.99982H8.81413Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.0001 14.5996C23.0001 14.0399 23.0001 13.76 22.8912 13.5461C22.7953 13.3579 22.6423 13.2049 22.4541 13.109C22.2402 13 21.9603 13 21.4004 13H2.59962C2.03978 13 1.75985 13 1.54593 13.109C1.3578 13.2049 1.20476 13.3579 1.10891 13.5461C0.999925 13.76 0.999951 14.0399 1 14.5996C1.00002 14.8135 1.00004 15.0273 1.00004 15.2412C1.00003 16.0462 1.00002 16.7105 1.04423 17.2517C1.09016 17.8138 1.18872 18.3305 1.43601 18.8159C1.81951 19.5685 2.43143 20.1804 3.18408 20.5639C3.66941 20.8112 4.18612 20.9098 4.74821 20.9557C5.2894 20.9999 5.95376 20.9999 6.75872 20.9999H17.2414C18.0463 20.9999 18.7107 20.9999 19.2519 20.9557C19.814 20.9098 20.3307 20.8112 20.816 20.5639C21.5687 20.1804 22.1806 19.5685 22.5641 18.8159C22.8114 18.3305 22.9099 17.8138 22.9558 17.2517C23.0001 16.7105 23.0001 16.0462 23 15.2412C23 15.0274 23.0001 14.8135 23.0001 14.5996ZM6.00002 15C5.44773 15 5.00002 15.4477 5.00002 16C5.00002 16.5523 5.44773 17 6.00002 17H10C10.5523 17 11 16.5523 11 16C11 15.4477 10.5523 15 10 15H6.00002Z" fill="currentColor"/>
</symbol>
<symbol id="wifi-off" viewBox="0 0 24 20" fill="none">
<path d="M3.70711 0.292893C3.31658 -0.0976311 2.68342 -0.0976311 2.29289 0.292893C1.90237 0.683418 1.90237 1.31658 2.29289 1.70711L4.05356 3.46777C2.76797 4.14869 1.58058 4.98947 0.517762 5.96339C0.110582 6.33652 0.0829746 6.96908 0.4561 7.37626C0.829225 7.78344 1.46179 7.81105 1.86897 7.43792C2.95836 6.43965 4.19519 5.60049 5.5426 4.95681L7.83025 7.24447C6.4361 7.76113 5.16466 8.52904 4.07093 9.49251C3.65651 9.85758 3.6165 10.4895 3.98157 10.9039C4.34664 11.3183 4.97854 11.3583 5.39296 10.9933C6.53546 9.98682 7.90822 9.23792 9.42113 8.83535L12.0863 11.5005C12.0576 11.5002 12.0288 11.5 11.9999 11.5C10.3484 11.5 8.82808 12.0732 7.63085 13.0306C7.19953 13.3755 7.1295 14.0048 7.47443 14.4361C7.81937 14.8675 8.44865 14.9375 8.87997 14.5926C8.93165 14.5512 8.98416 14.5109 9.03748 14.4716C9.06175 14.4573 9.08563 14.4418 9.10903 14.4252C9.95503 13.8241 10.9671 13.5012 12.0049 13.5012C12.8194 13.5012 13.618 13.7001 14.333 14.0763C14.5973 14.2161 14.8477 14.3789 15.0814 14.5621C15.138 14.6064 15.1978 14.6437 15.2598 14.674L20.2929 19.7071C20.6834 20.0976 21.3166 20.0976 21.7071 19.7071C22.0976 19.3166 22.0976 18.6834 21.7071 18.2929L3.70711 0.292893Z" fill="currentColor"/>
<path d="M10.3098 3.59816C10.8684 3.53482 11.4326 3.50269 11.9998 3.50269C15.652 3.50269 19.1788 4.83517 21.9186 7.2502C22.3329 7.6154 22.9648 7.57559 23.33 7.16129C23.6952 6.74698 23.6554 6.11507 23.2411 5.74987C20.136 3.01283 16.139 1.50269 11.9998 1.50269C11.357 1.50269 10.7176 1.53911 10.0845 1.61089C9.53569 1.67311 9.14126 2.16841 9.20348 2.71718C9.2657 3.26595 9.761 3.66038 10.3098 3.59816Z" fill="currentColor"/>
<path d="M15.6095 7.04529C15.0822 6.88101 14.5216 7.17528 14.3573 7.70256C14.193 8.22985 14.4873 8.79048 15.0146 8.95476C16.2585 9.34233 17.4241 9.97212 18.4401 10.8184C18.8645 11.1719 19.495 11.1144 19.8485 10.69C20.2019 10.2657 20.1445 9.6351 19.7201 9.28164C18.5009 8.26611 17.1022 7.51034 15.6095 7.04529Z" fill="currentColor"/>
<path d="M12 16.5C11.4477 16.5 11 16.9477 11 17.5C11 18.0523 11.4477 18.5 12 18.5H12.01C12.5623 18.5 13.01 18.0523 13.01 17.5C13.01 16.9477 12.5623 16.5 12.01 16.5H12Z" fill="currentColor"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -24,7 +24,7 @@ export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
return [...this.cache.values()];
}
override async onEvent(evs: Array<TaggedNostrEvent>, pub?: EventPublisher) {
override async onEvent(evs: Array<TaggedNostrEvent>, _: string, pub?: EventPublisher) {
if (!pub) return;
const unwrapped = (

View File

@ -1,11 +1,11 @@
import { EventKind, NostrEvent, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { RefreshFeedCache, TWithCreated } from "./RefreshFeedCache";
import { LoginSession } from "Login";
import { db } from "Db";
import { NostrEventForSession, db } from "Db";
import { Day } from "Const";
import { unixNow } from "@snort/shared";
export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
export class NotificationsCache extends RefreshFeedCache<NostrEventForSession> {
#kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
constructor() {
@ -14,7 +14,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
buildSub(session: LoginSession, rb: RequestBuilder) {
if (session.publicKey) {
const newest = this.newest();
const newest = this.newest(v => v.tags.some(a => a[0] === "p" && a[1] === session.publicKey));
rb.withFilter()
.kinds(this.#kinds)
.tag("p", [session.publicKey])
@ -22,10 +22,15 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
}
}
async onEvent(evs: readonly TaggedNostrEvent[]) {
async onEvent(evs: readonly TaggedNostrEvent[], pubKey: string) {
const filtered = evs.filter(a => this.#kinds.includes(a.kind) && a.tags.some(b => b[0] === "p"));
if (filtered.length > 0) {
await this.bulkSet(filtered);
await this.bulkSet(
filtered.map(v => ({
...v,
forSession: pubKey,
})),
);
this.notifyChange(filtered.map(v => this.key(v)));
}
}
@ -34,7 +39,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
return of.id;
}
takeSnapshot(): TWithCreated<NostrEvent>[] {
takeSnapshot() {
return [...this.cache.values()];
}
}

View File

@ -6,14 +6,18 @@ export type TWithCreated<T> = (T | Readonly<T>) & { created_at: number };
export abstract class RefreshFeedCache<T> extends FeedCache<TWithCreated<T>> {
abstract buildSub(session: LoginSession, rb: RequestBuilder): void;
abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pub?: EventPublisher): void;
abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pubKey: string, pub?: EventPublisher): void;
/**
* Get latest event
*/
protected newest() {
protected newest(filter?: (e: TWithCreated<T>) => boolean) {
let ret = 0;
this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret));
this.cache.forEach(v => {
if (!filter || filter(v)) {
ret = v.created_at > ret ? v.created_at : ret;
}
});
return ret;
}

View File

@ -1,4 +1,6 @@
import { UserProfileCache, UserRelaysCache, RelayMetricCache } from "@snort/system";
import { SnortSystemDb } from "@snort/system-web";
import { EventInteractionCache } from "./EventInteractionCache";
import { ChatCache } from "./ChatCache";
import { Payments } from "./PaymentsCache";
@ -6,9 +8,11 @@ import { GiftWrapCache } from "./GiftWrapCache";
import { NotificationsCache } from "./Notifications";
import { FollowsFeedCache } from "./FollowsFeed";
export const UserCache = new UserProfileCache();
export const UserRelays = new UserRelaysCache();
export const RelayMetrics = new RelayMetricCache();
export const SystemDb = new SnortSystemDb();
export const UserCache = new UserProfileCache(SystemDb.users);
export const UserRelays = new UserRelaysCache(SystemDb.userRelays);
export const RelayMetrics = new RelayMetricCache(SystemDb.relayMetrics);
export const Chats = new ChatCache();
export const PaymentsCache = new Payments();
export const InteractionCache = new EventInteractionCache();

View File

@ -1,5 +1,3 @@
import { RelaySettings } from "@snort/system";
/**
* 1 Hour in seconds
*/
@ -15,11 +13,6 @@ export const Day = Hour * 24;
*/
export const ApiHost = "https://api.snort.social";
/**
* LibreTranslate endpoint
*/
export const TranslateHost = "https://translate.snort.social";
/**
* Void.cat file upload service url
*/
@ -35,53 +28,21 @@ export const KieranPubKey = "npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7v
*/
export const SnortPubKey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
/**
* Websocket re-connect timeout
*/
export const DefaultConnectTimeout = 2000;
/**
* How long profile cache should be considered valid for
*/
export const ProfileCacheExpire = 1_000 * 60 * 60 * 6;
/**
* Default bootstrap relays
*/
export const DefaultRelays = new Map<string, RelaySettings>([
["wss://relay.snort.social/", { read: true, write: true }],
["wss://nostr.wine/", { read: true, write: false }],
["wss://nos.lol/", { read: true, write: true }],
]);
export const DefaultRelays = new Map(Object.entries(CONFIG.defaultRelays));
/**
* Default search relays
*/
export const SearchRelays = ["wss://relay.nostr.band"];
/**
* List of recommended follows for new users
*/
export const RecommendedFollows = [
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", // jack
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf
"020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", // adam3us
"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", // gigi
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", // Kieran
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", // jb55
"e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", // wiz
"00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", // cameri
"A341F45FF9758F570A21B000C17D4E53A3A497C8397F26C0E6D61E5ACFFC7A98", // Saylor
"E88A691E98D9987C964521DFF60025F60700378A4879180DCBBB4A5027850411", // NVK
"C4EABAE1BE3CF657BC1855EE05E69DE9F059CB7A059227168B80B89761CBC4E0", // jackmallers
"85080D3BAD70CCDCD7F74C29A44F55BB85CBCD3DD0CBB957DA1D215BDB931204", // preston
"C49D52A573366792B9A6E4851587C28042FB24FA5625C6D67B8C95C8751ACA15", // holdonaut
"83E818DFBECCEA56B0F551576B3FD39A7A50E1D8159343500368FA085CCD964B", // jeffbooth
"3F770D65D3A764A9C5CB503AE123E62EC7598AD035D836E2A810F3877A745B24", // DerekRoss
"472F440F29EF996E92A186B8D320FF180C855903882E59D50DE1B8BD5669301E", // MartyBent
"1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", // yegorpetrov
"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", // ODELL
export const DeveloperAccounts = [
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", // kieran
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0", // Martti
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", // verbiricha
"1bc70a0148b3f316da33fe3c89f23e3e71ac4ff998027ec712b905cd24f6a411", // Karnage
];
/**

View File

@ -2,7 +2,7 @@ import Dexie, { Table } from "dexie";
import { HexKey, NostrEvent, TaggedNostrEvent, u256 } from "@snort/system";
export const NAME = "snortDB";
export const VERSION = 14;
export const VERSION = 15;
export interface SubCache {
id: string;
@ -35,6 +35,10 @@ export interface UnwrappedGift {
tags?: Array<Array<string>>; // some tags extracted
}
export type NostrEventForSession = TaggedNostrEvent & {
forSession: string;
};
const STORES = {
chats: "++id",
eventInteraction: "++id",
@ -50,7 +54,7 @@ export class SnortDB extends Dexie {
eventInteraction!: Table<EventInteraction>;
payments!: Table<Payment>;
gifts!: Table<UnwrappedGift>;
notifications!: Table<NostrEvent>;
notifications!: Table<NostrEventForSession>;
followsFeed!: Table<TaggedNostrEvent>;
constructor() {

View File

@ -1,14 +1,24 @@
button {
position: relative;
}
.spinner-wrapper {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.spinner-button > span {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.light .spinner-button {
border: 1px solid var(--border-color);
color: var(--font-secondary);
box-shadow: rgba(0, 0, 0, 0.08) 0 1px 1px;
}
.light .spinner-button:hover {
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 3px;
}

View File

@ -1,39 +1,23 @@
import "./AsyncButton.css";
import React, { useState, ForwardedRef } from "react";
import React, { ForwardedRef } from "react";
import Spinner from "../Icons/Spinner";
import useLoading from "Hooks/useLoading";
import classNames from "classnames";
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
disabled?: boolean;
onClick(e: React.MouseEvent): Promise<void> | void;
children?: React.ReactNode;
export interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick?: (e: React.MouseEvent) => Promise<void> | void;
}
const AsyncButton = React.forwardRef<HTMLButtonElement, AsyncButtonProps>((props, ref) => {
const [loading, setLoading] = useState<boolean>(false);
async function handle(e: React.MouseEvent) {
e.stopPropagation();
if (loading || props.disabled) return;
setLoading(true);
try {
if (typeof props.onClick === "function") {
const f = props.onClick(e);
if (f instanceof Promise) {
await f;
}
}
} finally {
setLoading(false);
}
}
const { handle, loading } = useLoading(props.onClick, props.disabled);
return (
<button
ref={ref as ForwardedRef<HTMLButtonElement>}
className="spinner-button"
type="button"
disabled={loading || props.disabled}
{...props}
className={classNames("spinner-button", props.className)}
onClick={handle}>
<span style={{ visibility: loading ? "hidden" : "visible" }}>{props.children}</span>
{loading && (

View File

@ -1,35 +1,22 @@
import Icon from "Icons/Icon";
import useLoading from "Hooks/useLoading";
import Spinner from "Icons/Spinner";
import { HTMLProps, useState } from "react";
export interface AsyncIconProps extends HTMLProps<HTMLDivElement> {
export type AsyncIconProps = React.HTMLProps<HTMLDivElement> & {
iconName: string;
iconSize?: number;
loading?: boolean;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => Promise<void>;
}
onClick?: (e: React.MouseEvent) => Promise<void> | void;
};
export function AsyncIcon(props: AsyncIconProps) {
const [loading, setLoading] = useState(props.loading ?? false);
async function handleClick(e: React.MouseEvent<HTMLDivElement>) {
setLoading(true);
try {
if (props.onClick) {
await props.onClick(e);
}
} catch (ex) {
console.error(ex);
}
setLoading(false);
}
const { loading, handle } = useLoading(props.onClick, props.disabled);
const mergedProps = { ...props } as Record<string, unknown>;
delete mergedProps["iconName"];
delete mergedProps["iconSize"];
delete mergedProps["loading"];
return (
<div {...mergedProps} onClick={e => handleClick(e)}>
<div {...mergedProps} onClick={handle} className={props.className}>
{loading ? <Spinner /> : <Icon name={props.iconName} size={props.iconSize} />}
{props.children}
</div>

View File

@ -1,3 +1,5 @@
import { MetadataCache } from "@snort/system";
import { ChatParticipant } from "chat";
import NoteToSelf from "../User/NoteToSelf";
import ProfileImage from "../User/ProfileImage";
@ -6,7 +8,7 @@ import useLogin from "Hooks/useLogin";
export function ChatParticipantProfile({ participant }: { participant: ChatParticipant }) {
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
if (participant.id === publicKey) {
return <NoteToSelf className="f-grow" pubkey={participant.id} />;
return <NoteToSelf className="grow" />;
}
return <ProfileImage pubkey={participant.id} className="f-grow" profile={participant.profile} />;
return <ProfileImage pubkey={participant.id} className="grow" profile={participant.profile as MetadataCache} />;
}

View File

@ -19,7 +19,7 @@ export interface DMProps {
export default function DM(props: DMProps) {
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const publisher = useEventPublisher();
const { publisher } = useEventPublisher();
const msg = props.data;
const [content, setContent] = useState<string>();
const { ref, inView } = useInView({ triggerOnce: true });

View File

@ -6,7 +6,7 @@ import DM from "Element/Chat/DM";
import useLogin from "Hooks/useLogin";
import WriteMessage from "Element/Chat/WriteMessage";
import { Chat, createEmptyChatObject, useChatSystem } from "chat";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { ChatParticipantProfile } from "./ChatParticipant";
export default function DmWindow({ id }: { id: string }) {
@ -32,9 +32,9 @@ export default function DmWindow({ id }: { id: string }) {
<div className="dm-window">
<div>{sender()}</div>
<div>
<div className="flex f-col">{chat && <DmChatSelected chat={chat} />}</div>
<div className="flex flex-col">{chat && <DmChatSelected chat={chat} />}</div>
</div>
<div>
<div className="flex g8">
<WriteMessage chat={chat} />
</div>
</div>

View File

@ -1,21 +1,17 @@
import { NostrPrefix, NostrEvent, NostrLink } from "@snort/system";
import useEventPublisher from "Hooks/useEventPublisher";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";
import { useState } from "react";
import { NostrEvent, NostrLink, NostrPrefix } from "@snort/system";
import useEventPublisher from "Hooks/useEventPublisher";
import useFileUpload from "Upload";
import { openFile } from "SnortUtils";
import Textarea from "../Textarea";
import { System } from "index";
import { Chat } from "chat";
import { AsyncIcon } from "Element/AsyncIcon";
export default function WriteMessage({ chat }: { chat: Chat }) {
const [msg, setMsg] = useState("");
const [sending, setSending] = useState(false);
const [uploading, setUploading] = useState(false);
const [otherEvents, setOtherEvents] = useState<Array<NostrEvent>>([]);
const [error, setError] = useState("");
const publisher = useEventPublisher();
const { publisher, system } = useEventPublisher();
const uploader = useFileUpload();
async function attachFile() {
@ -32,12 +28,13 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
}
async function uploadFile(file: File | Blob) {
setUploading(true);
try {
if (file) {
const rx = await uploader.upload(file, file.name);
if (rx.header) {
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`;
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode(
CONFIG.eventLinkPrefix,
)}`;
setMsg(`${msg ? `${msg}\n` : ""}${link}`);
setOtherEvents([...otherEvents, rx.header]);
} else if (rx.url) {
@ -50,25 +47,19 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
if (e instanceof Error) {
setError(e.message);
}
} finally {
setUploading(false);
}
}
async function sendMessage() {
if (msg && publisher && chat) {
setSending(true);
const ev = await chat.createMessage(msg, publisher);
await chat.sendMessage(ev, System);
await chat.sendMessage(ev, system);
setMsg("");
setSending(false);
}
}
function onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
if (!sending) {
setMsg(e.target.value);
}
setMsg(e.target.value);
}
async function onEnter(e: React.KeyboardEvent<HTMLTextAreaElement>) {
@ -81,10 +72,8 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
return (
<>
<button className="btn-rnd" onClick={() => attachFile()}>
{uploading ? <Spinner width={20} /> : <Icon name="attachment" size={20} />}
</button>
<div className="w-max">
<AsyncIcon className="circle flex items-center button" iconName="attachment" onClick={() => attachFile()} />
<div className="grow">
<Textarea
autoFocus={true}
placeholder=""
@ -98,9 +87,7 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
/>
{error && <b className="error">{error}</b>}
</div>
<button className="btn-rnd" onClick={() => sendMessage()}>
{sending ? <Spinner width={20} /> : <Icon name="arrow-right" size={20} />}
</button>
<AsyncIcon className="circle flex items-center button" iconName="arrow-right" onClick={() => sendMessage()} />
</>
);
}

View File

@ -1,4 +1,5 @@
import { useState, ReactNode } from "react";
import classNames from "classnames";
import Icon from "Icons/Icon";
import ShowMore from "Element/Event/ShowMore";
@ -38,15 +39,13 @@ interface CollapsedSectionProps {
export const CollapsedSection = ({ title, children, className }: CollapsedSectionProps) => {
const [collapsed, setCollapsed] = useState(true);
const icon = (
<div className={`collapse-icon ${collapsed ? "" : "flip"}`}>
<div className={classNames("collapse-icon", { flip: !collapsed })}>
<Icon name="arrowFront" />
</div>
);
return (
<>
<div
className={`collapsable-section${className ? ` ${className}` : ""}`}
onClick={() => setCollapsed(!collapsed)}>
<div className={classNames("collapsable-section", className)} onClick={() => setCollapsed(!collapsed)}>
{title}
<CollapsedIcon icon={icon} collapsed={collapsed} />
</div>

View File

@ -1,5 +1,4 @@
.copy .copy-body {
font-size: var(--font-size-small);
color: var(--font-color);
margin-right: 6px;
}

View File

@ -1,4 +1,5 @@
import "./Copy.css";
import classNames from "classnames";
import Icon from "Icons/Icon";
import { useCopy } from "useCopy";
@ -13,7 +14,7 @@ export default function Copy({ text, maxSize = 32, className }: CopyProps) {
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
return (
<div className={`copy flex pointer g8${className ? ` ${className}` : ""}`} onClick={() => copy(text)}>
<div className={classNames("copy flex pointer g8 items-center", className)} onClick={() => copy(text)}>
<span className="copy-body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}

View File

@ -3,15 +3,28 @@ import { useArticles } from "Feed/ArticlesFeed";
import { orderDescending } from "SnortUtils";
import Note from "../Event/Note";
import { useReactions } from "Feed/Reactions";
import { useContext } from "react";
import { DeckContext } from "Pages/DeckLayout";
export default function Articles() {
const data = useArticles();
const deck = useContext(DeckContext);
const related = useReactions("articles:reactions", data.data?.map(v => NostrLink.fromEvent(v)) ?? []);
return (
<>
{orderDescending(data.data ?? []).map(a => (
<Note data={a} key={a.id} related={related.data ?? []} />
<Note
data={a}
key={a.id}
related={related.data ?? []}
options={{
longFormPreview: true,
}}
onClick={ev => {
deck?.setArticle(ev);
}}
/>
))}
</>
);

View File

@ -4,37 +4,34 @@ import useLogin from "Hooks/useLogin";
import "./Nav.css";
import Icon from "Icons/Icon";
import { Link } from "react-router-dom";
import { profileLink } from "SnortUtils";
import { NoteCreatorButton } from "Element/Event/NoteCreatorButton";
import { ProfileLink } from "Element/User/ProfileLink";
export function DeckNav() {
const { publicKey } = useLogin();
const profile = useUserProfile(publicKey);
const unreadDms = 0;
const hasNotifications = false;
return (
<nav className="deck flex-column f-space">
<div className="flex-column f-center g24">
<nav className="deck flex flex-col justify-between">
<div className="flex flex-col items-center g24">
<Link className="btn" to="/messages">
<Icon name="mail" size={24} />
{unreadDms > 0 && <span className="has-unread"></span>}
</Link>
<Link className="btn" to="/notifications">
<Icon name="bell-02" size={24} />
{hasNotifications && <span className="has-unread"></span>}
</Link>
<NoteCreatorButton />
</div>
<div className="flex-column f-center g16">
<Link className="btn" to="/">
<div className="flex flex-col items-center g16">
{/*<Link className="btn" to="/">
<Icon name="grid-01" size={24} />
</Link>
</Link>*/}
<Link className="btn" to="/settings">
<Icon name="settings-02" size={24} />
</Link>
<Link to={profileLink(publicKey ?? "")}>
<ProfileLink pubkey={publicKey ?? ""} user={profile}>
<Avatar pubkey={publicKey ?? ""} user={profile} />
</Link>
</ProfileLink>
</div>
</nav>
);

View File

@ -52,9 +52,9 @@ export default function CashuNuts({ token }: { token: string }) {
const amount = cashu.token[0].proofs.reduce((acc, v) => acc + v.amount, 0);
return (
<div className="cashu flex f-space p24 br">
<div className="flex-column g8 f-ellipsis">
<div className="flex f-center g16">
<div className="cashu flex justify-between p24 br">
<div className="flex flex-col g8 f-ellipsis">
<div className="flex items-center g16">
<svg width="30" height="39" viewBox="0 0 30 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 47711">
<path

View File

@ -1,7 +1,3 @@
.hashtag {
color: var(--highlight);
}
.hashtag::after {
content: " ";
}

View File

@ -3,6 +3,7 @@ import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useMemo } from "react";
import { decodeInvoice } from "@snort/shared";
import classNames from "classnames";
import SendSats from "Element/SendSats";
import Icon from "Icons/Icon";
@ -60,7 +61,7 @@ export default function Invoice(props: InvoiceProps) {
return (
<>
<div className={`note-invoice flex ${isExpired ? "expired" : ""} ${isPaid ? "paid" : ""}`}>
<div className={classNames("note-invoice flex", { expired: isExpired, paid: isPaid })}>
<div className="invoice-header">{header()}</div>
<p className="invoice-amount">

View File

@ -21,6 +21,8 @@
padding: 0;
font-size: 16px;
font-weight: 700;
line-height: initial;
margin: 0.5em 0;
}
.link-preview-container:hover .link-preview-title > h1 {

View File

@ -2,7 +2,7 @@ import "./LinkPreview.css";
import { CSSProperties, useEffect, useState } from "react";
import Spinner from "Icons/Spinner";
import SnortApi, { LinkPreviewData } from "SnortApi";
import SnortApi, { LinkPreviewData } from "External/SnortApi";
import useImgProxy from "Hooks/useImgProxy";
import { MediaElement } from "Element/Embed/MediaElement";
@ -81,7 +81,7 @@ const LinkPreview = ({ url }: { url: string }) => {
</div>
</a>
)}
{!preview && <Spinner className="f-center" />}
{!preview && <Spinner className="items-center" />}
</div>
);
};

View File

@ -1,4 +1,4 @@
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { Magnet } from "SnortUtils";

View File

@ -1,14 +1,7 @@
import { ProxyImg } from "Element/ProxyImg";
import useImgProxy from "Hooks/useImgProxy";
import React from "react";
/*
[
"imeta",
"url https://nostr.build/i/148e3e8cbe29ae268b0d6aad0065a086319d3c3b1fdf8b89f1e2327d973d2d05.jpg",
"blurhash e6A0%UE2t6D*R%?u?a9G?aM|~pM|%LR*RjR-%2NG%2t7_2R*%1IVWB",
"dim 3024x4032"
],
*/
interface MediaElementProps {
mime: string;
url: string;
@ -19,12 +12,14 @@ interface MediaElementProps {
}
export function MediaElement(props: MediaElementProps) {
const { proxy } = useImgProxy();
if (props.mime.startsWith("image/")) {
return <ProxyImg key={props.url} src={props.url} onClick={props.onMediaClick} />;
} else if (props.mime.startsWith("audio/")) {
return <audio key={props.url} src={props.url} controls />;
} else if (props.mime.startsWith("video/")) {
return <video key={props.url} src={props.url} controls />;
return <video key={props.url} src={props.url} controls poster={proxy(props.url)} />;
} else {
return (
<a

View File

@ -1,16 +1,25 @@
import { Link } from "react-router-dom";
import { HexKey } from "@snort/system";
import { NostrLink, NostrPrefix } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { profileLink } from "SnortUtils";
import DisplayName from "../User/DisplayName";
import { useHover } from "@uidotdev/usehooks";
export default function Mention({ pubkey, relays }: { pubkey: HexKey; relays?: Array<string> | string }) {
const user = useUserProfile(pubkey);
import DisplayName from "Element/User/DisplayName";
import { ProfileCard } from "Element/User/ProfileCard";
import { ProfileLink } from "Element/User/ProfileLink";
export default function Mention({ link }: { link: NostrLink }) {
const [ref, hovering] = useHover<HTMLAnchorElement>();
const profile = useUserProfile(link.id);
if (link.type !== NostrPrefix.Profile && link.type !== NostrPrefix.PublicKey) return;
return (
<Link to={profileLink(pubkey, relays)} onClick={e => e.stopPropagation()}>
@<DisplayName user={user} pubkey={pubkey} />
</Link>
<>
<ProfileLink pubkey={link.id} user={profile} onClick={e => e.stopPropagation()}>
<span ref={ref}>
@<DisplayName user={profile} pubkey={link.id} />
</span>
</ProfileLink>
<ProfileCard pubkey={link.id} user={profile} show={hovering} ref={ref} />
</>
);
}

View File

@ -8,12 +8,12 @@ export default function NostrLink({ link, depth }: { link: string; depth?: numbe
const nav = tryParseNostrLink(link);
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
return <Mention pubkey={nav.id} relays={nav.relays} />;
return <Mention link={nav} />;
} else if (nav?.type === NostrPrefix.Note || nav?.type === NostrPrefix.Event || nav?.type === NostrPrefix.Address) {
if ((depth ?? 0) > 0) {
const evLink = nav.encode();
return (
<Link to={`/e/${evLink}`} onClick={e => e.stopPropagation()} state={{ from: location.pathname }}>
<Link to={`/${evLink}`} onClick={e => e.stopPropagation()} state={{ from: location.pathname }}>
#{evLink.substring(0, 12)}
</Link>
);

View File

@ -2,12 +2,11 @@ import { NostrEvent } from "@snort/system";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { LNURL } from "@snort/shared";
import { dedupe, hexToBech32 } from "SnortUtils";
import { dedupe, findTag, hexToBech32, getDisplayName } from "SnortUtils";
import FollowListBase from "Element/User/FollowListBase";
import AsyncButton from "Element/AsyncButton";
import { useWallet } from "Wallet";
import { Toastore } from "Toaster";
import { getDisplayName } from "Element/User/DisplayName";
import { UserCache } from "Cache";
import useLogin from "Hooks/useLogin";
import useEventPublisher from "Hooks/useEventPublisher";
@ -16,7 +15,7 @@ import { WalletInvoiceState } from "Wallet";
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
const wallet = useWallet();
const login = useLogin();
const publisher = useEventPublisher();
const { publisher } = useEventPublisher();
const ids = dedupe(ev.tags.filter(a => a[0] === "p").map(a => a[1]));
async function zapAll() {
@ -66,12 +65,12 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
pubkeys={ids}
showAbout={true}
className={className}
title={ev.tags.find(a => a[0] === "d")?.[1]}
title={findTag(ev, "title") ?? findTag(ev, "d")}
actions={
<>
<AsyncButton className="mr5 transparent" onClick={() => zapAll()}>
<AsyncButton className="mr5 secondary" onClick={() => zapAll()}>
<FormattedMessage
defaultMessage="Zap All {n} sats"
defaultMessage="Zap all {n} sats"
values={{
n: <FormattedNumber value={login.preferences.defaultZapAmount * ids.length} />,
}}

View File

@ -4,7 +4,7 @@ import { NostrEvent, NostrLink } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import ProfileImage from "Element/User/ProfileImage";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) {
const media = ev.tags.find(a => a[0] === "media");
@ -12,12 +12,12 @@ export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) {
const subject = ev.tags.find(a => a[0] === "subject");
const refPersons = ev.tags.filter(a => a[0] === "p");
const link = NostrLink.fromEvent(ev).encode();
const link = NostrLink.fromEvent(ev).encode(CONFIG.eventLinkPrefix);
return (
<>
<div className="flex zapstr mb10 card">
<ProxyImg src={cover?.[1] ?? ""} size={100} />
<div className="flex f-col">
<div className="flex flex-col">
<div>
<h3>{subject?.[1] ?? ""}</h3>
</div>

View File

@ -0,0 +1,19 @@
import { OfflineError } from "@snort/shared";
import { Offline } from "./Offline";
import classNames from "classnames";
export function ErrorOrOffline({
error,
onRetry,
className,
}: {
error: Error;
onRetry?: () => void | Promise<void>;
className?: string;
}) {
if (error instanceof OfflineError) {
return <Offline onRetry={onRetry} className={className} />;
} else {
return <b className={classNames("error", className)}>{error.message}</b>;
}
}

View File

@ -0,0 +1,15 @@
import Progress from "Element/Progress";
import { UploadProgress } from "Upload";
export default function FileUploadProgress({ progress }: { progress: Array<UploadProgress> }) {
return (
<div className="flex flex-col g8">
{progress.map(p => (
<div className="flex flex-col g2" id={p.id}>
{p.file.name}
<Progress value={p.progress} status={p.stage} />
</div>
))}
</div>
);
}

View File

@ -0,0 +1,67 @@
.long-form-note p {
font-family: Georgia;
line-height: 1.7;
}
.long-form-note hr {
border: 0;
height: 1px;
background-color: var(--gray);
margin: 5px 0px;
}
.long-form-note .reading {
border: 1px dashed var(--highlight);
}
.long-form-note .header-image {
height: 360px;
background: var(--img);
background-position: center;
background-size: cover;
}
.long-form-note h1 {
font-size: 32px;
font-weight: 700;
line-height: 40px; /* 125% */
margin: 0;
}
.long-form-note small {
font-weight: 400;
line-height: 24px; /* 150% */
}
.long-form-note img:not(.custom-emoji),
.long-form-note video,
.long-form-note iframe,
.long-form-note audio {
width: 100%;
display: block;
}
.long-form-note iframe,
.long-form-note video {
width: -webkit-fill-available;
aspect-ratio: 16 / 9;
}
.long-form-note .footer {
display: flex;
}
.long-form-note .footer .footer-reactions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-left: auto;
gap: 48px;
}
@media (min-width: 720px) {
.long-form-note .footer .footer-reactions {
margin-left: 0;
}
}

View File

@ -0,0 +1,146 @@
import "./LongFormText.css";
import { CSSProperties, useCallback, useRef, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { TaggedNostrEvent } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { findTag } from "SnortUtils";
import Text from "Element/Text";
import { Markdown } from "./Markdown";
import useImgProxy from "Hooks/useImgProxy";
import ProfilePreview from "Element/User/ProfilePreview";
import NoteFooter from "./NoteFooter";
import NoteTime from "./NoteTime";
interface LongFormTextProps {
ev: TaggedNostrEvent;
isPreview: boolean;
related: ReadonlyArray<TaggedNostrEvent>;
onClick?: () => void;
}
export function LongFormText(props: LongFormTextProps) {
const title = findTag(props.ev, "title");
const summary = findTag(props.ev, "summary");
const image = findTag(props.ev, "image");
const { proxy } = useImgProxy();
const [reading, setReading] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const { reactions, reposts, zaps } = useEventReactions(props.ev, props.related);
function previewText() {
return (
<Text
id={props.ev.id}
content={props.ev.content}
tags={props.ev.tags}
creator={props.ev.pubkey}
truncate={props.isPreview ? 250 : undefined}
disableLinkPreview={props.isPreview}
/>
);
}
function readTime() {
const wpm = 225;
const words = props.ev.content.trim().split(/\s+/).length;
return {
words,
wpm,
mins: Math.ceil(words / wpm),
};
}
const readAsync = async (text: string) => {
return await new Promise<void>(resolve => {
const ut = new SpeechSynthesisUtterance(text);
ut.onend = () => {
resolve();
};
window.speechSynthesis.speak(ut);
});
};
const readArticle = useCallback(async () => {
if (ref.current && !reading) {
setReading(true);
const paragraphs = ref.current.querySelectorAll("p,h1,h2,h3,h4,h5,h6");
for (const p of paragraphs) {
if (p.textContent) {
p.classList.add("reading");
await readAsync(p.textContent);
p.classList.remove("reading");
}
}
setReading(false);
}
}, [ref, reading]);
const stopReading = () => {
setReading(false);
if (ref.current) {
const paragraphs = ref.current.querySelectorAll("p,h1,h2,h3,h4,h5,h6");
paragraphs.forEach(a => a.classList.remove("reading"));
window.speechSynthesis.cancel();
}
};
function fullText() {
return (
<>
<NoteFooter ev={props.ev} reposts={reposts} zaps={zaps} positive={reactions.positive} />
<hr />
<div className="flex g8">
<div>
<FormattedMessage
defaultMessage="{n} mins to read"
values={{
n: <FormattedNumber value={readTime().mins} />,
}}
/>
</div>
<div></div>
{!reading && (
<div className="pointer" onClick={() => readArticle()}>
<FormattedMessage defaultMessage="Listen to this article" />
</div>
)}
{reading && (
<div className="pointer" onClick={() => stopReading()}>
<FormattedMessage defaultMessage="Stop listening" />
</div>
)}
</div>
<hr />
<Markdown content={props.ev.content} tags={props.ev.tags} ref={ref} />
<hr />
<NoteFooter ev={props.ev} reposts={reposts} zaps={zaps} positive={reactions.positive} />
</>
);
}
return (
<div
className="long-form-note flex flex-col g16 p pointer"
onClick={e => {
e.stopPropagation();
props.onClick?.();
}}>
<ProfilePreview
pubkey={props.ev.pubkey}
actions={
<>
<NoteTime from={props.ev.created_at * 1000} />
</>
}
options={{
about: false,
}}
/>
<h1>{title}</h1>
<small>{summary}</small>
{image && <div className="header-image" style={{ "--img": `url(${proxy(image)})` } as CSSProperties} />}
{props.isPreview ? previewText() : fullText()}
</div>
);
}

View File

@ -0,0 +1,44 @@
.markdown a {
color: var(--highlight);
}
.markdown blockquote {
margin: 0;
color: var(--font-secondary-color);
border-left: 2px solid var(--font-secondary-color);
padding-left: 12px;
}
.markdown hr {
border: 0;
height: 1px;
background-image: var(--gray-gradient);
margin: 20px;
}
.markdown img:not(.custom-emoji),
.markdown video,
.markdown iframe,
.markdown audio {
width: 100%;
display: block;
}
.markdown iframe,
.markdown video {
width: -webkit-fill-available;
aspect-ratio: 16 / 9;
}
.markdown ul,
.markdown ol {
padding-inline-start: 20px;
}
.markdown ul {
list-style: circle;
}
.markdown ol {
list-style: decimal;
}

View File

@ -0,0 +1,132 @@
import "./Markdown.css";
import { ReactNode, forwardRef, useMemo } from "react";
import { transformText } from "@snort/system";
import { marked, Token } from "marked";
import { Link } from "react-router-dom";
import markedFootnote, { Footnotes, Footnote, FootnoteRef } from "marked-footnote";
import { ProxyImg } from "Element/ProxyImg";
import NostrLink from "Element/Embed/NostrLink";
interface MarkdownProps {
content: string;
tags?: Array<Array<string>>;
}
function renderToken(t: Token | Footnotes | Footnote | FootnoteRef, tags: Array<Array<string>>): ReactNode {
try {
switch (t.type) {
case "paragraph": {
return <p>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</p>;
}
case "image": {
return <ProxyImg src={t.href} />;
}
case "heading": {
switch (t.depth) {
case 1:
return <h1>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h1>;
case 2:
return <h2>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h2>;
case 3:
return <h3>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h3>;
case 4:
return <h4>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h4>;
case 5:
return <h5>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h5>;
case 6:
return <h6>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h6>;
}
throw new Error("Invalid heading");
}
case "codespan": {
return <code>{t.raw}</code>;
}
case "code": {
return <pre>{t.raw}</pre>;
}
case "br": {
return <br />;
}
case "hr": {
return <hr />;
}
case "blockquote": {
return <blockquote>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</blockquote>;
}
case "link": {
return (
<Link to={t.href as string} className="ext" target="_blank">
{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}
</Link>
);
}
case "list": {
if (t.ordered) {
return <ol>{(t.items as Token[]).map(a => renderToken(a, tags))}</ol>;
} else {
return <ul>{(t.items as Token[]).map(a => renderToken(a, tags))}</ul>;
}
}
case "list_item": {
return <li>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</li>;
}
case "em": {
return <em>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</em>;
}
case "del": {
return <s>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</s>;
}
case "footnoteRef": {
return (
<sup>
<Link to={`#fn-${t.label}`} className="super">
[{t.label}]
</Link>
</sup>
);
}
case "footnotes":
case "footnote": {
return;
}
default: {
if ("tokens" in t) {
return (t.tokens as Array<Token>).map(a => renderToken(a, tags));
}
return transformText(t.raw, tags).map(v => {
switch (v.type) {
case "link": {
if (v.content.startsWith("nostr:")) {
return <NostrLink link={v.content} />;
} else {
return v.content;
}
}
case "mention": {
return <NostrLink link={v.content} />;
}
default: {
return v.content;
}
}
});
}
}
} catch (e) {
console.error(e);
}
}
export const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: MarkdownProps, ref) => {
const parsed = useMemo(() => {
return marked.use(markedFootnote()).lexer(props.content);
}, [props.content, props.tags]);
return (
<div className="markdown" ref={ref}>
{parsed.filter(a => a.type !== "footnote" && a.type !== "footnotes").map(a => renderToken(a, props.tags ?? []))}
</div>
);
});

View File

@ -1,4 +1,4 @@
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { NostrEvent, NostrLink } from "@snort/system";
import { findTag } from "SnortUtils";

View File

@ -131,23 +131,16 @@
align-items: center;
justify-content: center;
user-select: none;
color: var(--font-secondary-color);
font-feature-settings: "tnum";
gap: 5px;
}
.reaction-pill.reacted {
color: var(--highlight);
}
.reaction-pill:hover {
cursor: pointer;
color: var(--highlight);
.reaction-pill:not(.reacted):not(:hover) {
color: var(--font-secondary-color);
}
.trash-icon {
color: var(--error);
margin-right: auto;
}
.note-expand .body {

View File

@ -1,6 +1,6 @@
import "./Note.css";
import React from "react";
import { EventKind, TaggedNostrEvent } from "@snort/system";
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
import { NostrFileElement } from "Element/Event/NostrFileHeader";
import ZapstrEmbed from "Element/Embed/ZapstrEmbed";
import PubkeyList from "Element/Embed/PubkeyList";
@ -9,6 +9,7 @@ import { ZapGoal } from "Element/Event/ZapGoal";
import NoteReaction from "Element/Event/NoteReaction";
import ProfilePreview from "Element/User/ProfilePreview";
import { NoteInner } from "./NoteInner";
import { LongFormText } from "./LongFormText";
export interface NoteProps {
data: TaggedNostrEvent;
@ -19,6 +20,7 @@ export interface NoteProps {
onClick?: (e: TaggedNostrEvent) => void;
depth?: number;
searchedValue?: string;
threadChains?: Map<string, Array<NostrEvent>>;
options?: {
showHeader?: boolean;
showContextMenu?: boolean;
@ -32,6 +34,7 @@ export interface NoteProps {
canUnbookmark?: boolean;
canClick?: boolean;
showMediaSpotlight?: boolean;
longFormPreview?: boolean;
};
}
@ -46,18 +49,28 @@ export default function Note(props: NoteProps) {
if (ev.kind === EventKind.ZapstrTrack) {
return <ZapstrEmbed ev={ev} />;
}
if (ev.kind === EventKind.PubkeyLists) {
if (ev.kind === EventKind.PubkeyLists || ev.kind === EventKind.ContactList) {
return <PubkeyList ev={ev} className={className} />;
}
if (ev.kind === EventKind.LiveEvent) {
return <LiveEvent ev={ev} />;
}
if (ev.kind === EventKind.SetMetadata) {
return <ProfilePreview actions={<></>} pubkey={ev.pubkey} className="card" />;
return <ProfilePreview actions={<></>} pubkey={ev.pubkey} />;
}
if (ev.kind === (9041 as EventKind)) {
return <ZapGoal ev={ev} />;
}
if (ev.kind === EventKind.LongFormTextNote) {
return (
<LongFormText
ev={ev}
related={props.related}
isPreview={props.options?.longFormPreview ?? false}
onClick={() => props.onClick?.(ev)}
/>
);
}
return <NoteInner {...props} />;
}

View File

@ -0,0 +1,117 @@
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { removeUndefined, unwrap } from "@snort/shared";
import { NostrEvent, OkResponse } from "@snort/system";
import AsyncButton from "Element/AsyncButton";
import Icon from "Icons/Icon";
import { getRelayName, sanitizeRelayUrl } from "SnortUtils";
import { removeRelay } from "Login";
import useLogin from "Hooks/useLogin";
import useEventPublisher from "Hooks/useEventPublisher";
import { saveRelays } from "Pages/settings/Relays";
export function NoteBroadcaster({
evs,
onClose,
customRelays,
}: {
evs: Array<NostrEvent>;
onClose: () => void;
customRelays?: Array<string>;
}) {
const [results, setResults] = useState<Array<OkResponse>>([]);
const { formatMessage } = useIntl();
const login = useLogin();
const { publisher, system } = useEventPublisher();
async function sendEventToRelays(ev: NostrEvent) {
if (customRelays) {
return removeUndefined(
await Promise.all(
customRelays.map(async r => {
try {
return await system.WriteOnceToRelay(r, ev);
} catch (e) {
console.error(e);
}
}),
),
);
} else {
return await system.BroadcastEvent(ev, r => setResults(x => [...x, r]));
}
}
useEffect(() => {
Promise.all(evs.map(a => sendEventToRelays(a)).flat()).catch(console.error);
}, []);
async function removeRelayFromResult(r: OkResponse) {
if (publisher) {
removeRelay(login, unwrap(sanitizeRelayUrl(r.relay)));
await saveRelays(system, publisher, login.relays.item);
setResults(s => s.filter(a => a.relay !== r.relay));
}
}
async function retryPublish(r: OkResponse) {
const ev = evs.find(a => a.id === r.id);
if (ev) {
const rsp = await system.WriteOnceToRelay(unwrap(sanitizeRelayUrl(r.relay)), ev);
setResults(s =>
s.map(x => {
if (x.relay === r.relay && x.id === r.id) {
return rsp; //replace with new response
}
return x;
}),
);
}
}
return (
<div className="flex flex-col g16">
<h3>
<FormattedMessage defaultMessage="Sending notes and other stuff" />
</h3>
{results
.filter(a => a.message !== "Duplicate request")
.sort(a => (a.ok ? -1 : 1))
.map(r => (
<div className="flex items-center g16">
<Icon name={r.ok ? "check" : "x"} className={r.ok ? "success" : "error"} size={24} />
<div className="flex flex-col grow g4">
<b>{getRelayName(r.relay)}</b>
{r.message && <small>{r.message}</small>}
</div>
{!r.ok && (
<div className="flex g8">
<AsyncButton
onClick={() => retryPublish(r)}
className="p4 br-compact flex items-center secondary"
title={formatMessage({
defaultMessage: "Retry publishing",
})}>
<Icon name="refresh-ccw-01" />
</AsyncButton>
<AsyncButton
onClick={() => removeRelayFromResult(r)}
className="p4 br-compact flex items-center secondary"
title={formatMessage({
defaultMessage: "Remove from my relays",
})}>
<Icon name="trash-01" className="trash-icon" />
</AsyncButton>
</div>
)}
</div>
))}
<div className="flex-row g8">
<button type="button" onClick={() => onClose()}>
<FormattedMessage defaultMessage="Close" />
</button>
</div>
</div>
);
}

View File

@ -1,9 +1,8 @@
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { TranslateHost } from "Const";
import { System } from "index";
import Icon from "Icons/Icon";
import { setPinned, setBookmarked } from "Login";
import messages from "Element/messages";
@ -11,7 +10,8 @@ import useLogin from "Hooks/useLogin";
import useModeration from "Hooks/useModeration";
import useEventPublisher from "Hooks/useEventPublisher";
import { ReBroadcaster } from "../ReBroadcaster";
import { useState } from "react";
import SnortApi from "External/SnortApi";
import { SubscriptionType, getCurrentSubscription } from "Subscription";
export interface NoteTranslation {
text: string;
@ -30,7 +30,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
const { formatMessage } = useIntl();
const login = useLogin();
const { mute, block } = useModeration();
const publisher = useEventPublisher();
const { publisher, system } = useEventPublisher();
const [showBroadcast, setShowBroadcast] = useState(false);
const lang = window.navigator.language;
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
@ -41,13 +41,13 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
async function deleteEvent() {
if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) {
const evDelete = await publisher.delete(ev.id);
System.BroadcastEvent(evDelete);
system.BroadcastEvent(evDelete);
}
}
async function share() {
const link = NostrLink.fromEvent(ev).encode();
const url = `${window.location.protocol}//${window.location.host}/e/${link}`;
const link = NostrLink.fromEvent(ev).encode(CONFIG.eventLinkPrefix);
const url = `${window.location.protocol}//${window.location.host}/${link}`;
if ("share" in window.navigator) {
await window.navigator.share({
title: "Snort",
@ -59,30 +59,37 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
}
async function translate() {
const res = await fetch(`${TranslateHost}/translate`, {
method: "POST",
body: JSON.stringify({
q: ev.content,
source: "auto",
target: lang.split("-")[0],
}),
headers: { "Content-Type": "application/json" },
const api = new SnortApi();
const targetLang = lang.split("-")[0].toUpperCase();
const result = await api.translate({
text: [ev.content],
target_lang: targetLang,
});
if (res.ok) {
const result = await res.json();
if (typeof props.onTranslated === "function" && result) {
if ("translations" in result) {
if (
typeof props.onTranslated === "function" &&
result.translations.length > 0 &&
targetLang != result.translations[0].detected_source_language
) {
props.onTranslated({
text: result.translatedText,
fromLanguage: langNames.of(result.detectedLanguage.language),
confidence: result.detectedLanguage.confidence,
text: result.translations[0].text,
fromLanguage: langNames.of(result.translations[0].detected_source_language),
confidence: 1,
} as NoteTranslation);
}
}
}
useEffect(() => {
const sub = getCurrentSubscription(login.subscriptions);
if (sub?.type === SubscriptionType.Premium && (login.preferences.autoTranslate ?? true)) {
translate();
}
}, []);
async function copyId() {
const link = NostrLink.fromEvent(ev).encode();
const link = NostrLink.fromEvent(ev).encode(CONFIG.eventLinkPrefix);
await navigator.clipboard.writeText(link);
}
@ -90,7 +97,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
if (publisher) {
const es = [...login.pinned.item, id];
const ev = await publisher.noteList(es, Lists.Pinned);
System.BroadcastEvent(ev);
system.BroadcastEvent(ev);
setPinned(login, es, ev.created_at * 1000);
}
}
@ -99,7 +106,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
if (publisher) {
const es = [...login.bookmarked.item, id];
const ev = await publisher.noteList(es, Lists.Bookmarked);
System.BroadcastEvent(ev);
system.BroadcastEvent(ev);
setBookmarked(login, es, ev.created_at * 1000);
}
}

View File

@ -16,6 +16,7 @@
.note-creator-modal .note.card {
padding: 0;
border: none;
min-height: unset;
}
.note-creator-modal .note.card.note-quote {
@ -84,29 +85,8 @@
height: 32px;
}
.note-create-button {
width: 48px;
height: 48px;
color: white;
background: linear-gradient(90deg, rgba(239, 150, 68, 1) 0%, rgba(123, 65, 246, 1) 100%);
border: none;
border-radius: 100%;
position: fixed;
bottom: 40px;
right: calc(((100vw - 640px) / 2) - 75px);
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.note-create-button {
right: 16px;
}
}
.light .note-creator textarea {
background-color: #fff;
background-color: var(--gray-superdark);
}
.light .note-creator {

View File

@ -1,14 +1,7 @@
import "./NoteCreator.css";
import { FormattedMessage, useIntl } from "react-intl";
import {
EventKind,
NostrPrefix,
TaggedNostrEvent,
EventBuilder,
tryParseNostrLink,
NostrLink,
NostrEvent,
} from "@snort/system";
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
import classNames from "classnames";
import Icon from "Icons/Icon";
import useEventPublisher from "Hooks/useEventPublisher";
@ -21,18 +14,22 @@ import Note from "Element/Event/Note";
import { ClipboardEventHandler } from "react";
import useLogin from "Hooks/useLogin";
import { System, WasmPowWorker } from "index";
import { GetPowWorker } from "index";
import AsyncButton from "Element/AsyncButton";
import { AsyncIcon } from "Element/AsyncIcon";
import { fetchNip05Pubkey } from "@snort/shared";
import { ZapTarget } from "Zapper";
import { useNoteCreator } from "State/NoteCreator";
import { NoteBroadcaster } from "./NoteBroadcaster";
import FileUploadProgress from "./FileUpload";
import { ToggleSwitch } from "Icons/Toggle";
export function NoteCreator() {
const { formatMessage } = useIntl();
const uploader = useFileUpload();
const login = useLogin(s => ({ relays: s.relays, publicKey: s.publicKey, pow: s.preferences.pow }));
const publisher = login.pow ? useEventPublisher()?.pow(login.pow, new WasmPowWorker()) : useEventPublisher();
const { publisher: pub } = useEventPublisher();
const publisher = login.pow ? pub?.pow(login.pow, GetPowWorker()) : pub;
const note = useNoteCreator();
const relays = login.relays;
@ -102,8 +99,25 @@ export function NoteCreator() {
extraTags ??= [];
extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
}
// add quote repost
if (note.quote) {
if (!note.note.endsWith("\n")) {
note.note += "\n";
}
const link = NostrLink.fromEvent(note.quote);
note.note += `nostr:${link.encode(CONFIG.eventLinkPrefix)}`;
const quoteTag = link.toEventTag();
if (quoteTag) {
extraTags ??= [];
if (quoteTag[0] === "e") {
quoteTag[0] = "q"; // how to 'q' tag replacable events?
}
extraTags.push(quoteTag);
}
}
const hk = (eb: EventBuilder) => {
extraTags?.forEach(t => eb.tag(t));
note.extraTags?.forEach(t => eb.tag(t));
eb.kind(kind);
return eb;
};
@ -123,24 +137,11 @@ export function NoteCreator() {
}
}
async function sendEventToRelays(ev: NostrEvent) {
if (note.selectedCustomRelays) {
await Promise.all(note.selectedCustomRelays.map(r => System.WriteOnceToRelay(r, ev)));
} else {
System.BroadcastEvent(ev);
}
}
async function sendNote() {
const ev = await buildNote();
if (ev) {
await sendEventToRelays(ev);
for (const oe of note.otherEvents ?? []) {
await sendEventToRelays(oe);
}
note.update(v => {
v.reset();
v.show = false;
note.update(n => {
n.sending = (note.otherEvents ?? []).concat(ev);
});
}
}
@ -168,11 +169,24 @@ export function NoteCreator() {
const rx = await uploader.upload(file, file.name);
note.update(v => {
if (rx.header) {
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`;
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode(
CONFIG.eventLinkPrefix,
)}`;
v.note = `${v.note ? `${v.note}\n` : ""}${link}`;
v.otherEvents = [...(v.otherEvents ?? []), rx.header];
} else if (rx.url) {
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
if (rx.metadata) {
v.extraTags ??= [];
const imeta = ["imeta", `url ${rx.url}`];
if (rx.metadata.blurhash) {
imeta.push(`blurhash ${rx.metadata.blurhash}`);
}
if (rx.metadata.width && rx.metadata.height) {
imeta.push(`dim ${rx.metadata.width}x${rx.metadata.height}`);
}
v.extraTags.push(imeta);
}
} else if (rx?.error) {
v.error = rx.error;
}
@ -201,7 +215,7 @@ export function NoteCreator() {
});
}
async function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
async function onSubmit(ev: React.MouseEvent) {
ev.stopPropagation();
await sendNote();
}
@ -280,11 +294,11 @@ export function NoteCreator() {
function renderRelayCustomisation() {
return (
<div className="flex-column g8">
<div className="flex flex-col g8">
{Object.keys(relays.item || {})
.filter(el => relays.item[el].write)
.map((r, i, a) => (
<div className="p flex f-space note-creator-relay">
<div className="p flex justify-between note-creator-relay">
<div>{r}</div>
<div>
<input
@ -327,6 +341,151 @@ export function NoteCreator() {
));
}*/
function noteCreatorAdvanced() {
return (
<>
<div>
<h4>
<FormattedMessage defaultMessage="Custom Relays" />
</h4>
<p>
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
</p>
{renderRelayCustomisation()}
</div>
<div className="flex flex-col g8">
<h4>
<FormattedMessage defaultMessage="Zap Splits" />
</h4>
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
<div className="flex flex-col g8">
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
<div className="flex items-center g8">
<div className="flex flex-col f-4 g4">
<h4>
<FormattedMessage defaultMessage="Recipient" />
</h4>
<input
type="text"
value={v.value}
onChange={e =>
note.update(
v => (v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
)
}
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
/>
</div>
<div className="flex flex-col f-1 g4">
<h4>
<FormattedMessage defaultMessage="Weight" />
</h4>
<input
type="number"
min={0}
value={v.weight}
onChange={e =>
note.update(
v =>
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
)
}
/>
</div>
<div className="flex flex-col s g4">
<div>&nbsp;</div>
<Icon
name="close"
onClick={() => note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))}
/>
</div>
</div>
))}
<button
type="button"
onClick={() =>
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
}>
<FormattedMessage defaultMessage="Add" />
</button>
</div>
<span className="warning">
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
</span>
</div>
<div className="flex flex-col g8">
<h4>
<FormattedMessage defaultMessage="Sensitive Content" />
</h4>
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
<input
className="w-max"
type="text"
value={note.sensitive}
onChange={e => note.update(v => (v.sensitive = e.target.value))}
maxLength={50}
minLength={1}
placeholder={formatMessage({
defaultMessage: "Reason",
})}
/>
<span className="warning">
<FormattedMessage defaultMessage="Not all clients support this yet" />
</span>
</div>
</>
);
}
function noteCreatorFooter() {
return (
<div className="flex items-center justify-between">
<div className="flex items-center g8">
<ProfileImage
pubkey={login.publicKey ?? ""}
className="note-creator-icon"
link=""
showUsername={false}
showFollowingMark={false}
/>
{note.pollOptions === undefined && !note.replyTo && (
<AsyncIcon
iconName="list"
iconSize={24}
onClick={() => note.update(v => (v.pollOptions = ["A", "B"]))}
className={classNames("note-creator-icon", { active: note.pollOptions !== undefined })}
/>
)}
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
<AsyncIcon
iconName="settings-04"
iconSize={24}
onClick={() => note.update(v => (v.advanced = !v.advanced))}
className={classNames("note-creator-icon", { active: note.advanced })}
/>
<span className="sm:inline hidden">
<FormattedMessage defaultMessage="Preview" />
</span>
<ToggleSwitch
onClick={() => loadPreview()}
size={40}
className={classNames({ active: Boolean(note.preview) })}
/>
</div>
<div className="flex g8">
<button className="secondary" onClick={cancel}>
<FormattedMessage defaultMessage="Cancel" />
</button>
<AsyncButton onClick={onSubmit} className="primary">
{note.replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
</AsyncButton>
</div>
</div>
);
}
const handlePaste: ClipboardEventHandler<HTMLDivElement> = evt => {
if (evt.clipboardData) {
const clipboardItems = evt.clipboardData.items;
@ -346,167 +505,85 @@ export function NoteCreator() {
}
};
function noteCreatorForm() {
return (
<>
{note.replyTo && (
<>
<h4>
<FormattedMessage defaultMessage="Reply To" />
</h4>
<Note
data={note.replyTo}
related={[]}
options={{
showFooter: false,
showContextMenu: false,
showTime: false,
canClick: false,
showMedia: false,
longFormPreview: true,
}}
/>
</>
)}
{note.quote && (
<>
<h4>
<FormattedMessage defaultMessage="Quote Repost" />
</h4>
<Note
data={note.quote}
related={[]}
options={{
showFooter: false,
showContextMenu: false,
showTime: false,
canClick: false,
showMedia: false,
longFormPreview: true,
}}
/>
</>
)}
{note.preview && getPreviewNote()}
{!note.preview && (
<div onPaste={handlePaste} className={classNames("note-creator", { poll: Boolean(note.pollOptions) })}>
<Textarea
autoFocus
className={classNames("textarea", { "textarea--focused": note.active })}
onChange={c => onChange(c)}
value={note.note}
onFocus={() => note.update(v => (v.active = true))}
onKeyDown={e => {
if (e.key === "Enter" && e.metaKey) {
sendNote().catch(console.warn);
}
}}
/>
{renderPollOptions()}
</div>
)}
{uploader.progress.length > 0 && <FileUploadProgress progress={uploader.progress} />}
{noteCreatorFooter()}
{note.error && <span className="error">{note.error}</span>}
{note.advanced && noteCreatorAdvanced()}
</>
);
}
function reset() {
note.update(v => {
v.reset();
v.show = false;
});
}
if (!note.show) return null;
return (
<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => (v.show = false))}>
{note.replyTo && (
<Note
data={note.replyTo}
related={[]}
options={{
showFooter: false,
showContextMenu: false,
showTime: false,
canClick: false,
showMedia: false,
}}
/>
)}
{note.preview && getPreviewNote()}
{!note.preview && (
<div onPaste={handlePaste} className={`note-creator${note.pollOptions ? " poll" : ""}`}>
<Textarea
autoFocus
className={`textarea ${note.active ? "textarea--focused" : ""}`}
onChange={c => onChange(c)}
value={note.note}
onFocus={() => note.update(v => (v.active = true))}
onKeyDown={e => {
if (e.key === "Enter" && e.metaKey) {
sendNote().catch(console.warn);
}
}}
/>
{renderPollOptions()}
</div>
)}
<div className="flex f-space">
<div className="flex g8">
<ProfileImage
pubkey={login.publicKey ?? ""}
className="note-creator-icon"
link=""
showUsername={false}
showFollowingMark={false}
/>
{note.pollOptions === undefined && !note.replyTo && (
<div className="note-creator-icon">
<Icon name="pie-chart" onClick={() => note.update(v => (v.pollOptions = ["A", "B"]))} size={24} />
</div>
)}
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
<button className="secondary" onClick={() => note.update(v => (v.advanced = !v.advanced))}>
<FormattedMessage defaultMessage="Advanced" />
</button>
</div>
<div className="flex g8">
<button className="secondary" onClick={cancel}>
<FormattedMessage defaultMessage="Cancel" />
</button>
<AsyncButton onClick={onSubmit}>
{note.replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
</AsyncButton>
</div>
</div>
{note.error && <span className="error">{note.error}</span>}
{note.advanced && (
<>
<button className="secondary" onClick={loadPreview}>
<FormattedMessage defaultMessage="Toggle Preview" />
</button>
<div>
<h4>
<FormattedMessage defaultMessage="Custom Relays" />
</h4>
<p>
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
</p>
{renderRelayCustomisation()}
</div>
<div className="flex-column g8">
<h4>
<FormattedMessage defaultMessage="Zap Splits" />
</h4>
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
<div className="flex-column g8">
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
<div className="flex f-center g8">
<div className="flex-column f-4 g4">
<h4>
<FormattedMessage defaultMessage="Recipient" />
</h4>
<input
type="text"
value={v.value}
onChange={e =>
note.update(
v => (v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
)
}
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
/>
</div>
<div className="flex-column f-1 g4">
<h4>
<FormattedMessage defaultMessage="Weight" />
</h4>
<input
type="number"
min={0}
value={v.weight}
onChange={e =>
note.update(
v =>
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
)
}
/>
</div>
<div className="flex-column f-shrink g4">
<div>&nbsp;</div>
<Icon
name="close"
onClick={() => note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))}
/>
</div>
</div>
))}
<button
type="button"
onClick={() =>
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
}>
<FormattedMessage defaultMessage="Add" />
</button>
</div>
<span className="warning">
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
</span>
</div>
<div className="flex-column g8">
<h4>
<FormattedMessage defaultMessage="Sensitive Content" />
</h4>
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
<input
className="w-max"
type="text"
value={note.sensitive}
onChange={e => note.update(v => (v.sensitive = e.target.value))}
maxLength={50}
minLength={1}
placeholder={formatMessage({
defaultMessage: "Reason",
})}
/>
<span className="warning">
<FormattedMessage defaultMessage="Not all clients support this yet" />
</span>
</div>
</>
)}
<Modal id="note-creator" className="note-creator-modal" onClose={reset}>
{note.sending && <NoteBroadcaster evs={note.sending} onClose={reset} customRelays={note.selectedCustomRelays} />}
{!note.sending && noteCreatorForm()}
</Modal>
);
}

View File

@ -0,0 +1,20 @@
.note-create-button {
width: 48px;
height: 48px;
color: white;
background: linear-gradient(90deg, rgba(239, 150, 68, 1) 0%, rgba(123, 65, 246, 1) 100%);
border: none;
border-radius: 100%;
position: fixed;
bottom: 40px;
right: calc(((100vw - 640px) / 2) - 75px);
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.note-create-button {
right: 16px;
}
}

View File

@ -0,0 +1,64 @@
import "./NoteCreatorButton.css";
import { useRef, useMemo } from "react";
import { useLocation } from "react-router-dom";
import classNames from "classnames";
import { isFormElement } from "SnortUtils";
import useKeyboardShortcut from "Hooks/useKeyboardShortcut";
import useLogin from "Hooks/useLogin";
import Icon from "Icons/Icon";
import { useNoteCreator } from "State/NoteCreator";
import { NoteCreator } from "./NoteCreator";
export const NoteCreatorButton = ({ className }: { className?: string }) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const location = useLocation();
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
const { show, replyTo, update } = useNoteCreator(v => ({ show: v.show, replyTo: v.replyTo, update: v.update }));
useKeyboardShortcut("n", event => {
// if event happened in a form element, do nothing, otherwise focus on search input
if (event.target && !isFormElement(event.target as HTMLElement)) {
event.preventDefault();
if (buttonRef.current) {
buttonRef.current.click();
}
}
});
const shouldHideNoteCreator = useMemo(() => {
const isReply = replyTo && show;
const hideOn = [
"/settings",
"/messages",
"/new",
"/login",
"/donate",
"/e",
"/nevent",
"/note1",
"/naddr",
"/subscribe",
];
return (readonly || hideOn.some(a => location.pathname.startsWith(a))) && !isReply;
}, [location, readonly]);
return (
<>
{!shouldHideNoteCreator && (
<button
ref={buttonRef}
className={classNames("primary circle", className)}
onClick={() =>
update(v => {
v.replyTo = undefined;
v.show = true;
})
}>
<Icon name="plus" size={16} />
</button>
)}
<NoteCreator key="global-note-creator" />
</>
);
};

View File

@ -1,25 +1,26 @@
import React, { HTMLProps, useContext, useEffect, useState } from "react";
import { useIntl } from "react-intl";
import React, { forwardRef, useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useLongPress } from "use-long-press";
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, NostrLink } from "@snort/system";
import { SnortContext, useUserProfile } from "@snort/system-react";
import { normalizeReaction } from "@snort/shared";
import { useUserProfile } from "@snort/system-react";
import { Menu, MenuItem } from "@szhsin/react-menu";
import classNames from "classnames";
import { formatShort } from "Number";
import useEventPublisher from "Hooks/useEventPublisher";
import { delay, findTag, normalizeReaction } from "SnortUtils";
import { NoteCreator } from "Element/Event/NoteCreator";
import { delay, findTag, getDisplayName } from "SnortUtils";
import SendSats from "Element/SendSats";
import { ZapsSummary } from "Element/Event/Zap";
import { AsyncIcon } from "Element/AsyncIcon";
import { AsyncIcon, AsyncIconProps } from "Element/AsyncIcon";
import { useWallet } from "Wallet";
import useLogin from "Hooks/useLogin";
import { useInteractionCache } from "Hooks/useInteractionCache";
import { ZapPoolController } from "ZapPoolController";
import { System } from "index";
import { Zapper, ZapTarget } from "Zapper";
import { getDisplayName } from "Element/User/DisplayName";
import { useNoteCreator } from "State/NoteCreator";
import Icon from "Icons/Icon";
import messages from "../messages";
@ -40,12 +41,12 @@ export interface NoteFooterProps {
reposts: TaggedNostrEvent[];
zaps: ParsedZap[];
positive: TaggedNostrEvent[];
replies?: number;
ev: TaggedNostrEvent;
}
export default function NoteFooter(props: NoteFooterProps) {
const { ev, positive, reposts, zaps } = props;
const system = useContext(SnortContext);
const { formatMessage } = useIntl();
const {
publicKey,
@ -54,9 +55,8 @@ export default function NoteFooter(props: NoteFooterProps) {
} = useLogin(s => ({ preferences: s.preferences, publicKey: s.publicKey, readonly: s.readonly }));
const author = useUserProfile(ev.pubkey);
const interactionCache = useInteractionCache(publicKey, ev.id);
const publisher = useEventPublisher();
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update }));
const willRenderNoteCreator = note.show && note.replyTo?.id === ev.id;
const { publisher, system } = useEventPublisher();
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
const [tip, setTip] = useState(false);
const [zapping, setZapping] = useState(false);
const walletState = useWallet();
@ -90,7 +90,7 @@ export default function NoteFooter(props: NoteFooterProps) {
async function react(content: string) {
if (!hasReacted(content) && publisher) {
const evLike = await publisher.react(ev, content);
System.BroadcastEvent(evLike);
system.BroadcastEvent(evLike);
await interactionCache.react();
}
}
@ -99,7 +99,7 @@ export default function NoteFooter(props: NoteFooterProps) {
if (!hasReposted() && publisher) {
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
const evRepost = await publisher.repost(ev);
System.BroadcastEvent(evRepost);
system.BroadcastEvent(evRepost);
await interactionCache.repost();
}
}
@ -156,7 +156,9 @@ export default function NoteFooter(props: NoteFooterProps) {
const result = await zapper.send(wallet, targets, amount);
const totalSent = result.reduce((acc, v) => (acc += v.sent), 0);
if (totalSent > 0) {
ZapPoolController.allocate(totalSent);
if (CONFIG.features.zapPool) {
ZapPoolController?.allocate(totalSent);
}
await interactionCache.zap();
}
});
@ -195,7 +197,7 @@ export default function NoteFooter(props: NoteFooterProps) {
if (targets) {
return (
<AsyncFooterIcon
className={didZap ? "reacted" : ""}
className={didZap ? "reacted text-nostr-orange" : "hover:text-nostr-orange"}
{...longPress()}
title={formatMessage({ defaultMessage: "Zap" })}
iconName={canFastZap ? "zapFast" : "zap"}
@ -210,16 +212,40 @@ export default function NoteFooter(props: NoteFooterProps) {
function repostIcon() {
if (readonly) return;
return (
<AsyncFooterIcon
className={hasReposted() ? "reacted" : ""}
iconName="repeat"
title={formatMessage({ defaultMessage: "Repost" })}
value={reposts.length}
onClick={async () => {
if (readonly) return;
await repost();
}}
/>
<Menu
menuButton={
<AsyncFooterIcon
className={hasReposted() ? "reacted text-nostr-blue" : "hover:text-nostr-blue"}
iconName="repeat"
title={formatMessage({ defaultMessage: "Repost" })}
value={reposts.length}
/>
}
menuClassName="ctx-menu"
align="start">
<div className="close-menu-container">
{/* This menu item serves as a "close menu" button;
it allows the user to click anywhere nearby the menu to close it. */}
<MenuItem>
<div className="close-menu" />
</MenuItem>
</div>
<MenuItem onClick={() => repost()} disabled={hasReposted()}>
<Icon name="repeat" />
<FormattedMessage defaultMessage="Repost" />
</MenuItem>
<MenuItem
onClick={() =>
note.update(n => {
n.reset();
n.quote = ev;
n.show = true;
})
}>
<Icon name="edit" />
<FormattedMessage defaultMessage="Quote Repost" />
</MenuItem>
</Menu>
);
}
@ -230,7 +256,7 @@ export default function NoteFooter(props: NoteFooterProps) {
const reacted = hasReacted("+");
return (
<AsyncFooterIcon
className={reacted ? "reacted" : ""}
className={reacted ? "reacted text-nostr-red" : "hover:text-nostr-red"}
iconName={reacted ? "heart-solid" : "heart"}
title={formatMessage({ defaultMessage: "Like" })}
value={positive.length}
@ -246,10 +272,10 @@ export default function NoteFooter(props: NoteFooterProps) {
if (readonly) return;
return (
<AsyncFooterIcon
className={note.show ? "reacted" : ""}
className={note.show ? "reacted text-nostr-purple" : "hover:text-nostr-purple"}
iconName="reply"
title={formatMessage({ defaultMessage: "Reply" })}
value={0}
value={props.replies ?? 0}
onClick={async () => handleReplyButtonClick()}
/>
);
@ -275,7 +301,6 @@ export default function NoteFooter(props: NoteFooterProps) {
{tipButton()}
{powIcon()}
</div>
{willRenderNoteCreator && <NoteCreator key={`note-creator-${ev.id}`} />}
<SendSats targets={getZapTarget()} onClose={() => setTip(false)} show={tip} note={ev.id} allocatePool={true} />
</div>
<ZapsSummary zaps={zaps} />
@ -283,22 +308,15 @@ export default function NoteFooter(props: NoteFooterProps) {
);
}
interface AsyncFooterIconProps extends HTMLProps<HTMLDivElement> {
iconName: string;
value: number;
loading?: boolean;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => Promise<void>;
}
function AsyncFooterIcon(props: AsyncFooterIconProps) {
const AsyncFooterIcon = forwardRef((props: AsyncIconProps & { value: number }) => {
const mergedProps = {
...props,
iconSize: 18,
className: `reaction-pill${props.className ? ` ${props.className}` : ""}`,
className: classNames("transition duration-200 ease-in-out reaction-pill", props.className),
};
return (
<AsyncIcon {...mergedProps}>
{props.value > 0 && <div className="reaction-pill-number">{formatShort(props.value)}</div>}
</AsyncIcon>
);
}
});

View File

@ -1,88 +1,50 @@
import { Link, useNavigate } from "react-router-dom";
import React, { ReactNode, useMemo, useState } from "react";
import {
dedupeByPubkey,
findTag,
getReactions,
hexToBech32,
normalizeReaction,
profileLink,
Reaction,
tagFilterOfTextRepost,
} from "../../SnortUtils";
import useModeration from "../../Hooks/useModeration";
import { useInView } from "react-intersection-observer";
import useLogin from "../../Hooks/useLogin";
import useEventPublisher from "../../Hooks/useEventPublisher";
import { NoteContextMenu, NoteTranslation } from "./NoteContextMenu";
import { FormattedMessage, useIntl } from "react-intl";
import { UserCache } from "../../Cache";
import classNames from "classnames";
import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { findTag, hexToBech32 } from "SnortUtils";
import useModeration from "Hooks/useModeration";
import useLogin from "Hooks/useLogin";
import useEventPublisher from "Hooks/useEventPublisher";
import { NoteContextMenu, NoteTranslation } from "./NoteContextMenu";
import { UserCache } from "Cache";
import messages from "../messages";
import { System } from "../../index";
import { setBookmarked, setPinned } from "../../Login";
import { setBookmarked, setPinned } from "Login";
import Text from "../Text";
import { ProxyImg } from "../ProxyImg";
import Reveal from "./Reveal";
import Poll from "./Poll";
import ProfileImage from "../User/ProfileImage";
import Icon from "../../Icons/Icon";
import Icon from "Icons/Icon";
import NoteTime from "./NoteTime";
import NoteFooter from "./NoteFooter";
import Reactions from "./Reactions";
import HiddenNote from "./HiddenNote";
import { NoteProps } from "./Note";
import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, parseZap, TaggedNostrEvent } from "@snort/system";
import { chainKey } from "Hooks/useThreadContext";
import { ProfileLink } from "Element/User/ProfileLink";
export function NoteInner(props: NoteProps) {
const { data: ev, related, highlight, options: opt, ignoreModeration = false, className } = props;
const baseClassName = `note card${className ? ` ${className}` : ""}`;
const baseClassName = classNames("note card", className);
const navigate = useNavigate();
const [showReactions, setShowReactions] = useState(false);
const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
const { isEventMuted } = useModeration();
const { ref, inView } = useInView({ triggerOnce: true });
const { reactions, reposts, deletions, zaps } = useEventReactions(ev, related);
const login = useLogin();
const { pinned, bookmarked } = login;
const publisher = useEventPublisher();
const { publisher, system } = useEventPublisher();
const [translated, setTranslated] = useState<NoteTranslation>();
const [showTranslation, setShowTranslation] = useState(true);
const { formatMessage } = useIntl();
const reactions = useMemo(() => getReactions(related, ev.id, EventKind.Reaction), [related, ev]);
const groupReactions = useMemo(() => {
const result = reactions?.reduce(
(acc, reaction) => {
const kind = normalizeReaction(reaction.content);
const rs = acc[kind] || [];
return { ...acc, [kind]: [...rs, reaction] };
},
{
[Reaction.Positive]: [] as TaggedNostrEvent[],
[Reaction.Negative]: [] as TaggedNostrEvent[],
},
);
return {
[Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]),
[Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]),
};
}, [reactions]);
const positive = groupReactions[Reaction.Positive];
const negative = groupReactions[Reaction.Negative];
const reposts = useMemo(
() =>
dedupeByPubkey([
...getReactions(related, ev.id, EventKind.TextNote).filter(e => e.tags.some(tagFilterOfTextRepost(e, ev.id))),
...getReactions(related, ev.id, EventKind.Repost),
]),
[related, ev],
);
const zaps = useMemo(() => {
const sortedZaps = getReactions(related, ev.id, EventKind.ZapReceipt)
.map(a => parseZap(a, UserCache, ev))
.filter(z => z.valid);
sortedZaps.sort((a, b) => b.amount - a.amount);
return sortedZaps;
}, [related]);
const totalReactions = positive.length + negative.length + reposts.length + zaps.length;
const totalReactions = reactions.positive.length + reactions.negative.length + reposts.length + zaps.length;
const options = {
showHeader: true,
@ -99,7 +61,7 @@ export function NoteInner(props: NoteProps) {
if (window.confirm(formatMessage(messages.ConfirmUnpin))) {
const es = pinned.item.filter(e => e !== id);
const ev = await publisher.noteList(es, Lists.Pinned);
System.BroadcastEvent(ev);
system.BroadcastEvent(ev);
setPinned(login, es, ev.created_at * 1000);
}
}
@ -110,53 +72,36 @@ export function NoteInner(props: NoteProps) {
if (window.confirm(formatMessage(messages.ConfirmUnbookmark))) {
const es = bookmarked.item.filter(e => e !== id);
const ev = await publisher.noteList(es, Lists.Bookmarked);
System.BroadcastEvent(ev);
system.BroadcastEvent(ev);
setBookmarked(login, es, ev.created_at * 1000);
}
}
}
const innerContent = () => {
if (ev.kind === EventKind.LongFormTextNote) {
const title = findTag(ev, "title");
const summary = findTag(ev, "simmary");
const image = findTag(ev, "image");
return (
<div className="long-form-note">
<h3>{title}</h3>
<div className="text">
<p>{summary}</p>
<Text
id={ev.id}
content={ev.content}
highlighText={props.searchedValue}
tags={ev.tags}
creator={ev.pubkey}
depth={props.depth}
truncate={255}
disableLinkPreview={true}
disableMediaSpotlight={!(props.options?.showMediaSpotlight ?? true)}
/>
{image && <ProxyImg src={image} />}
</div>
</div>
);
} else {
const body = ev?.content ?? "";
return (
<Text
id={ev.id}
highlighText={props.searchedValue}
content={body}
tags={ev.tags}
creator={ev.pubkey}
depth={props.depth}
disableMedia={!(options.showMedia ?? true)}
disableMediaSpotlight={!(props.options?.showMediaSpotlight ?? true)}
/>
);
}
};
const innerContent = useMemo(() => {
const body = translated && showTranslation ? translated.text : ev?.content ?? "";
const id = translated && showTranslation ? `${ev.id}-translated` : ev.id;
return (
<Text
id={id}
highlighText={props.searchedValue}
content={body}
tags={ev.tags}
creator={ev.pubkey}
depth={props.depth}
disableMedia={!(options.showMedia ?? true)}
disableMediaSpotlight={!(props.options?.showMediaSpotlight ?? true)}
/>
);
}, [
ev,
translated,
showTranslation,
props.searchedValue,
props.depth,
options.showMedia,
props.options?.showMediaSpotlight,
]);
const transformBody = () => {
if (deletions?.length > 0) {
@ -194,11 +139,11 @@ export function NoteInner(props: NoteProps) {
<FormattedMessage defaultMessage="Click here to load anyway" />
</>
}>
{innerContent()}
{innerContent}
</Reveal>
);
}
return innerContent();
return innerContent;
};
function goToEvent(
@ -219,9 +164,9 @@ export function NoteInner(props: NoteProps) {
const link = NostrLink.fromEvent(eTarget);
// detect cmd key and open in new tab
if (e.metaKey) {
window.open(`/e/${link.encode()}`, "_blank");
window.open(`/${link.encode(CONFIG.eventLinkPrefix)}`, "_blank");
} else {
navigate(`/e/${link.encode()}`, {
navigate(`/${link.encode(CONFIG.eventLinkPrefix)}`, {
state: eTarget,
});
}
@ -248,7 +193,11 @@ export function NoteInner(props: NoteProps) {
mentions.push({
pk,
name: u?.name ?? shortNpub,
link: <Link to={profileLink(pk)}>{u?.name ? `@${u.name}` : shortNpub}</Link>,
link: (
<ProfileLink pubkey={pk} user={u}>
{u?.name ? `@${u.name}` : shortNpub}
</ProfileLink>
),
});
}
mentions.sort(a => (a.name.startsWith(NostrPrefix.PublicKey) ? 1 : -1));
@ -264,6 +213,7 @@ export function NoteInner(props: NoteProps) {
const pubMentions =
mentions.length > maxMentions ? mentions?.slice(0, maxMentions).map(renderMention) : mentions?.map(renderMention);
const others = mentions.length > maxMentions ? formatMessage(messages.Others, { n: othersLength }) : "";
const link = replyLink?.encode(CONFIG.eventLinkPrefix);
return (
<div className="reply">
re:&nbsp;
@ -272,13 +222,13 @@ export function NoteInner(props: NoteProps) {
{pubMentions} {others}
</>
) : (
replyLink && <Link to={`/e/${replyLink.encode()}`}>{replyLink.encode().substring(0, 12)}</Link>
replyLink && <Link to={`/${link}`}>{link?.substring(0, 12)}</Link>
)}
</div>
);
}
const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls, EventKind.LongFormTextNote];
const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls];
if (!canRenderAsTextNote.includes(ev.kind)) {
const alt = findTag(ev, "alt");
if (alt) {
@ -303,15 +253,19 @@ export function NoteInner(props: NoteProps) {
if (translated && translated.confidence > 0.5) {
return (
<>
<p className="highlight">
<span
className="text-xs font-semibold text-gray-light select-none"
onClick={e => {
e.stopPropagation();
setShowTranslation(s => !s);
}}>
<FormattedMessage {...messages.TranslatedFrom} values={{ lang: translated.fromLanguage }} />
</p>
{translated.text}
</span>
</>
);
} else if (translated) {
return (
<p className="highlight">
<p className="text-xs font-semibold text-gray-light">
<FormattedMessage {...messages.TranslationFailed} />
</p>
);
@ -374,12 +328,20 @@ export function NoteInner(props: NoteProps) {
</div>
)}
</div>
{options.showFooter && <NoteFooter ev={ev} positive={positive} reposts={reposts} zaps={zaps} />}
{options.showFooter && (
<NoteFooter
ev={ev}
positive={reactions.positive}
reposts={reposts}
zaps={zaps}
replies={props.threadChains?.get(chainKey(ev))?.length}
/>
)}
<Reactions
show={showReactions}
setShow={setShowReactions}
positive={positive}
negative={negative}
positive={reactions.positive}
negative={reactions.negative}
reposts={reposts}
zaps={zaps}
/>
@ -388,7 +350,7 @@ export function NoteInner(props: NoteProps) {
}
const note = (
<div className={`${baseClassName}${highlight ? " active " : " "}`} onClick={e => goToEvent(e, ev)} ref={ref}>
<div className={classNames(baseClassName, { active: highlight })} onClick={e => goToEvent(e, ev)} ref={ref}>
{content()}
</div>
);

View File

@ -4,10 +4,9 @@ import { useMemo } from "react";
import { EventKind, NostrEvent, TaggedNostrEvent, NostrPrefix, EventExt } from "@snort/system";
import Note from "Element/Event/Note";
import { getDisplayName } from "Element/User/DisplayName";
import { eventLink, hexToBech32 } from "SnortUtils";
import { eventLink, hexToBech32, getDisplayName } from "SnortUtils";
import useModeration from "Hooks/useModeration";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import Icon from "Icons/Icon";
import { useUserProfile } from "@snort/system-react";
import { useInView } from "react-intersection-observer";

View File

@ -1,4 +1,4 @@
import { TaggedNostrEvent, ParsedZap } from "@snort/system";
import { TaggedNostrEvent, ParsedZap, NostrLink } from "@snort/system";
import { LNURL } from "@snort/shared";
import { useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
@ -21,7 +21,7 @@ type PollTally = "zaps" | "pubkeys";
export default function Poll(props: PollProps) {
const { formatMessage } = useIntl();
const publisher = useEventPublisher();
const { publisher } = useEventPublisher();
const { wallet } = useWallet();
const { preferences: prefs, publicKey: myPubKey, relays } = useLogin();
const pollerProfile = useUserProfile(props.ev.pubkey);
@ -58,7 +58,7 @@ export default function Poll(props: PollProps) {
setVoting(opt);
const r = Object.keys(relays.item);
const zap = await publisher.zap(amount * 1000, props.ev.pubkey, r, props.ev.id, undefined, eb =>
const zap = await publisher.zap(amount * 1000, props.ev.pubkey, r, NostrLink.fromEvent(props.ev), undefined, eb =>
eb.tag(["poll_option", opt.toString()]),
);
@ -108,7 +108,7 @@ export default function Poll(props: PollProps) {
return (
<>
<div className="flex f-space p">
<div className="flex justify-between p">
<small>
<FormattedMessage
defaultMessage="You are voting with {amount} sats"
@ -147,7 +147,7 @@ export default function Poll(props: PollProps) {
const weight = totalVotes === 0 ? 0 : total / totalVotes;
return (
<div key={a[1]} className="flex" onClick={e => zapVote(e, opt)}>
<div className="f-grow">{opt === voting ? <Spinner /> : <>{desc}</>}</div>
<div className="grow">{opt === voting ? <Spinner /> : <>{desc}</>}</div>
{showResults && (
<>
<div className="flex">

View File

@ -1,6 +1,6 @@
.reactions-modal .modal-body {
padding: 24px 32px;
background-color: #1b1b1b;
background-color: var(--gray-superdark);
border-radius: 16px;
position: relative;
min-height: 33vh;

View File

@ -1,5 +1,4 @@
import "../Reveal.css";
import Icon from "Icons/Icon";
import { WarningNotice } from "Element/WarningNotice";
import { useState } from "react";
interface RevealProps {
@ -7,22 +6,12 @@ interface RevealProps {
children: React.ReactNode;
}
export default function Reveal(props: RevealProps): JSX.Element {
export default function Reveal(props: RevealProps) {
const [reveal, setReveal] = useState(false);
if (!reveal) {
return (
<div
onClick={e => {
e.stopPropagation();
setReveal(true);
}}
className="note-notice flex g8">
<Icon name="alert-circle" size={24} />
<div>{props.message}</div>
</div>
);
} else {
return <>{props.children}</>;
return <WarningNotice onClick={() => setReveal(true)}>{props.message}</WarningNotice>;
} else if (props.children) {
return props.children;
}
}

View File

@ -1,4 +1,4 @@
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { FileExtensionRegex } from "Const";
import Reveal from "Element/Event/Reveal";

View File

@ -12,3 +12,7 @@
font-weight: normal;
text-decoration: underline;
}
.show-more-container {
min-height: 40px;
}

View File

@ -1,7 +1,8 @@
import "./ShowMore.css";
import { useIntl } from "react-intl";
import messages from "../messages";
import { FormattedMessage } from "react-intl";
import { useInView } from "react-intersection-observer";
import { useEffect } from "react";
import classNames from "classnames";
interface ShowMoreProps {
text?: string;
@ -10,16 +11,29 @@ interface ShowMoreProps {
}
const ShowMore = ({ text, onClick, className = "" }: ShowMoreProps) => {
const { formatMessage } = useIntl();
const defaultText = formatMessage(messages.ShowMore);
const classNames = className ? `show-more ${className}` : "show-more";
return (
<div className="show-more-container">
<button className={classNames} onClick={onClick}>
{text || defaultText}
<button className={classNames("show-more", className)} onClick={onClick}>
{text || <FormattedMessage defaultMessage="Show More" />}
</button>
</div>
);
};
export default ShowMore;
export function ShowMoreInView({ text, onClick, className }: ShowMoreProps) {
const { ref, inView } = useInView();
useEffect(() => {
if (inView) {
onClick();
}
}, [inView]);
return (
<div className={classNames("show-more-container", className)} ref={ref}>
{text}
</div>
);
}

View File

@ -2,9 +2,10 @@ import "./Thread.css";
import { useMemo, useState, ReactNode, useContext } from "react";
import { useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink } from "@snort/system";
import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink, NostrLink } from "@snort/system";
import classNames from "classnames";
import { getReactions, getAllReactions } from "SnortUtils";
import { getAllLinkReactions, getLinkReactions } from "SnortUtils";
import BackButton from "Element/BackButton";
import Note from "Element/Event/Note";
import NoteGhost from "Element/Event/NoteGhost";
@ -50,6 +51,7 @@ const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProp
key={a.id}
related={related}
onClick={onNavigate}
threadChains={chains}
/>
<div className="line-container"></div>
</div>
@ -82,18 +84,22 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, related, chains, on
const [collapsed, setCollapsed] = useState(!activeInReplies);
const hasMultipleNotes = replies.length > 1;
const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
const className = `subthread-container ${isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"}`;
const className = classNames(
"subthread-container",
isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid",
);
return (
<>
<div className={className}>
<Divider variant="small" />
<Note
highlight={active === note.id}
className={`thread-note ${isLastVisibleNote ? "is-last-note" : ""}`}
className={classNames("thread-note", { "is-last-note": isLastVisibleNote })}
data={note}
key={note.id}
related={related}
onClick={onNavigate}
threadChains={chains}
/>
<div className="line-container"></div>
</div>
@ -154,16 +160,19 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return (
<>
<div
className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${
isLast ? "subthread-last" : "subthread-mid"
}`}>
className={classNames("subthread-container", {
"subthread-multi": hasMultipleNotes,
"subthread-last": isLast,
"subthread-mid": !isLast,
})}>
<Divider variant="small" />
<Note
highlight={active === first.id}
className={`thread-note ${isLastSubthread && isLast ? "is-last-note" : ""}`}
className={classNames("thread-note", { "is-last-note": isLastSubthread && isLast })}
data={first}
key={first.id}
related={related}
threadChains={chains}
/>
<div className="line-container"></div>
</div>
@ -185,17 +194,20 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return (
<div
key={r.id}
className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${
lastReply ? "subthread-last" : "subthread-mid"
}`}>
className={classNames("subthread-container", {
"subthread-multi": !lastReply,
"subthread-last": !lastReply,
"subthread-mid": lastReply,
})}>
<Divider variant="small" />
<Note
className={`thread-note ${lastNote ? "is-last-note" : ""}`}
className={classNames("thread-note", { "is-last-note": lastNote })}
highlight={active === r.id}
data={r}
key={r.id}
related={related}
onClick={onNavigate}
threadChains={chains}
/>
<div className="line-container"></div>
</div>
@ -205,9 +217,10 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
);
};
export function ThreadRoute() {
export function ThreadRoute({ id }: { id?: string }) {
const params = useParams();
const link = parseNostrLink(params.id ?? "", NostrPrefix.Note);
const resolvedId = id ?? params.id;
const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note);
return (
<ThreadContextWrapper link={link}>
@ -225,7 +238,7 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
function navigateThread(e: TaggedNostrEvent) {
thread.setCurrent(e.id);
//router.navigate(`/e/${NostrLink.fromEvent(e).encode()}`, { replace: true })
//router.navigate(`/${NostrLink.fromEvent(e).encode()}`, { replace: true })
}
const parent = useMemo(() => {
@ -247,9 +260,10 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
className={className}
key={note.id}
data={note}
related={getReactions(thread.reactions, note.id)}
related={getLinkReactions(thread.reactions, NostrLink.fromEvent(note))}
options={{ showReactionsLink: true, showMediaSpotlight: !props.disableSpotlight }}
onClick={navigateThread}
threadChains={thread.chains}
/>
);
} else {
@ -267,9 +281,9 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
<Subthread
active={thread.current}
notes={replies}
related={getAllReactions(
related={getAllLinkReactions(
thread.reactions,
replies.map(a => a.id),
replies.map(a => NostrLink.fromEvent(a)),
)}
chains={thread.chains}
onNavigate={navigateThread}

View File

@ -17,7 +17,7 @@ const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean
return valid && sender ? (
<div className="card">
<div className="flex f-space">
<div className="flex justify-between">
<ProfileImage pubkey={sender} />
{receiver !== pubKey && showZapped && <ProfileImage pubkey={unwrap(receiver)} />}
<h3>

View File

@ -1,21 +1,3 @@
.zap-goal {
}
.zap-goal h1 {
line-height: 1em;
}
.zap-goal .progress {
position: relative;
height: 1em;
border-radius: 4px;
overflow: hidden;
background-color: var(--gray);
}
.zap-goal .progress > div {
position: absolute;
background-color: var(--success);
width: var(--progress);
height: 100%;
}

View File

@ -1,5 +1,5 @@
import "./ZapGoal.css";
import { CSSProperties, useState } from "react";
import { useState } from "react";
import { NostrEvent, NostrLink } from "@snort/system";
import useZapsFeed from "Feed/ZapsFeed";
import { formatShort } from "Number";
@ -7,17 +7,19 @@ import { findTag } from "SnortUtils";
import Icon from "Icons/Icon";
import SendSats from "../SendSats";
import { Zapper } from "Zapper";
import Progress from "Element/Progress";
import { FormattedNumber } from "react-intl";
export function ZapGoal({ ev }: { ev: NostrEvent }) {
const [zap, setZap] = useState(false);
const zaps = useZapsFeed(NostrLink.fromEvent(ev));
const target = Number(findTag(ev, "amount"));
const amount = zaps.reduce((acc, v) => (acc += v.amount * 1000), 0);
const progress = 100 * (amount / target);
const progress = amount / target;
return (
<div className="zap-goal card">
<div className="flex f-space">
<div className="flex items-center justify-between">
<h2>{ev.content}</h2>
<div className="zap-button flex" onClick={() => setZap(true)}>
<Icon name="zap" size={15} />
@ -25,20 +27,15 @@ export function ZapGoal({ ev }: { ev: NostrEvent }) {
<SendSats targets={Zapper.fromEvent(ev)} show={zap} onClose={() => setZap(false)} />
</div>
<div className="flex f-space">
<div>{progress.toFixed(1)}%</div>
<div className="flex justify-between">
<div>
<FormattedNumber value={progress} style="percent" />
</div>
<div>
{formatShort(amount / 1000)}/{formatShort(target / 1000)}
</div>
</div>
<div className="progress">
<div
style={
{
"--progress": `${Math.min(100, progress)}%`,
} as CSSProperties
}></div>
</div>
<Progress value={progress} />
</div>
);
}

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { useInView } from "react-intersection-observer";
import messages from "../messages";

View File

@ -1,5 +1,5 @@
import "./Timeline.css";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { useCallback, useMemo } from "react";
import { useInView } from "react-intersection-observer";
import { TaggedNostrEvent, EventKind, u256 } from "@snort/system";
@ -84,7 +84,7 @@ const Timeline = (props: TimelineProps) => {
<>
<div className="card latest-notes" onClick={() => onShowLatest()} ref={ref}>
{latestAuthors.slice(0, 3).map(p => {
return <ProfileImage pubkey={p} showUsername={false} link={""} />;
return <ProfileImage pubkey={p} showUsername={false} link={""} showProfileCard={false} />;
})}
<FormattedMessage
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
@ -95,7 +95,7 @@ const Timeline = (props: TimelineProps) => {
{!inView && (
<div className="card latest-notes latest-notes-fixed pointer fade-in" onClick={() => onShowLatest(true)}>
{latestAuthors.slice(0, 3).map(p => {
return <ProfileImage pubkey={p} showUsername={false} link={""} />;
return <ProfileImage pubkey={p} showUsername={false} link={""} showProfileCard={false} />;
})}
<FormattedMessage
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
@ -117,7 +117,7 @@ const Timeline = (props: TimelineProps) => {
/>
))}
{(props.loadMore === undefined || props.loadMore === true) && (
<div className="flex f-center">
<div className="flex items-center">
<button type="button" onClick={() => feed.loadMore()}>
<FormattedMessage defaultMessage="Load more" />
</button>

View File

@ -1,6 +1,6 @@
import "./Timeline.css";
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { TaggedNostrEvent, EventKind, u256, NostrEvent, NostrLink } from "@snort/system";
import { unixNow } from "@snort/shared";
import { SnortContext } from "@snort/system-react";
@ -49,7 +49,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
function <T extends NostrEvent>(nts: Array<T>) {
const a = nts.filter(a => a.kind !== EventKind.LiveEvent);
return a
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true))
.filter(a => !isMuted(a.pubkey) && login.follows.item.includes(a.pubkey) && (props.noteFilter?.(a) ?? true));
},
[props.postsOnly, muted, login.follows.timestamp],
@ -126,7 +126,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
/>
),
)}
<div className="flex f-center p">
<div className="flex items-center p">
<AsyncButton
onClick={async () => {
await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at);

View File

@ -1,22 +0,0 @@
import { useState, useEffect, FC, ComponentProps } from "react";
import { useIntl, FormattedMessage } from "react-intl";
type ExtendedProps = ComponentProps<typeof FormattedMessage>;
const ExtendedFormattedMessage: FC<ExtendedProps> = props => {
const { id, defaultMessage, values } = props;
const { formatMessage } = useIntl();
const [processedMessage, setProcessedMessage] = useState<string | null>(null);
useEffect(() => {
const translatedMessage = formatMessage({ id, defaultMessage }, values);
if (typeof translatedMessage === "string") {
setProcessedMessage(translatedMessage.replace("Snort", process.env.APP_NAME_CAPITALIZED || "Snort"));
}
}, [id, defaultMessage, values, formatMessage]);
return <>{processedMessage}</>;
};
export default ExtendedFormattedMessage;

View File

@ -23,14 +23,16 @@ import WavlakeEmbed from "Element/Embed/WavlakeEmbed";
import LinkPreview from "Element/Embed/LinkPreview";
import NostrLink from "Element/Embed/NostrLink";
import MagnetLink from "Element/Embed/MagnetLink";
import { ReactNode } from "react";
interface HypeTextProps {
link: string;
children?: ReactNode | Array<ReactNode> | null;
depth?: number;
showLinkPreview?: boolean;
}
export default function HyperText({ link, depth, showLinkPreview }: HypeTextProps) {
export default function HyperText({ link, depth, showLinkPreview, children }: HypeTextProps) {
const a = link;
try {
const url = new URL(a);
@ -78,7 +80,7 @@ export default function HyperText({ link, depth, showLinkPreview }: HypeTextProp
return (
<>
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
{a}
{children ?? a}
</a>
{/*<NostrNestsEmbed link={a} />,*/}
</>
@ -100,7 +102,7 @@ export default function HyperText({ link, depth, showLinkPreview }: HypeTextProp
}
return (
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
{a}
{children ?? a}
</a>
);
}

View File

@ -1,14 +1,19 @@
import classNames from "classnames";
import Icon, { IconProps } from "Icons/Icon";
import type { ReactNode } from "react";
interface IconButtonProps {
onClick(): void;
children: ReactNode;
onClick?: () => void;
icon: IconProps;
className?: string;
children?: ReactNode;
}
const IconButton = ({ onClick, children }: IconButtonProps) => {
const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
return (
<button className="icon" type="button" onClick={onClick}>
<div className="icon-wrapper">{children}</div>
<button className={classNames("icon", className)} type="button" onClick={onClick}>
<Icon {...icon} />
{children}
</button>
);
};

View File

@ -1,5 +1,5 @@
import { useNavigate } from "react-router-dom";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
export default function AccountName({ name = "", link = true }) {
const navigate = useNavigate();

View File

@ -1,11 +1,11 @@
import AccountName from "./AccountName";
import useLogin from "../../Hooks/useLogin";
import { useUserProfile } from "@snort/system-react";
import { System } from "../../index";
import { UserCache } from "../../Cache";
import useEventPublisher from "../../Hooks/useEventPublisher";
import { mapEventToProfile } from "@snort/system";
import FormattedMessage from "Element/FormattedMessage";
import { useUserProfile } from "@snort/system-react";
import AccountName from "./AccountName";
import useLogin from "Hooks/useLogin";
import { UserCache } from "Cache";
import useEventPublisher from "Hooks/useEventPublisher";
import { FormattedMessage } from "react-intl";
export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) {
const { publicKey, readonly } = useLogin(s => ({
@ -13,7 +13,7 @@ export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) {
readonly: s.readonly,
}));
const profile = useUserProfile(publicKey);
const publisher = useEventPublisher();
const { publisher, system } = useEventPublisher();
async function saveProfile(nip05: string) {
if (readonly) {
@ -35,7 +35,7 @@ export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) {
if (publisher) {
const ev = await publisher.metadata(userCopy);
System.BroadcastEvent(ev);
system.BroadcastEvent(ev);
const newProfile = mapEventToProfile(ev);
if (newProfile) {

View File

@ -5,8 +5,8 @@ import { LoginStore } from "Login";
import AccountName from "./AccountName";
import ActiveAccount from "./ActiveAccount";
import ReservedAccount from "./ReservedAccount";
import { ProfileLoader } from "../../index";
import FormattedMessage from "Element/FormattedMessage";
import { ProfileLoader } from "index";
import { FormattedMessage } from "react-intl";
import { injectIntl } from "react-intl";
import messages from "Element/messages";

View File

@ -1,5 +1,5 @@
import AccountName from "./AccountName";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
export default function ReservedAccount({ name = "", enableReserved = () => {}, declineReserved = () => {} }) {
return (

View File

@ -1,5 +1,5 @@
import { NostrEvent, NostrLink } from "@snort/system";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { findTag } from "SnortUtils";
@ -70,7 +70,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
}
return (
<div className="flex f-space br p24 bg-primary">
<div className="flex justify-between br p24 bg-primary">
<div className="flex g12">
<ProfileImage pubkey={host} showUsername={false} size={56} />
<div>

View File

@ -32,7 +32,7 @@ function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
const image = findTag(ev, "image");
const status = findTag(ev, "status");
const link = NostrLink.fromEvent(ev).encode();
const link = NostrLink.fromEvent(ev).encode(CONFIG.eventLinkPrefix);
const imageProxy = proxy(image ?? "");
return (
@ -43,7 +43,7 @@ function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
"--img": `url(${imageProxy})`,
} as CSSProperties
}></div>
<div className="flex f-col details">
<div className="flex flex-col details">
<div className="flex g2">
<span className="live">{status}</span>
<div className="reaction-pill">

View File

@ -4,7 +4,7 @@ const Logo = () => {
const navigate = useNavigate();
return (
<h1 className="logo" onClick={() => navigate("/")}>
{process.env.APP_NAME}
{CONFIG.appNameCapitalized}
</h1>
);
};

View File

@ -1,4 +1,4 @@
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { logout } from "Login";

View File

@ -1,3 +1,4 @@
import { createPortal } from "react-dom";
import "./Modal.css";
import { ReactNode, useEffect } from "react";
@ -26,7 +27,7 @@ export default function Modal(props: ModalProps) {
};
}, []);
return (
return createPortal(
<div className={`modal${props.className ? ` ${props.className}` : ""}`} onClick={props.onClose}>
<div className="modal-body" onClick={props.onClose}>
<div
@ -37,6 +38,7 @@ export default function Modal(props: ModalProps) {
{props.children}
</div>
</div>
</div>
</div>,
document.body,
);
}

View File

@ -25,7 +25,6 @@ import SnortServiceProvider from "Nip05/SnortServiceProvider";
import { UserCache } from "Cache";
import messages from "./messages";
import { System } from "index";
type Nip05ServiceProps = {
name: string;
@ -45,7 +44,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
const { formatMessage } = useIntl();
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const user = useUserProfile(publicKey);
const publisher = useEventPublisher();
const { publisher, system } = useEventPublisher();
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
const [error, setError] = useState<ServiceError>();
@ -216,7 +215,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
nip05,
} as UserMetadata;
const ev = await publisher.metadata(newProfile);
System.BroadcastEvent(ev);
system.BroadcastEvent(ev);
if (props.onSuccess) {
props.onSuccess(nip05);
}
@ -251,7 +250,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
)}
{error && <b className="error">{error.error}</b>}
{!registerStatus && (
<div className="flex mb10">
<div className="flex items-center mb10">
<input
type="text"
className="nip-handle"
@ -306,7 +305,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
title={formatMessage(messages.Buying, { item: `${handle}@${domain}` })}
/>
{registerStatus?.paid && (
<div className="flex f-col">
<div className="flex flex-col">
<h4>
<FormattedMessage {...messages.OrderPaid} />
</h4>

View File

@ -0,0 +1,20 @@
import Icon from "Icons/Icon";
import AsyncButton from "./AsyncButton";
import { FormattedMessage } from "react-intl";
import classNames from "classnames";
export function Offline({ onRetry, className }: { onRetry?: () => void | Promise<void>; className?: string }) {
return (
<div className={classNames("flex items-center g8", className)}>
<Icon name="wifi-off" className="error" />
<div className="error">
<FormattedMessage defaultMessage="Offline" />
</div>
{onRetry && (
<AsyncButton onClick={onRetry}>
<FormattedMessage defaultMessage="Retry" />
</AsyncButton>
)}
</div>
);
}

View File

@ -2,7 +2,7 @@ import Spinner from "Icons/Spinner";
export default function PageSpinner() {
return (
<div className="flex f-center">
<div className="flex justify-center items-center">
<Spinner width={50} height={50} />
</div>
);

View File

@ -9,7 +9,7 @@ import useEventPublisher from "Hooks/useEventPublisher";
import { LoginStore, createPublisher, sessionNeedsPin } from "Login";
import Modal from "./Modal";
import AsyncButton from "./AsyncButton";
import { WasmPowWorker } from "index";
import { GetPowWorker } from "index";
export function PinPrompt({
onResult,
@ -63,7 +63,7 @@ export function PinPrompt({
submitButtonRef.current.click();
}
}}>
<div className="flex-column g12">
<div className="flex flex-col g12">
<h2>
<FormattedMessage defaultMessage="Enter Pin" />
</h2>
@ -93,7 +93,7 @@ export function PinPrompt({
export function LoginUnlock() {
const login = useLogin();
const publisher = useEventPublisher();
const { publisher } = useEventPublisher();
async function encryptMigration(pin: string) {
const k = unwrap(login.privateKey);
@ -101,7 +101,7 @@ export function LoginUnlock() {
const pub = EventPublisher.privateKey(k);
if (login.preferences.pow) {
pub.pow(login.preferences.pow, new WasmPowWorker());
pub.pow(login.preferences.pow, GetPowWorker());
}
LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({
@ -118,7 +118,7 @@ export function LoginUnlock() {
const pub = createPublisher(login);
if (pub) {
if (login.preferences.pow) {
pub.pow(login.preferences.pow, new WasmPowWorker());
pub.pow(login.preferences.pow, GetPowWorker());
}
LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({
@ -145,7 +145,7 @@ export function LoginUnlock() {
<FormattedMessage
defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open {site}."
values={{
site: process.env.APP_NAME_CAPITALIZED,
site: CONFIG.appNameCapitalized,
}}
/>
</p>

View File

@ -0,0 +1,23 @@
.progress {
position: relative;
height: 1em;
border-radius: 4px;
overflow: hidden;
background-color: var(--gray);
}
.progress > div {
position: absolute;
background-color: var(--success);
width: var(--progress);
height: 100%;
}
.progress > span {
position: absolute;
width: 100%;
height: 100%;
text-align: center;
font-size: small;
line-height: 1em;
}

View File

@ -0,0 +1,21 @@
import "./Progress.css";
import { FormattedNumber } from "react-intl";
import { CSSProperties, ReactNode } from "react";
export default function Progress({ value, status }: { value: number; status?: ReactNode }) {
const v = Math.max(0.01, Math.min(1, value));
return (
<div className="progress">
<div
style={
{
"--progress": `${v * 100}%`,
} as CSSProperties
}></div>
<span>
{status}
<FormattedNumber value={v} style="percent" />
</span>
</div>
);
}

View File

@ -1,6 +1,6 @@
import useImgProxy from "Hooks/useImgProxy";
import React, { useState } from "react";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { getUrlHostname } from "SnortUtils";
interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {

View File

@ -1,5 +1,5 @@
import { useContext, useState } from "react";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import { TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";
@ -23,11 +23,11 @@ export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: Tagged
function renderRelayCustomisation() {
return (
<div className="flex-column g8">
<div className="flex flex-col g8">
{Object.keys(relays.item || {})
.filter(el => relays.item[el].write)
.map((r, i, a) => (
<div className="card flex f-space">
<div className="card flex justify-between">
<div>{r}</div>
<div>
<input

View File

@ -1,8 +1,6 @@
.relay {
margin-top: 10px;
background-color: var(--gray-secondary);
border-radius: 5px;
text-align: start;
display: grid;
grid-template-columns: min-content auto;
overflow: hidden;
@ -12,31 +10,3 @@
.relay > div {
padding: 5px;
}
.relay-extra {
padding: 5px;
margin: 0 5px;
background-color: var(--gray-tertiary);
border-radius: 0 0 5px 5px;
white-space: nowrap;
font-size: var(--font-size-small);
}
.icon-btn {
padding: 2px 10px;
border-radius: 10px;
background-color: var(--gray);
user-select: none;
color: var(--font-color);
}
.icon-btn:hover {
cursor: pointer;
}
.checkmark {
margin-left: 0.5em;
padding: 2px 10px;
background-color: var(--gray);
border-radius: 10px;
}

View File

@ -1,18 +1,17 @@
import "./Relay.css";
import { useMemo } from "react";
import FormattedMessage from "Element/FormattedMessage";
import { useContext, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { RelaySettings } from "@snort/system";
import { unixNowMs } from "@snort/shared";
import classNames from "classnames";
import useRelayState from "Feed/RelayState";
import { System } from "index";
import { SnortContext } from "@snort/system-react";
import { getRelayName, unwrap } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import { setRelays } from "Login";
import Icon from "Icons/Icon";
import messages from "../messages";
import { removeRelay, setRelays } from "Login";
import { RelayFavicon } from "./RelaysMetadata";
import { AsyncIcon } from "Element/AsyncIcon";
export interface RelayProps {
addr: string;
@ -20,9 +19,11 @@ export interface RelayProps {
export default function Relay(props: RelayProps) {
const navigate = useNavigate();
const system = useContext(SnortContext);
const login = useLogin();
const relaySettings = unwrap(
login.relays.item[props.addr] ?? System.Sockets.find(a => a.address === props.addr)?.settings ?? {},
login.relays.item[props.addr] ?? system.Sockets.find(a => a.address === props.addr)?.settings ?? {},
);
const state = useRelayState(props.addr);
const name = useMemo(() => getRelayName(props.addr), [props.addr]);
@ -40,48 +41,52 @@ export default function Relay(props: RelayProps) {
return (
<>
<div className={`relay w-max`}>
<div className={`flex ${state?.connected ? "bg-success" : "bg-error"}`}>
<Icon name="wifi" />
<div className="relay">
<div className={classNames("flex items-center", state?.connected ? "bg-success" : "bg-error")}>
<RelayFavicon url={props.addr} />
</div>
<div className="f-grow f-col">
<div className="flex mb10">
<b className="f-2">{name}</b>
<div className="f-1">
<FormattedMessage {...messages.Write} />
<span
className="checkmark"
<div className="flex flex-col g8">
<div>
<b>{name}</b>
</div>
{!state?.ephemeral && (
<div className="flex g8">
<AsyncIcon
iconName="write"
iconSize={16}
className={classNames("button-icon-sm transparent", { active: relaySettings.write })}
onClick={() =>
configure({
write: !relaySettings.write,
read: relaySettings.read,
})
}>
<Icon name={relaySettings.write ? "check" : "close"} size={12} />
</span>
</div>
<div className="f-1">
<FormattedMessage {...messages.Read} />
<span
className="checkmark"
}
/>
<AsyncIcon
iconName="read"
iconSize={16}
className={classNames("button-icon-sm transparent", { active: relaySettings.read })}
onClick={() =>
configure({
write: relaySettings.write,
read: !relaySettings.read,
})
}>
<Icon name={relaySettings.read ? "check" : "close"} size={12} />
</span>
}
/>
<AsyncIcon
iconName="trash"
iconSize={16}
className="button-icon-sm transparent trash-icon"
onClick={() => removeRelay(login, props.addr)}
/>
<AsyncIcon
iconName="gear"
iconSize={16}
className="button-icon-sm transparent"
onClick={() => navigate(state?.id ?? "")}
/>
</div>
</div>
<div className="flex">
<div className="f-grow"></div>
<div>
<span className="icon-btn" onClick={() => navigate(state?.id ?? "")}>
<Icon name="gear" size={12} />
</span>
</div>
</div>
)}
</div>
</div>
</>

View File

@ -1,45 +1,9 @@
.favicon {
width: 21px;
height: 21px;
border-radius: 100%;
margin-right: 12px;
max-width: unset;
}
.relay-card {
display: flex;
flex-direction: row;
align-items: center;
}
.relay-settings {
margin-left: auto;
display: flex;
align-items: center;
}
.relay-settings svg:not(:last-child) {
margin-right: 12px;
}
.relay-settings svg.enabled {
.relay-active {
color: var(--highlight);
}
.relay-settings svg.disabled {
opacity: 0.3;
}
@media (max-width: 520px) {
.relay-settings svg {
width: 16px;
height: 16px;
}
}
.relay-url {
font-size: 14px;
}
@media (min-width: 520px) {
.relay-url {
font-size: 16px;
}
}

View File

@ -5,14 +5,19 @@ import { useState } from "react";
import { FullRelaySettings } from "@snort/system";
import Icon from "Icons/Icon";
const RelayFavicon = ({ url }: { url: string }) => {
export const RelayFavicon = ({ url }: { url: string }) => {
const cleanUrl = url
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://")
.replace(/\/$/, "");
const [faviconUrl, setFaviconUrl] = useState(`${cleanUrl}/favicon.ico`);
return (
<img className="favicon" src={faviconUrl} onError={() => setFaviconUrl(Nostrich)} alt={`favicon for ${url}`} />
<img
className="circle favicon"
src={faviconUrl}
onError={() => setFaviconUrl(Nostrich)}
alt={`favicon for ${url}`}
/>
);
};
@ -22,20 +27,20 @@ interface RelaysMetadataProps {
const RelaysMetadata = ({ relays }: RelaysMetadataProps) => {
return (
<div className="main-content">
<>
{relays?.map(({ url, settings }) => {
return (
<div key={url} className="card relay-card">
<div key={url} className="card flex g8">
<RelayFavicon url={url} />
<code className="relay-url f-ellipsis">{url}</code>
<div className="relay-settings">
<Icon name="read" className={settings.read ? "enabled" : "disabled"} />
<Icon name="write" className={settings.write ? "enabled" : "disabled"} />
<code className="grow f-ellipsis">{url}</code>
<div className="flex g8">
<Icon name="read" className={settings.read ? "relay-active" : "disabled"} />
<Icon name="write" className={settings.write ? "relay-active" : "disabled"} />
</div>
</div>
);
})}
</div>
</>
);
};

View File

@ -2,7 +2,7 @@ import "./RootTabs.css";
import { useState, ReactNode, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Menu, MenuItem } from "@szhsin/react-menu";
import FormattedMessage from "Element/FormattedMessage";
import { FormattedMessage } from "react-intl";
import useLogin from "Hooks/useLogin";
import Icon from "Icons/Icon";

View File

@ -0,0 +1,29 @@
.search {
flex-grow: 1;
display: flex;
background: var(--gray-superdark);
border-radius: 1000px;
}
.search input {
border: none !important;
border-radius: 0 !important;
font-size: 15px;
line-height: 21px;
padding: 9px 16px;
}
.search > svg {
margin: 9px 16px;
}
@media (max-width: 768px) {
.search {
padding: unset;
background: unset;
}
.search input {
display: none;
}
}

View File

@ -0,0 +1,159 @@
import "./SearchBox.css";
import Spinner from "../Icons/Spinner";
import Icon from "../Icons/Icon";
import { FormattedMessage, useIntl } from "react-intl";
import { fetchNip05Pubkey } from "../Nip05/Verifier";
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { NostrLink, tryParseNostrLink } from "@snort/system";
import { useLocation, useNavigate } from "react-router-dom";
import { unixNow } from "@snort/shared";
import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "../Feed/TimelineFeed";
import Note from "./Event/Note";
const MAX_RESULTS = 3;
export default function SearchBox() {
const { formatMessage } = useIntl();
const [search, setSearch] = useState("");
const [searching, setSearching] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const [activeIndex, setActiveIndex] = useState<number>(-1);
const resultListRef = useRef<HTMLDivElement | null>(null);
const options: TimelineFeedOptions = {
method: "LIMIT_UNTIL",
window: undefined,
now: unixNow(),
};
const subject: TimelineSubject = {
type: "profile_keyword",
discriminator: search,
items: [search],
relay: undefined,
streams: false,
};
const { main } = useTimelineFeed(subject, options);
useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setSearch("");
}
};
document.addEventListener("keydown", handleGlobalKeyDown);
return () => {
document.removeEventListener("keydown", handleGlobalKeyDown);
};
}, []);
useEffect(() => {
// Close the search on navigation
setSearch("");
setActiveIndex(-1);
}, [location]);
const executeSearch = async () => {
try {
setSearching(true);
const link = tryParseNostrLink(search);
if (link) {
navigate(`/${link.encode()}`);
return;
}
if (search.includes("@")) {
const [handle, domain] = search.split("@");
const pk = await fetchNip05Pubkey(handle, domain);
if (pk) {
navigate(`/${new NostrLink(CONFIG.profileLinkPrefix, pk).encode()}`);
return;
}
}
navigate(`/search/${encodeURIComponent(search)}`);
} finally {
setSearching(false);
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.value.match(/nsec1[a-zA-Z0-9]{20,65}/gi)) {
setSearch(e.target.value);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case "Enter":
if (activeIndex === 0) {
navigate(`/search/${encodeURIComponent(search)}`);
} else if (activeIndex > 0 && main) {
const selectedResult = main[activeIndex - 1];
navigate(`/${new NostrLink(CONFIG.profileLinkPrefix, selectedResult.pubkey).encode()}`);
} else {
executeSearch();
}
break;
case "ArrowDown":
e.preventDefault();
setActiveIndex(prev => Math.min(prev + 1, Math.min(MAX_RESULTS, main ? main.length : 0)));
break;
case "ArrowUp":
e.preventDefault();
setActiveIndex(prev => Math.max(prev - 1, 0));
break;
default:
break;
}
};
return (
<div className="search relative">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "Search" })}
className="w-max"
value={search}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
{searching ? (
<Spinner width={24} height={24} />
) : (
<Icon name="search" size={24} onClick={() => navigate("/search")} />
)}
{search && !searching && (
<div
className="absolute top-full mt-2 w-full border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-black shadow-lg rounded-lg z-10 overflow-hidden"
ref={resultListRef}>
<div
className={`p-2 cursor-pointer ${
activeIndex === 0
? "bg-neutral-300 dark:bg-neutral-800 hover:bg-neutral-400 dark:hover:bg-neutral-600"
: "hover:bg-neutral-200 dark:hover:bg-neutral-800"
}`}
onMouseEnter={() => setActiveIndex(0)}
onClick={() => navigate(`/search/${encodeURIComponent(search)}`, { state: { forceRefresh: true } })}>
<FormattedMessage defaultMessage="Search notes" />: <b>{search}</b>
</div>
{main?.slice(0, MAX_RESULTS).map((result, idx) => (
<div
key={idx}
className={`p-2 cursor-pointer ${
activeIndex === idx + 1
? "bg-neutral-300 dark:bg-neutral-800 hover:bg-neutral-400 dark:hover:bg-neutral-600"
: "hover:bg-neutral-200 dark:hover:bg-neutral-800"
}`}
onMouseEnter={() => setActiveIndex(idx + 1)}>
<Note data={result} depth={0} related={[]} />
</div>
))}
</div>
)}
</div>
);
}

View File

@ -1,9 +1,8 @@
import "./SendSats.css";
import React, { ReactNode, useContext, useEffect, useState } from "react";
import React, { ReactNode, useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { LNURLSuccessAction } from "@snort/shared";
import { formatShort } from "Number";
@ -48,8 +47,7 @@ export default function SendSats(props: SendSatsProps) {
const [success, setSuccess] = useState<LNURLSuccessAction>();
const [amount, setAmount] = useState<SendSatsInputSelection>();
const system = useContext(SnortContext);
const publisher = useEventPublisher();
const { publisher, system } = useEventPublisher();
const walletState = useWallet();
const wallet = walletState.wallet;
@ -103,7 +101,7 @@ export default function SendSats(props: SendSatsProps) {
function successAction() {
if (!success) return null;
return (
<div className="flex f-center">
<div className="flex items-center">
<p className="flex g12">
<Icon name="check" className="success" />
{success?.description ?? <FormattedMessage defaultMessage="Paid" />}
@ -155,7 +153,7 @@ export default function SendSats(props: SendSatsProps) {
const total = props.targets.reduce((acc, v) => (acc += v.weight), 0);
return (
<div className="flex-column g12">
<div className="flex flex-col g12">
<h2>
{zapper?.canZap() ? (
<FormattedMessage defaultMessage="Send zap splits to" />
@ -181,9 +179,9 @@ export default function SendSats(props: SendSatsProps) {
if (!(props.show ?? false)) return null;
return (
<Modal id="send-sats" className="lnurl-modal" onClose={onClose}>
<div className="p flex-column g12">
<div className="p flex flex-col g12">
<div className="flex g12">
<div className="flex f-grow">{props.title || title()}</div>
<div className="flex items-center grow">{props.title || title()}</div>
<div onClick={onClose}>
<Icon name="close" />
</div>
@ -312,7 +310,7 @@ function SendSatsInput(props: {
type="number"
min={min}
max={max}
className="f-grow"
className="grow"
placeholder={formatMessage(messages.Custom)}
value={customAmount}
onChange={e => setCustomAmount(parseInt(e.target.value))}
@ -329,8 +327,8 @@ function SendSatsInput(props: {
}
return (
<div className="flex-column g24">
<div className="flex-column g8">
<div className="flex flex-col g24">
<div className="flex flex-col g8">
<h3>
<FormattedMessage defaultMessage="Zap amount in sats" />
</h3>
@ -340,7 +338,7 @@ function SendSatsInput(props: {
<input
type="text"
placeholder={formatMessage(messages.Comment)}
className="f-grow"
className="grow"
maxLength={props.zapper.maxComment()}
onChange={e => setComment(e.target.value)}
/>
@ -348,11 +346,9 @@ function SendSatsInput(props: {
</div>
<SendSatsZapTypeSelector zapType={zapType} setZapType={setZapType} />
{(amount ?? 0) > 0 && (
<AsyncButton className="zap-action" onClick={() => props.onNextStage(getValue())}>
<div className="zap-action-container">
<Icon name="zap" />
<FormattedMessage defaultMessage="Zap {n} sats" values={{ n: formatShort(amount) }} />
</div>
<AsyncButton onClick={() => props.onNextStage(getValue())}>
<Icon name="zap" />
<FormattedMessage defaultMessage="Zap {n} sats" values={{ n: formatShort(amount) }} />
</AsyncButton>
)}
</div>
@ -367,7 +363,7 @@ function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; se
</button>
);
return (
<div className="flex-column g8">
<div className="flex flex-col g8">
<h3>
<FormattedMessage defaultMessage="Zap Type" />
</h3>
@ -391,13 +387,13 @@ function SendSatsInvoice(props: {
onInvoicePaid: () => void;
}) {
return (
<div className="flex-column g12 txt-center">
<div className="flex flex-col items-center g12 txt-center">
{props.notice && <b className="error">{props.notice}</b>}
{props.invoice.map(v => (
<>
<QrCode data={v.pr} link={`lightning:${v.pr}`} />
<div className="flex-column g12">
<Copy text={v.pr} maxSize={26} className="f-center" />
<div className="flex flex-col g12">
<Copy text={v.pr} maxSize={26} className="items-center" />
<a href={`lightning:${v.pr}`}>
<button type="button">
<FormattedMessage defaultMessage="Open Wallet" />

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