Compare commits
109 Commits
Author | SHA1 | Date |
---|---|---|
Kieran | b470d2fd14 | |
Kieran | a0ced76d46 | |
Kieran | 7a717a5c56 | |
Kieran | 06da58ac52 | |
Kieran | 41a2f06f58 | |
Kieran | dcf854ddbb | |
Kieran | a56591996b | |
Kieran | 6de9c566e7 | |
Kieran | ec4e6498d3 | |
Kieran | 2ed38d1b97 | |
artur | 1143cdfc88 | |
artur | 51aba89e45 | |
artur | ef291f0db7 | |
artur | 8aafae7d6c | |
artur | 01d6839080 | |
Kieran | 59038d118e | |
Kieran | c36a3e0bc1 | |
Kieran | 53cffb2865 | |
Kieran | 88c4ea65ef | |
Kieran | 1ddb2dc71c | |
Kieran | 51e6e0984e | |
Kieran | 241ead7a03 | |
callebtc | c52b56871c | |
callebtc | 0b6b17f4f9 | |
callebtc | 0cb006816e | |
callebtc | 701049368e | |
callebtc | 4f8f472e84 | |
Kieran | ca1bb86036 | |
Kieran | 1923273f6f | |
Kieran | 9d67da3b6f | |
Kieran | 7baa85ca11 | |
Kieran | e480e74882 | |
Kieran | 94d1e2c0f9 | |
Kieran | 32bb686405 | |
Kieran | e42325f3b5 | |
Kieran | 6fe9a27041 | |
Kieran | 551169c2c7 | |
hellodword | 190f2f467c | |
Kieran | edc6dbdb2a | |
Kieran | 2b7a74865e | |
Kieran | 5089cd63eb | |
Kieran | 805ce1d96e | |
Kieran | 48852f3099 | |
vivganes | 8c164c0340 | |
Kieran | 198b5c3858 | |
Kieran | 11b1bc81b5 | |
Kieran | 46fc4500d7 | |
Kieran | 6219626ba0 | |
Kieran | aad92ec887 | |
Kieran | 1dcf15ceeb | |
Kieran | 816aa3b838 | |
Kieran | dd46586e43 | |
Kieran | 69ec48141b | |
Kieran | ce6a4ce35c | |
Kieran | 5e5843c8d4 | |
Kieran | ab2398c2a4 | |
Kieran | e09df95adf | |
Kieran | 1e11e6a1a2 | |
Kieran | 7ca87fb4bd | |
Kieran | 5ec7c1a765 | |
Michael Rhee | 9258253f65 | |
Kieran | d47bbd5b26 | |
vivganes | 8ae21dee56 | |
Kieran | 3c27a6463b | |
Kieran | 2b4964016c | |
vivganes | d4c1f0704b | |
Kieran | 82956f3c69 | |
Kieran | a31aa35490 | |
Kieran | 45ca273064 | |
Kieran | e6daf6536e | |
Sam Samskies | f8c3889b28 | |
Kieran | 28dec3aa0e | |
Kieran | 1f0d17112c | |
Kieran | ed878f57f4 | |
Kieran | 8f504cfef9 | |
Sam Samskies | 969284e47b | |
vivganes | 56fef7b7fb | |
Kieran | a86e7e4757 | |
Sam Samskies | b068c00b7f | |
Sam Samskies | 74bfa03227 | |
Michael Rhee | be1cd923cd | |
Sam Samskies | 9b2fb3b6bf | |
Kieran | cc1d42c517 | |
Kieran | e5b215abb5 | |
Kieran | 9dacad430a | |
Kieran | 6657c84876 | |
Kieran | 64da4248b5 | |
Kieran | d5f768998c | |
Kieran | 7abb49066e | |
Kieran | 11101d4cbf | |
Kieran | 3462a488dc | |
Kieran | 13461cca80 | |
Kieran | a6eefb1027 | |
Kieran | f4244f881e | |
vivganes | a12c53820e | |
Kieran | 28383b8464 | |
Kieran | 82d2b19e36 | |
Kieran | 6d8416f88e | |
Kieran | a96a8eb11f | |
Kieran | 364637ab54 | |
Kieran | 8e2f01fdff | |
Kieran | 6fa641e33e | |
Kieran | 3d8269b674 | |
vivganes | a7ab5ffa31 | |
vivganes | de0c18a9cf | |
ennmichael | 1e5811b117 | |
ennmichael | d754551ebb | |
ennmichael | 3ffe4d5b19 | |
ennmichael | 31b0538337 |
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "@snort/app",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@cashu/cashu-ts": "^0.6.1",
|
||||
"@jukben/emoji-search": "^2.0.1",
|
||||
"@lightninglabs/lnc-web": "^0.2.3-alpha",
|
||||
"@noble/hashes": "^1.2.0",
|
||||
|
@ -20,6 +21,7 @@
|
|||
"bech32": "^2.0.0",
|
||||
"dexie": "^3.2.2",
|
||||
"dexie-react-hooks": "^1.1.1",
|
||||
"dns-over-http-resolver": "^2.1.1",
|
||||
"events": "^3.3.0",
|
||||
"light-bolt11-decoder": "^2.1.0",
|
||||
"qr-code-styling": "^1.6.0-rc.1",
|
||||
|
@ -67,9 +69,7 @@
|
|||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
">0.5%"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
|
|
|
@ -1,407 +1,156 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<symbol id="arrowBack" viewBox="0 0 16 13">
|
||||
<path
|
||||
d="M14.6667 6.5H1.33334M1.33334 6.5L6.33334 11.5M1.33334 6.5L6.33334 1.5"
|
||||
stroke="currentColor"
|
||||
stroke-Width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M14.6667 6.5H1.33334M1.33334 6.5L6.33334 11.5M1.33334 6.5L6.33334 1.5" stroke="currentColor" stroke-Width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="arrowFront" viewBox="0 0 8 14" fill="none">
|
||||
<path d="M1 13L7 7L1 1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="arrowUp" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M5.99992 10.6673V1.33398M5.99992 1.33398L1.33325 6.00065M5.99992 1.33398L10.6666 6.00065"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M5.99992 10.6673V1.33398M5.99992 1.33398L1.33325 6.00065M5.99992 1.33398L10.6666 6.00065" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="attachment" viewBox="0 0 21 22" fill="none">
|
||||
<path
|
||||
d="M19.1525 9.89945L10.1369 18.9151C8.08662 20.9653 4.7625 20.9653 2.71225 18.9151C0.661997 16.8648 0.661998 13.5407 2.71225 11.4904L11.7279 2.47483C13.0947 1.108 15.3108 1.108 16.6776 2.47483C18.0444 3.84167 18.0444 6.05775 16.6776 7.42458L8.01555 16.0866C7.33213 16.7701 6.22409 16.7701 5.54068 16.0866C4.85726 15.4032 4.85726 14.2952 5.54068 13.6118L13.1421 6.01037"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M19.1525 9.89945L10.1369 18.9151C8.08662 20.9653 4.7625 20.9653 2.71225 18.9151C0.661997 16.8648 0.661998 13.5407 2.71225 11.4904L11.7279 2.47483C13.0947 1.108 15.3108 1.108 16.6776 2.47483C18.0444 3.84167 18.0444 6.05775 16.6776 7.42458L8.01555 16.0866C7.33213 16.7701 6.22409 16.7701 5.54068 16.0866C4.85726 15.4032 4.85726 14.2952 5.54068 13.6118L13.1421 6.01037" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="badge" viewBox="0 0 16 15" fill="none">
|
||||
<path
|
||||
d="M6.00004 7.50065L7.33337 8.83398L10.3334 5.83398M11.9342 2.83299C12.0714 3.16501 12.3349 3.42892 12.6667 3.5667L13.8302 4.04864C14.1622 4.18617 14.426 4.44998 14.5636 4.78202C14.7011 5.11407 14.7011 5.48715 14.5636 5.81919L14.082 6.98185C13.9444 7.31404 13.9442 7.6875 14.0824 8.01953L14.5632 9.18185C14.6313 9.34631 14.6664 9.52259 14.6665 9.70062C14.6665 9.87865 14.6315 10.0549 14.5633 10.2194C14.4952 10.3839 14.3953 10.5333 14.2694 10.6592C14.1435 10.7851 13.9941 10.8849 13.8296 10.953L12.6669 11.4346C12.3349 11.5718 12.071 11.8354 11.9333 12.1672L11.4513 13.3307C11.3138 13.6627 11.05 13.9265 10.718 14.0641C10.3859 14.2016 10.0129 14.2016 9.68085 14.0641L8.51823 13.5825C8.18619 13.4453 7.81326 13.4455 7.48143 13.5832L6.31797 14.0645C5.98612 14.2017 5.61338 14.2016 5.28162 14.0642C4.94986 13.9267 4.68621 13.6632 4.54858 13.3316L4.06652 12.1677C3.92924 11.8357 3.66574 11.5718 3.33394 11.434L2.17048 10.9521C1.8386 10.8146 1.57488 10.5509 1.4373 10.2191C1.29971 9.88724 1.29953 9.51434 1.43678 9.18235L1.91835 8.01968C2.05554 7.68763 2.05526 7.31469 1.91757 6.98284L1.43669 5.81851C1.36851 5.65405 1.3334 5.47777 1.33337 5.29974C1.33335 5.12171 1.3684 4.94542 1.43652 4.78094C1.50465 4.61646 1.60452 4.46702 1.73042 4.34115C1.85632 4.21529 2.00579 4.11546 2.17028 4.04739L3.33291 3.5658C3.66462 3.42863 3.92836 3.16545 4.06624 2.83402L4.54816 1.67052C4.68569 1.33848 4.94949 1.07467 5.28152 0.937137C5.61355 0.7996 5.98662 0.7996 6.31865 0.937137L7.48127 1.41873C7.81331 1.55593 8.18624 1.55565 8.51808 1.41795L9.68202 0.937884C10.014 0.800424 10.387 0.800452 10.719 0.937962C11.0509 1.07547 11.3147 1.3392 11.4522 1.67116L11.9343 2.835L11.9342 2.83299Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M6.00004 7.50065L7.33337 8.83398L10.3334 5.83398M11.9342 2.83299C12.0714 3.16501 12.3349 3.42892 12.6667 3.5667L13.8302 4.04864C14.1622 4.18617 14.426 4.44998 14.5636 4.78202C14.7011 5.11407 14.7011 5.48715 14.5636 5.81919L14.082 6.98185C13.9444 7.31404 13.9442 7.6875 14.0824 8.01953L14.5632 9.18185C14.6313 9.34631 14.6664 9.52259 14.6665 9.70062C14.6665 9.87865 14.6315 10.0549 14.5633 10.2194C14.4952 10.3839 14.3953 10.5333 14.2694 10.6592C14.1435 10.7851 13.9941 10.8849 13.8296 10.953L12.6669 11.4346C12.3349 11.5718 12.071 11.8354 11.9333 12.1672L11.4513 13.3307C11.3138 13.6627 11.05 13.9265 10.718 14.0641C10.3859 14.2016 10.0129 14.2016 9.68085 14.0641L8.51823 13.5825C8.18619 13.4453 7.81326 13.4455 7.48143 13.5832L6.31797 14.0645C5.98612 14.2017 5.61338 14.2016 5.28162 14.0642C4.94986 13.9267 4.68621 13.6632 4.54858 13.3316L4.06652 12.1677C3.92924 11.8357 3.66574 11.5718 3.33394 11.434L2.17048 10.9521C1.8386 10.8146 1.57488 10.5509 1.4373 10.2191C1.29971 9.88724 1.29953 9.51434 1.43678 9.18235L1.91835 8.01968C2.05554 7.68763 2.05526 7.31469 1.91757 6.98284L1.43669 5.81851C1.36851 5.65405 1.3334 5.47777 1.33337 5.29974C1.33335 5.12171 1.3684 4.94542 1.43652 4.78094C1.50465 4.61646 1.60452 4.46702 1.73042 4.34115C1.85632 4.21529 2.00579 4.11546 2.17028 4.04739L3.33291 3.5658C3.66462 3.42863 3.92836 3.16545 4.06624 2.83402L4.54816 1.67052C4.68569 1.33848 4.94949 1.07467 5.28152 0.937137C5.61355 0.7996 5.98662 0.7996 6.31865 0.937137L7.48127 1.41873C7.81331 1.55593 8.18624 1.55565 8.51808 1.41795L9.68202 0.937884C10.014 0.800424 10.387 0.800452 10.719 0.937962C11.0509 1.07547 11.3147 1.3392 11.4522 1.67116L11.9343 2.835L11.9342 2.83299Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="bell" viewBox="0 0 20 23" fill="none">
|
||||
<path
|
||||
d="M7.35419 20.5C8.05933 21.1224 8.98557 21.5 10 21.5C11.0145 21.5 11.9407 21.1224 12.6458 20.5M16 7.5C16 5.9087 15.3679 4.38258 14.2427 3.25736C13.1174 2.13214 11.5913 1.5 10 1.5C8.40872 1.5 6.8826 2.13214 5.75738 3.25736C4.63216 4.38258 4.00002 5.9087 4.00002 7.5C4.00002 10.5902 3.22049 12.706 2.34968 14.1054C1.61515 15.2859 1.24788 15.8761 1.26134 16.0408C1.27626 16.2231 1.31488 16.2926 1.46179 16.4016C1.59448 16.5 2.19261 16.5 3.38887 16.5H16.6112C17.8074 16.5 18.4056 16.5 18.5382 16.4016C18.6852 16.2926 18.7238 16.2231 18.7387 16.0408C18.7522 15.8761 18.3849 15.2859 17.6504 14.1054C16.7795 12.706 16 10.5902 16 7.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M7.35419 20.5C8.05933 21.1224 8.98557 21.5 10 21.5C11.0145 21.5 11.9407 21.1224 12.6458 20.5M16 7.5C16 5.9087 15.3679 4.38258 14.2427 3.25736C13.1174 2.13214 11.5913 1.5 10 1.5C8.40872 1.5 6.8826 2.13214 5.75738 3.25736C4.63216 4.38258 4.00002 5.9087 4.00002 7.5C4.00002 10.5902 3.22049 12.706 2.34968 14.1054C1.61515 15.2859 1.24788 15.8761 1.26134 16.0408C1.27626 16.2231 1.31488 16.2926 1.46179 16.4016C1.59448 16.5 2.19261 16.5 3.38887 16.5H16.6112C17.8074 16.5 18.4056 16.5 18.5382 16.4016C18.6852 16.2926 18.7238 16.2231 18.7387 16.0408C18.7522 15.8761 18.3849 15.2859 17.6504 14.1054C16.7795 12.706 16 10.5902 16 7.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="block" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M4.10829 4.10768L15.8916 15.891M18.3333 9.99935C18.3333 14.6017 14.6023 18.3327 9.99996 18.3327C5.39759 18.3327 1.66663 14.6017 1.66663 9.99935C1.66663 5.39698 5.39759 1.66602 9.99996 1.66602C14.6023 1.66602 18.3333 5.39698 18.3333 9.99935Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M4.10829 4.10768L15.8916 15.891M18.3333 9.99935C18.3333 14.6017 14.6023 18.3327 9.99996 18.3327C5.39759 18.3327 1.66663 14.6017 1.66663 9.99935C1.66663 5.39698 5.39759 1.66602 9.99996 1.66602C14.6023 1.66602 18.3333 5.39698 18.3333 9.99935Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
<symbol id="close" viewBox="0 0 8 8" fill="none">
|
||||
<path
|
||||
d="M7.33332 0.666992L0.666656 7.33366M0.666656 0.666992L7.33332 7.33366"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M7.33332 0.666992L0.666656 7.33366M0.666656 0.666992L7.33332 7.33366" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="zap" viewBox="0 0 16 20" fill="none">
|
||||
<path
|
||||
d="M8.8333 1.70166L1.41118 10.6082C1.12051 10.957 0.975169 11.1314 0.972948 11.2787C0.971017 11.4068 1.02808 11.5286 1.12768 11.6091C1.24226 11.7017 1.46928 11.7017 1.92333 11.7017H7.99997L7.16663 18.3683L14.5888 9.46178C14.8794 9.11297 15.0248 8.93857 15.027 8.79128C15.0289 8.66323 14.9719 8.54141 14.8723 8.46092C14.7577 8.36833 14.5307 8.36833 14.0766 8.36833H7.99997L8.8333 1.70166Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M8.8333 1.70166L1.41118 10.6082C1.12051 10.957 0.975169 11.1314 0.972948 11.2787C0.971017 11.4068 1.02808 11.5286 1.12768 11.6091C1.24226 11.7017 1.46928 11.7017 1.92333 11.7017H7.99997L7.16663 18.3683L14.5888 9.46178C14.8794 9.11297 15.0248 8.93857 15.027 8.79128C15.0289 8.66323 14.9719 8.54141 14.8723 8.46092C14.7577 8.36833 14.5307 8.36833 14.0766 8.36833H7.99997L8.8333 1.70166Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="zapFast" viewBox="0 0 23 20" fill="none">
|
||||
<path
|
||||
d="M8 15.5H2.5M5.5 10H1M8 4.5H3M16 1L9.40357 10.235C9.1116 10.6438 8.96562 10.8481 8.97194 11.0185C8.97744 11.1669 9.04858 11.3051 9.1661 11.3958C9.30108 11.5 9.55224 11.5 10.0546 11.5H15L14 19L20.5964 9.76499C20.8884 9.35624 21.0344 9.15187 21.0281 8.98147C21.0226 8.83312 20.9514 8.69489 20.8339 8.60418C20.6989 8.5 20.4478 8.5 19.9454 8.5H15L16 1Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M8 15.5H2.5M5.5 10H1M8 4.5H3M16 1L9.40357 10.235C9.1116 10.6438 8.96562 10.8481 8.97194 11.0185C8.97744 11.1669 9.04858 11.3051 9.1661 11.3958C9.30108 11.5 9.55224 11.5 10.0546 11.5H15L14 19L20.5964 9.76499C20.8884 9.35624 21.0344 9.15187 21.0281 8.98147C21.0226 8.83312 20.9514 8.69489 20.8339 8.60418C20.6989 8.5 20.4478 8.5 19.9454 8.5H15L16 1Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="zapCircle" viewBox="0 0 33 32" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.5 1.33301C8.39986 1.33301 1.83337 7.8995 1.83337 15.9997C1.83337 24.0999 8.39986 30.6663 16.5 30.6663C24.6002 30.6663 31.1667 24.0999 31.1667 15.9997C31.1667 7.8995 24.6002 1.33301 16.5 1.33301ZM10.3155 16.3287L16.5 7.33301V13.9997H21.8056C22.4627 13.9997 22.7913 13.9997 22.9705 14.1364C23.1265 14.2555 23.2221 14.4372 23.2318 14.6333C23.243 14.8583 23.0569 15.1291 22.6845 15.6706L16.5 24.6663V17.9997H11.1944C10.5373 17.9997 10.2087 17.9997 10.0295 17.863C9.87353 17.7439 9.77791 17.5621 9.76818 17.3661C9.75699 17.141 9.94315 16.8702 10.3155 16.3287Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.21"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5 1.33301C8.39986 1.33301 1.83337 7.8995 1.83337 15.9997C1.83337 24.0999 8.39986 30.6663 16.5 30.6663C24.6002 30.6663 31.1667 24.0999 31.1667 15.9997C31.1667 7.8995 24.6002 1.33301 16.5 1.33301ZM10.3155 16.3287L16.5 7.33301V13.9997H21.8056C22.4627 13.9997 22.7913 13.9997 22.9705 14.1364C23.1265 14.2555 23.2221 14.4372 23.2318 14.6333C23.243 14.8583 23.0569 15.1291 22.6845 15.6706L16.5 24.6663V17.9997H11.1944C10.5373 17.9997 10.2087 17.9997 10.0295 17.863C9.87353 17.7439 9.77791 17.5621 9.76818 17.3661C9.75699 17.141 9.94315 16.8702 10.3155 16.3287Z" fill="currentColor" fill-opacity="0.21" />
|
||||
</symbol>
|
||||
<symbol id="search" viewBox="0 0 20 21" fill="none">
|
||||
<path
|
||||
d="M19 19.5L14.65 15.15M17 9.5C17 13.9183 13.4183 17.5 9 17.5C4.58172 17.5 1 13.9183 1 9.5C1 5.08172 4.58172 1.5 9 1.5C13.4183 1.5 17 5.08172 17 9.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M19 19.5L14.65 15.15M17 9.5C17 13.9183 13.4183 17.5 9 17.5C4.58172 17.5 1 13.9183 1 9.5C1 5.08172 4.58172 1.5 9 1.5C13.4183 1.5 17 5.08172 17 9.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="envelope" viewBox="0 0 22 19" fill="none">
|
||||
<path
|
||||
d="M1 4.5L9.16492 10.2154C9.82609 10.6783 10.1567 10.9097 10.5163 10.9993C10.8339 11.0785 11.1661 11.0785 11.4837 10.9993C11.8433 10.9097 12.1739 10.6783 12.8351 10.2154L21 4.5M5.8 17.5H16.2C17.8802 17.5 18.7202 17.5 19.362 17.173C19.9265 16.8854 20.3854 16.4265 20.673 15.862C21 15.2202 21 14.3802 21 12.7V6.3C21 4.61984 21 3.77976 20.673 3.13803C20.3854 2.57354 19.9265 2.1146 19.362 1.82698C18.7202 1.5 17.8802 1.5 16.2 1.5H5.8C4.11984 1.5 3.27976 1.5 2.63803 1.82698C2.07354 2.1146 1.6146 2.57354 1.32698 3.13803C1 3.77976 1 4.61984 1 6.3V12.7C1 14.3802 1 15.2202 1.32698 15.862C1.6146 16.4265 2.07354 16.8854 2.63803 17.173C3.27976 17.5 4.11984 17.5 5.8 17.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M1 4.5L9.16492 10.2154C9.82609 10.6783 10.1567 10.9097 10.5163 10.9993C10.8339 11.0785 11.1661 11.0785 11.4837 10.9993C11.8433 10.9097 12.1739 10.6783 12.8351 10.2154L21 4.5M5.8 17.5H16.2C17.8802 17.5 18.7202 17.5 19.362 17.173C19.9265 16.8854 20.3854 16.4265 20.673 15.862C21 15.2202 21 14.3802 21 12.7V6.3C21 4.61984 21 3.77976 20.673 3.13803C20.3854 2.57354 19.9265 2.1146 19.362 1.82698C18.7202 1.5 17.8802 1.5 16.2 1.5H5.8C4.11984 1.5 3.27976 1.5 2.63803 1.82698C2.07354 2.1146 1.6146 2.57354 1.32698 3.13803C1 3.77976 1 4.61984 1 6.3V12.7C1 14.3802 1 15.2202 1.32698 15.862C1.6146 16.4265 2.07354 16.8854 2.63803 17.173C3.27976 17.5 4.11984 17.5 5.8 17.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="mute" viewBox="0 0 19 18" fill="none">
|
||||
<path
|
||||
d="M13.75 12.3333L17.9166 16.5M17.9166 12.3333L13.75 16.5M9.99996 11.9167H6.24996C5.08699 11.9167 4.5055 11.9167 4.03234 12.0602C2.96701 12.3834 2.13333 13.217 1.81016 14.2824C1.66663 14.7555 1.66663 15.337 1.66663 16.5M12.0833 5.25C12.0833 7.32107 10.4044 9 8.33329 9C6.26222 9 4.58329 7.32107 4.58329 5.25C4.58329 3.17893 6.26222 1.5 8.33329 1.5C10.4044 1.5 12.0833 3.17893 12.0833 5.25Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M13.75 12.3333L17.9166 16.5M17.9166 12.3333L13.75 16.5M9.99996 11.9167H6.24996C5.08699 11.9167 4.5055 11.9167 4.03234 12.0602C2.96701 12.3834 2.13333 13.217 1.81016 14.2824C1.66663 14.7555 1.66663 15.337 1.66663 16.5M12.0833 5.25C12.0833 7.32107 10.4044 9 8.33329 9C6.26222 9 4.58329 7.32107 4.58329 5.25C4.58329 3.17893 6.26222 1.5 8.33329 1.5C10.4044 1.5 12.0833 3.17893 12.0833 5.25Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="copy" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M13.3333 13.3327V15.666C13.3333 16.5994 13.3333 17.0661 13.1516 17.4227C12.9918 17.7363 12.7369 17.9912 12.4233 18.151C12.0668 18.3327 11.6 18.3327 10.6666 18.3327H4.33329C3.39987 18.3327 2.93316 18.3327 2.57664 18.151C2.26304 17.9912 2.00807 17.7363 1.84828 17.4227C1.66663 17.0661 1.66663 16.5994 1.66663 15.666V9.33268C1.66663 8.39926 1.66663 7.93255 1.84828 7.57603C2.00807 7.26243 2.26304 7.00746 2.57664 6.84767C2.93316 6.66602 3.39987 6.66602 4.33329 6.66602H6.66663M9.33329 13.3327H15.6666C16.6 13.3327 17.0668 13.3327 17.4233 13.151C17.7369 12.9912 17.9918 12.7363 18.1516 12.4227C18.3333 12.0661 18.3333 11.5994 18.3333 10.666V4.33268C18.3333 3.39926 18.3333 2.93255 18.1516 2.57603C17.9918 2.26243 17.7369 2.00746 17.4233 1.84767C17.0668 1.66602 16.6 1.66602 15.6666 1.66602H9.33329C8.39987 1.66602 7.93316 1.66602 7.57664 1.84767C7.26304 2.00746 7.00807 2.26243 6.84828 2.57603C6.66663 2.93255 6.66663 3.39926 6.66663 4.33268V10.666C6.66663 11.5994 6.66663 12.0661 6.84828 12.4227C7.00807 12.7363 7.26304 12.9912 7.57664 13.151C7.93316 13.3327 8.39987 13.3327 9.33329 13.3327Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M13.3333 13.3327V15.666C13.3333 16.5994 13.3333 17.0661 13.1516 17.4227C12.9918 17.7363 12.7369 17.9912 12.4233 18.151C12.0668 18.3327 11.6 18.3327 10.6666 18.3327H4.33329C3.39987 18.3327 2.93316 18.3327 2.57664 18.151C2.26304 17.9912 2.00807 17.7363 1.84828 17.4227C1.66663 17.0661 1.66663 16.5994 1.66663 15.666V9.33268C1.66663 8.39926 1.66663 7.93255 1.84828 7.57603C2.00807 7.26243 2.26304 7.00746 2.57664 6.84767C2.93316 6.66602 3.39987 6.66602 4.33329 6.66602H6.66663M9.33329 13.3327H15.6666C16.6 13.3327 17.0668 13.3327 17.4233 13.151C17.7369 12.9912 17.9918 12.7363 18.1516 12.4227C18.3333 12.0661 18.3333 11.5994 18.3333 10.666V4.33268C18.3333 3.39926 18.3333 2.93255 18.1516 2.57603C17.9918 2.26243 17.7369 2.00746 17.4233 1.84767C17.0668 1.66602 16.6 1.66602 15.6666 1.66602H9.33329C8.39987 1.66602 7.93316 1.66602 7.57664 1.84767C7.26304 2.00746 7.00807 2.26243 6.84828 2.57603C6.66663 2.93255 6.66663 3.39926 6.66663 4.33268V10.666C6.66663 11.5994 6.66663 12.0661 6.84828 12.4227C7.00807 12.7363 7.26304 12.9912 7.57664 13.151C7.93316 13.3327 8.39987 13.3327 9.33329 13.3327Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="dislike" viewBox="0 0 19 20" fill="none">
|
||||
<path
|
||||
d="M13.1667 1.66667V10.8333M17.3333 8.16667V4.33334C17.3333 3.39992 17.3333 2.93321 17.1517 2.57669C16.9919 2.26308 16.7369 2.00812 16.4233 1.84833C16.0668 1.66667 15.6001 1.66667 14.6667 1.66667H5.76501C4.54711 1.66667 3.93816 1.66667 3.44632 1.88953C3.01284 2.08595 2.64442 2.40202 2.38437 2.8006C2.08931 3.25283 1.99672 3.8547 1.81153 5.05844L1.37563 7.89178C1.13137 9.47943 1.00925 10.2733 1.24484 10.8909C1.45162 11.4331 1.84054 11.8864 2.34494 12.1732C2.91961 12.5 3.72278 12.5 5.32912 12.5H6C6.46671 12.5 6.70007 12.5 6.87833 12.5908C7.03513 12.6707 7.16261 12.7982 7.24251 12.955C7.33334 13.1333 7.33334 13.3666 7.33334 13.8333V16.2785C7.33334 17.4133 8.25333 18.3333 9.3882 18.3333C9.65889 18.3333 9.90419 18.1739 10.0141 17.9266L12.8148 11.6252C12.9421 11.3385 13.0058 11.1952 13.1065 11.0902C13.1955 10.9973 13.3048 10.9263 13.4258 10.8827C13.5627 10.8333 13.7195 10.8333 14.0332 10.8333H14.6667C15.6001 10.8333 16.0668 10.8333 16.4233 10.6517C16.7369 10.4919 16.9919 10.2369 17.1517 9.92332C17.3333 9.5668 17.3333 9.10009 17.3333 8.16667Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M13.1667 1.66667V10.8333M17.3333 8.16667V4.33334C17.3333 3.39992 17.3333 2.93321 17.1517 2.57669C16.9919 2.26308 16.7369 2.00812 16.4233 1.84833C16.0668 1.66667 15.6001 1.66667 14.6667 1.66667H5.76501C4.54711 1.66667 3.93816 1.66667 3.44632 1.88953C3.01284 2.08595 2.64442 2.40202 2.38437 2.8006C2.08931 3.25283 1.99672 3.8547 1.81153 5.05844L1.37563 7.89178C1.13137 9.47943 1.00925 10.2733 1.24484 10.8909C1.45162 11.4331 1.84054 11.8864 2.34494 12.1732C2.91961 12.5 3.72278 12.5 5.32912 12.5H6C6.46671 12.5 6.70007 12.5 6.87833 12.5908C7.03513 12.6707 7.16261 12.7982 7.24251 12.955C7.33334 13.1333 7.33334 13.3666 7.33334 13.8333V16.2785C7.33334 17.4133 8.25333 18.3333 9.3882 18.3333C9.65889 18.3333 9.90419 18.1739 10.0141 17.9266L12.8148 11.6252C12.9421 11.3385 13.0058 11.1952 13.1065 11.0902C13.1955 10.9973 13.3048 10.9263 13.4258 10.8827C13.5627 10.8333 13.7195 10.8333 14.0332 10.8333H14.6667C15.6001 10.8333 16.0668 10.8333 16.4233 10.6517C16.7369 10.4919 16.9919 10.2369 17.1517 9.92332C17.3333 9.5668 17.3333 9.10009 17.3333 8.16667Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="dots" viewBox="0 0 4 16" fill="none">
|
||||
<path
|
||||
d="M1.99996 8.86865C2.4602 8.86865 2.83329 8.49556 2.83329 8.03532C2.83329 7.57508 2.4602 7.20199 1.99996 7.20199C1.53972 7.20199 1.16663 7.57508 1.16663 8.03532C1.16663 8.49556 1.53972 8.86865 1.99996 8.86865Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M1.99996 3.03532C2.4602 3.03532 2.83329 2.66222 2.83329 2.20199C2.83329 1.74175 2.4602 1.36865 1.99996 1.36865C1.53972 1.36865 1.16663 1.74175 1.16663 2.20199C1.16663 2.66222 1.53972 3.03532 1.99996 3.03532Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M1.99996 14.702C2.4602 14.702 2.83329 14.3289 2.83329 13.8687C2.83329 13.4084 2.4602 13.0353 1.99996 13.0353C1.53972 13.0353 1.16663 13.4084 1.16663 13.8687C1.16663 14.3289 1.53972 14.702 1.99996 14.702Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M1.99996 8.86865C2.4602 8.86865 2.83329 8.49556 2.83329 8.03532C2.83329 7.57508 2.4602 7.20199 1.99996 7.20199C1.53972 7.20199 1.16663 7.57508 1.16663 8.03532C1.16663 8.49556 1.53972 8.86865 1.99996 8.86865Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M1.99996 3.03532C2.4602 3.03532 2.83329 2.66222 2.83329 2.20199C2.83329 1.74175 2.4602 1.36865 1.99996 1.36865C1.53972 1.36865 1.16663 1.74175 1.16663 2.20199C1.16663 2.66222 1.53972 3.03532 1.99996 3.03532Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M1.99996 14.702C2.4602 14.702 2.83329 14.3289 2.83329 13.8687C2.83329 13.4084 2.4602 13.0353 1.99996 13.0353C1.53972 13.0353 1.16663 13.4084 1.16663 13.8687C1.16663 14.3289 1.53972 14.702 1.99996 14.702Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="gear" viewBox="0 0 20 22" fill="none">
|
||||
<path
|
||||
d="M7.39504 18.3711L7.97949 19.6856C8.15323 20.0768 8.43676 20.4093 8.79571 20.6426C9.15466 20.8759 9.5736 21.0001 10.0017 21C10.4298 21.0001 10.8488 20.8759 11.2077 20.6426C11.5667 20.4093 11.8502 20.0768 12.0239 19.6856L12.6084 18.3711C12.8164 17.9047 13.1664 17.5159 13.6084 17.26C14.0532 17.0034 14.5677 16.8941 15.0784 16.9478L16.5084 17.1C16.934 17.145 17.3636 17.0656 17.7451 16.8713C18.1265 16.6771 18.4434 16.3763 18.6573 16.0056C18.8714 15.635 18.9735 15.2103 18.951 14.7829C18.9285 14.3555 18.7825 13.9438 18.5306 13.5978L17.6839 12.4344C17.3825 12.0171 17.2214 11.5148 17.2239 11C17.2238 10.4866 17.3864 9.98635 17.6884 9.57111L18.535 8.40778C18.7869 8.06175 18.933 7.65007 18.9554 7.22267C18.9779 6.79528 18.8759 6.37054 18.6617 6C18.4478 5.62923 18.1309 5.32849 17.7495 5.13423C17.3681 4.93997 16.9385 4.86053 16.5128 4.90556L15.0828 5.05778C14.5722 5.11141 14.0576 5.00212 13.6128 4.74556C13.1699 4.48825 12.8199 4.09736 12.6128 3.62889L12.0239 2.31444C11.8502 1.92317 11.5667 1.59072 11.2077 1.3574C10.8488 1.12408 10.4298 0.99993 10.0017 1C9.5736 0.99993 9.15466 1.12408 8.79571 1.3574C8.43676 1.59072 8.15323 1.92317 7.97949 2.31444L7.39504 3.62889C7.18797 4.09736 6.83792 4.48825 6.39504 4.74556C5.95026 5.00212 5.43571 5.11141 4.92504 5.05778L3.4906 4.90556C3.06493 4.86053 2.63534 4.93997 2.25391 5.13423C1.87249 5.32849 1.55561 5.62923 1.34171 6C1.12753 6.37054 1.02549 6.79528 1.04798 7.22267C1.07046 7.65007 1.2165 8.06175 1.46838 8.40778L2.31504 9.57111C2.61698 9.98635 2.77958 10.4866 2.77949 11C2.77958 11.5134 2.61698 12.0137 2.31504 12.4289L1.46838 13.5922C1.2165 13.9382 1.07046 14.3499 1.04798 14.7773C1.02549 15.2047 1.12753 15.6295 1.34171 16C1.55582 16.3706 1.87274 16.6712 2.25411 16.8654C2.63548 17.0596 3.06496 17.1392 3.4906 17.0944L4.9206 16.9422C5.43127 16.8886 5.94581 16.9979 6.3906 17.2544C6.83513 17.511 7.18681 17.902 7.39504 18.3711Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.99992 14C11.6568 14 12.9999 12.6569 12.9999 11C12.9999 9.34315 11.6568 8 9.99992 8C8.34307 8 6.99992 9.34315 6.99992 11C6.99992 12.6569 8.34307 14 9.99992 14Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M7.39504 18.3711L7.97949 19.6856C8.15323 20.0768 8.43676 20.4093 8.79571 20.6426C9.15466 20.8759 9.5736 21.0001 10.0017 21C10.4298 21.0001 10.8488 20.8759 11.2077 20.6426C11.5667 20.4093 11.8502 20.0768 12.0239 19.6856L12.6084 18.3711C12.8164 17.9047 13.1664 17.5159 13.6084 17.26C14.0532 17.0034 14.5677 16.8941 15.0784 16.9478L16.5084 17.1C16.934 17.145 17.3636 17.0656 17.7451 16.8713C18.1265 16.6771 18.4434 16.3763 18.6573 16.0056C18.8714 15.635 18.9735 15.2103 18.951 14.7829C18.9285 14.3555 18.7825 13.9438 18.5306 13.5978L17.6839 12.4344C17.3825 12.0171 17.2214 11.5148 17.2239 11C17.2238 10.4866 17.3864 9.98635 17.6884 9.57111L18.535 8.40778C18.7869 8.06175 18.933 7.65007 18.9554 7.22267C18.9779 6.79528 18.8759 6.37054 18.6617 6C18.4478 5.62923 18.1309 5.32849 17.7495 5.13423C17.3681 4.93997 16.9385 4.86053 16.5128 4.90556L15.0828 5.05778C14.5722 5.11141 14.0576 5.00212 13.6128 4.74556C13.1699 4.48825 12.8199 4.09736 12.6128 3.62889L12.0239 2.31444C11.8502 1.92317 11.5667 1.59072 11.2077 1.3574C10.8488 1.12408 10.4298 0.99993 10.0017 1C9.5736 0.99993 9.15466 1.12408 8.79571 1.3574C8.43676 1.59072 8.15323 1.92317 7.97949 2.31444L7.39504 3.62889C7.18797 4.09736 6.83792 4.48825 6.39504 4.74556C5.95026 5.00212 5.43571 5.11141 4.92504 5.05778L3.4906 4.90556C3.06493 4.86053 2.63534 4.93997 2.25391 5.13423C1.87249 5.32849 1.55561 5.62923 1.34171 6C1.12753 6.37054 1.02549 6.79528 1.04798 7.22267C1.07046 7.65007 1.2165 8.06175 1.46838 8.40778L2.31504 9.57111C2.61698 9.98635 2.77958 10.4866 2.77949 11C2.77958 11.5134 2.61698 12.0137 2.31504 12.4289L1.46838 13.5922C1.2165 13.9382 1.07046 14.3499 1.04798 14.7773C1.02549 15.2047 1.12753 15.6295 1.34171 16C1.55582 16.3706 1.87274 16.6712 2.25411 16.8654C2.63548 17.0596 3.06496 17.1392 3.4906 17.0944L4.9206 16.9422C5.43127 16.8886 5.94581 16.9979 6.3906 17.2544C6.83513 17.511 7.18681 17.902 7.39504 18.3711Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9.99992 14C11.6568 14 12.9999 12.6569 12.9999 11C12.9999 9.34315 11.6568 8 9.99992 8C8.34307 8 6.99992 9.34315 6.99992 11C6.99992 12.6569 8.34307 14 9.99992 14Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="heart" viewBox="0 0 20 18" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.99425 3.315C8.32813 1.36716 5.54975 0.843192 3.4622 2.62683C1.37466 4.41048 1.08077 7.39264 2.72012 9.50216C4.08314 11.2561 8.2081 14.9552 9.56004 16.1525C9.7113 16.2865 9.78692 16.3534 9.87514 16.3798C9.95213 16.4027 10.0364 16.4027 10.1134 16.3798C10.2016 16.3534 10.2772 16.2865 10.4285 16.1525C11.7804 14.9552 15.9054 11.2561 17.2684 9.50216C18.9077 7.39264 18.6497 4.39171 16.5263 2.62683C14.4029 0.861954 11.6604 1.36716 9.99425 3.315Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99425 3.315C8.32813 1.36716 5.54975 0.843192 3.4622 2.62683C1.37466 4.41048 1.08077 7.39264 2.72012 9.50216C4.08314 11.2561 8.2081 14.9552 9.56004 16.1525C9.7113 16.2865 9.78692 16.3534 9.87514 16.3798C9.95213 16.4027 10.0364 16.4027 10.1134 16.3798C10.2016 16.3534 10.2772 16.2865 10.4285 16.1525C11.7804 14.9552 15.9054 11.2561 17.2684 9.50216C18.9077 7.39264 18.6497 4.39171 16.5263 2.62683C14.4029 0.861954 11.6604 1.36716 9.99425 3.315Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="json" viewBox="0 0 22 18" fill="none">
|
||||
<path
|
||||
d="M17.5708 17C18.8328 17 19.8568 15.977 19.8568 14.714V10.143L20.9998 9L19.8568 7.857V3.286C19.8568 2.023 18.8338 1 17.5708 1M4.429 1C3.166 1 2.143 2.023 2.143 3.286V7.857L1 9L2.143 10.143V14.714C2.143 15.977 3.166 17 4.429 17"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M17.5708 17C18.8328 17 19.8568 15.977 19.8568 14.714V10.143L20.9998 9L19.8568 7.857V3.286C19.8568 2.023 18.8338 1 17.5708 1M4.429 1C3.166 1 2.143 2.023 2.143 3.286V7.857L1 9L2.143 10.143V14.714C2.143 15.977 3.166 17 4.429 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="link" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M8.99996 12C9.42941 12.5742 9.97731 13.0492 10.6065 13.393C11.2357 13.7367 11.9315 13.9411 12.6466 13.9924C13.3617 14.0436 14.0795 13.9404 14.7513 13.6898C15.4231 13.4392 16.0331 13.0471 16.54 12.54L19.54 9.54003C20.4507 8.59702 20.9547 7.334 20.9433 6.02302C20.9319 4.71204 20.4061 3.45797 19.479 2.53093C18.552 1.60389 17.2979 1.07805 15.987 1.06666C14.676 1.05526 13.413 1.55924 12.47 2.47003L10.75 4.18003M13 10C12.5705 9.4259 12.0226 8.95084 11.3934 8.60709C10.7642 8.26333 10.0684 8.05891 9.3533 8.00769C8.63816 7.95648 7.92037 8.05966 7.24861 8.31025C6.57685 8.56083 5.96684 8.95296 5.45996 9.46003L2.45996 12.46C1.54917 13.403 1.04519 14.666 1.05659 15.977C1.06798 17.288 1.59382 18.5421 2.52086 19.4691C3.4479 20.3962 4.70197 20.922 6.01295 20.9334C7.32393 20.9448 8.58694 20.4408 9.52995 19.53L11.24 17.82"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M8.99996 12C9.42941 12.5742 9.97731 13.0492 10.6065 13.393C11.2357 13.7367 11.9315 13.9411 12.6466 13.9924C13.3617 14.0436 14.0795 13.9404 14.7513 13.6898C15.4231 13.4392 16.0331 13.0471 16.54 12.54L19.54 9.54003C20.4507 8.59702 20.9547 7.334 20.9433 6.02302C20.9319 4.71204 20.4061 3.45797 19.479 2.53093C18.552 1.60389 17.2979 1.07805 15.987 1.06666C14.676 1.05526 13.413 1.55924 12.47 2.47003L10.75 4.18003M13 10C12.5705 9.4259 12.0226 8.95084 11.3934 8.60709C10.7642 8.26333 10.0684 8.05891 9.3533 8.00769C8.63816 7.95648 7.92037 8.05966 7.24861 8.31025C6.57685 8.56083 5.96684 8.95296 5.45996 9.46003L2.45996 12.46C1.54917 13.403 1.04519 14.666 1.05659 15.977C1.06798 17.288 1.59382 18.5421 2.52086 19.4691C3.4479 20.3962 4.70197 20.922 6.01295 20.9334C7.32393 20.9448 8.58694 20.4408 9.52995 19.53L11.24 17.82" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="logout" viewBox="0 0 22 20" fill="none">
|
||||
<path
|
||||
d="M17 6L21 10M21 10L17 14M21 10H8M14 2.20404C12.7252 1.43827 11.2452 1 9.66667 1C4.8802 1 1 5.02944 1 10C1 14.9706 4.8802 19 9.66667 19C11.2452 19 12.7252 18.5617 14 17.796"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M17 6L21 10M21 10L17 14M21 10H8M14 2.20404C12.7252 1.43827 11.2452 1 9.66667 1C4.8802 1 1 5.02944 1 10C1 14.9706 4.8802 19 9.66667 19C11.2452 19 12.7252 18.5617 14 17.796" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="pin" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.9224 1.60798C10.752 1.43747 10.5936 1.27906 10.4521 1.16112C10.3041 1.03772 10.1054 0.897647 9.84544 0.844944C9.50107 0.775136 9.14307 0.84408 8.84926 1.03679C8.62745 1.18228 8.49504 1.38611 8.40342 1.55566C8.31586 1.7177 8.22763 1.92361 8.13268 2.14523L7.4096 3.83242C7.39337 3.8703 7.38541 3.88876 7.37934 3.90209L7.37892 3.903L7.37824 3.90372C7.36811 3.91431 7.35393 3.92856 7.32479 3.9577L6.28419 4.99829C6.23867 5.04382 6.21617 5.06618 6.19929 5.08193L6.19809 5.08305L6.19649 5.08343C6.17402 5.08874 6.14294 5.09505 6.0798 5.10768L3.60856 5.60192C3.31543 5.66053 3.05184 5.71323 2.84493 5.77469C2.63787 5.83621 2.37038 5.93737 2.16809 6.16535C1.90934 6.45696 1.79118 6.84722 1.84472 7.23339C1.88657 7.53529 2.05302 7.76784 2.19118 7.93388C2.32925 8.09979 2.51933 8.28985 2.73071 8.50121L4.64158 10.4121L1.34175 13.7119C1.0814 13.9723 1.0814 14.3944 1.34175 14.6547C1.6021 14.9151 2.02421 14.9151 2.28456 14.6547L5.58439 11.3549L7.49532 13.2658C7.70668 13.4772 7.89673 13.6673 8.06265 13.8053C8.22869 13.9435 8.46123 14.11 8.76314 14.1518C9.14931 14.2053 9.53956 14.0872 9.83117 13.8284C10.0592 13.6261 10.1603 13.3587 10.2218 13.1516C10.2833 12.9447 10.336 12.6811 10.3946 12.388L10.8889 9.91673C10.9015 9.85359 10.9078 9.82251 10.9131 9.80003L10.9135 9.79844L10.9146 9.79724C10.9303 9.78035 10.9527 9.75786 10.9982 9.71233L12.0388 8.67174C12.068 8.6426 12.0822 8.62841 12.0928 8.61829L12.0935 8.6176L12.0944 8.61719C12.1078 8.61111 12.1262 8.60316 12.1641 8.58692L13.8513 7.86385C14.0729 7.7689 14.2788 7.68066 14.4409 7.5931C14.6104 7.50149 14.8142 7.36908 14.9597 7.14726C15.1524 6.85346 15.2214 6.49545 15.1516 6.15109C15.0989 5.89111 14.9588 5.69247 14.8354 5.54443C14.7175 5.40295 14.5591 5.24456 14.3885 5.07409L10.9224 1.60798Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9224 1.60798C10.752 1.43747 10.5936 1.27906 10.4521 1.16112C10.3041 1.03772 10.1054 0.897647 9.84544 0.844944C9.50107 0.775136 9.14307 0.84408 8.84926 1.03679C8.62745 1.18228 8.49504 1.38611 8.40342 1.55566C8.31586 1.7177 8.22763 1.92361 8.13268 2.14523L7.4096 3.83242C7.39337 3.8703 7.38541 3.88876 7.37934 3.90209L7.37892 3.903L7.37824 3.90372C7.36811 3.91431 7.35393 3.92856 7.32479 3.9577L6.28419 4.99829C6.23867 5.04382 6.21617 5.06618 6.19929 5.08193L6.19809 5.08305L6.19649 5.08343C6.17402 5.08874 6.14294 5.09505 6.0798 5.10768L3.60856 5.60192C3.31543 5.66053 3.05184 5.71323 2.84493 5.77469C2.63787 5.83621 2.37038 5.93737 2.16809 6.16535C1.90934 6.45696 1.79118 6.84722 1.84472 7.23339C1.88657 7.53529 2.05302 7.76784 2.19118 7.93388C2.32925 8.09979 2.51933 8.28985 2.73071 8.50121L4.64158 10.4121L1.34175 13.7119C1.0814 13.9723 1.0814 14.3944 1.34175 14.6547C1.6021 14.9151 2.02421 14.9151 2.28456 14.6547L5.58439 11.3549L7.49532 13.2658C7.70668 13.4772 7.89673 13.6673 8.06265 13.8053C8.22869 13.9435 8.46123 14.11 8.76314 14.1518C9.14931 14.2053 9.53956 14.0872 9.83117 13.8284C10.0592 13.6261 10.1603 13.3587 10.2218 13.1516C10.2833 12.9447 10.336 12.6811 10.3946 12.388L10.8889 9.91673C10.9015 9.85359 10.9078 9.82251 10.9131 9.80003L10.9135 9.79844L10.9146 9.79724C10.9303 9.78035 10.9527 9.75786 10.9982 9.71233L12.0388 8.67174C12.068 8.6426 12.0822 8.62841 12.0928 8.61829L12.0935 8.6176L12.0944 8.61719C12.1078 8.61111 12.1262 8.60316 12.1641 8.58692L13.8513 7.86385C14.0729 7.7689 14.2788 7.68066 14.4409 7.5931C14.6104 7.50149 14.8142 7.36908 14.9597 7.14726C15.1524 6.85346 15.2214 6.49545 15.1516 6.15109C15.0989 5.89111 14.9588 5.69247 14.8354 5.54443C14.7175 5.40295 14.5591 5.24456 14.3885 5.07409L10.9224 1.60798Z" fill="currentColor" />
|
||||
</symbol>
|
||||
<symbol id="plus" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M8 1V15M1 8H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="profile" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M14 8H14.01M8 8H8.01M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11ZM14.5 8C14.5 8.27614 14.2761 8.5 14 8.5C13.7239 8.5 13.5 8.27614 13.5 8C13.5 7.72386 13.7239 7.5 14 7.5C14.2761 7.5 14.5 7.72386 14.5 8ZM8.5 8C8.5 8.27614 8.27614 8.5 8 8.5C7.72386 8.5 7.5 8.27614 7.5 8C7.5 7.72386 7.72386 7.5 8 7.5C8.27614 7.5 8.5 7.72386 8.5 8ZM11 16.5C13.5005 16.5 15.5 14.667 15.5 13H6.5C6.5 14.667 8.4995 16.5 11 16.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M14 8H14.01M8 8H8.01M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11ZM14.5 8C14.5 8.27614 14.2761 8.5 14 8.5C13.7239 8.5 13.5 8.27614 13.5 8C13.5 7.72386 13.7239 7.5 14 7.5C14.2761 7.5 14.5 7.72386 14.5 8ZM8.5 8C8.5 8.27614 8.27614 8.5 8 8.5C7.72386 8.5 7.5 8.27614 7.5 8C7.5 7.72386 7.72386 7.5 8 7.5C8.27614 7.5 8.5 7.72386 8.5 8ZM11 16.5C13.5005 16.5 15.5 14.667 15.5 13H6.5C6.5 14.667 8.4995 16.5 11 16.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="qr" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M4.5 4.5H4.51M15.5 4.5H15.51M4.5 15.5H4.51M11 11H11.01M15.5 15.5H15.51M15 19H19V15M12 14.5V19M19 12H14.5M13.6 8H17.4C17.9601 8 18.2401 8 18.454 7.89101C18.6422 7.79513 18.7951 7.64215 18.891 7.45399C19 7.24008 19 6.96005 19 6.4V2.6C19 2.03995 19 1.75992 18.891 1.54601C18.7951 1.35785 18.6422 1.20487 18.454 1.10899C18.2401 1 17.9601 1 17.4 1H13.6C13.0399 1 12.7599 1 12.546 1.10899C12.3578 1.20487 12.2049 1.35785 12.109 1.54601C12 1.75992 12 2.03995 12 2.6V6.4C12 6.96005 12 7.24008 12.109 7.45399C12.2049 7.64215 12.3578 7.79513 12.546 7.89101C12.7599 8 13.0399 8 13.6 8ZM2.6 8H6.4C6.96005 8 7.24008 8 7.45399 7.89101C7.64215 7.79513 7.79513 7.64215 7.89101 7.45399C8 7.24008 8 6.96005 8 6.4V2.6C8 2.03995 8 1.75992 7.89101 1.54601C7.79513 1.35785 7.64215 1.20487 7.45399 1.10899C7.24008 1 6.96005 1 6.4 1H2.6C2.03995 1 1.75992 1 1.54601 1.10899C1.35785 1.20487 1.20487 1.35785 1.10899 1.54601C1 1.75992 1 2.03995 1 2.6V6.4C1 6.96005 1 7.24008 1.10899 7.45399C1.20487 7.64215 1.35785 7.79513 1.54601 7.89101C1.75992 8 2.03995 8 2.6 8ZM2.6 19H6.4C6.96005 19 7.24008 19 7.45399 18.891C7.64215 18.7951 7.79513 18.6422 7.89101 18.454C8 18.2401 8 17.9601 8 17.4V13.6C8 13.0399 8 12.7599 7.89101 12.546C7.79513 12.3578 7.64215 12.2049 7.45399 12.109C7.24008 12 6.96005 12 6.4 12H2.6C2.03995 12 1.75992 12 1.54601 12.109C1.35785 12.2049 1.20487 12.3578 1.10899 12.546C1 12.7599 1 13.0399 1 13.6V17.4C1 17.9601 1 18.2401 1.10899 18.454C1.20487 18.6422 1.35785 18.7951 1.54601 18.891C1.75992 19 2.03995 19 2.6 19Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M4.5 4.5H4.51M15.5 4.5H15.51M4.5 15.5H4.51M11 11H11.01M15.5 15.5H15.51M15 19H19V15M12 14.5V19M19 12H14.5M13.6 8H17.4C17.9601 8 18.2401 8 18.454 7.89101C18.6422 7.79513 18.7951 7.64215 18.891 7.45399C19 7.24008 19 6.96005 19 6.4V2.6C19 2.03995 19 1.75992 18.891 1.54601C18.7951 1.35785 18.6422 1.20487 18.454 1.10899C18.2401 1 17.9601 1 17.4 1H13.6C13.0399 1 12.7599 1 12.546 1.10899C12.3578 1.20487 12.2049 1.35785 12.109 1.54601C12 1.75992 12 2.03995 12 2.6V6.4C12 6.96005 12 7.24008 12.109 7.45399C12.2049 7.64215 12.3578 7.79513 12.546 7.89101C12.7599 8 13.0399 8 13.6 8ZM2.6 8H6.4C6.96005 8 7.24008 8 7.45399 7.89101C7.64215 7.79513 7.79513 7.64215 7.89101 7.45399C8 7.24008 8 6.96005 8 6.4V2.6C8 2.03995 8 1.75992 7.89101 1.54601C7.79513 1.35785 7.64215 1.20487 7.45399 1.10899C7.24008 1 6.96005 1 6.4 1H2.6C2.03995 1 1.75992 1 1.54601 1.10899C1.35785 1.20487 1.20487 1.35785 1.10899 1.54601C1 1.75992 1 2.03995 1 2.6V6.4C1 6.96005 1 7.24008 1.10899 7.45399C1.20487 7.64215 1.35785 7.79513 1.54601 7.89101C1.75992 8 2.03995 8 2.6 8ZM2.6 19H6.4C6.96005 19 7.24008 19 7.45399 18.891C7.64215 18.7951 7.79513 18.6422 7.89101 18.454C8 18.2401 8 17.9601 8 17.4V13.6C8 13.0399 8 12.7599 7.89101 12.546C7.79513 12.3578 7.64215 12.2049 7.45399 12.109C7.24008 12 6.96005 12 6.4 12H2.6C2.03995 12 1.75992 12 1.54601 12.109C1.35785 12.2049 1.20487 12.3578 1.10899 12.546C1 12.7599 1 13.0399 1 13.6V17.4C1 17.9601 1 18.2401 1.10899 18.454C1.20487 18.6422 1.35785 18.7951 1.54601 18.891C1.75992 19 2.03995 19 2.6 19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="read" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M14.9996 2V7M14.9996 7L19.9996 7M14.9996 7L20.9996 1M10.2266 11.8631C9.02506 10.6615 8.07627 9.30285 7.38028 7.85323C7.32041 7.72854 7.29048 7.66619 7.26748 7.5873C7.18576 7.30695 7.24446 6.96269 7.41447 6.72526C7.46231 6.65845 7.51947 6.60129 7.63378 6.48698C7.98338 6.13737 8.15819 5.96257 8.27247 5.78679C8.70347 5.1239 8.70347 4.26932 8.27247 3.60643C8.15819 3.43065 7.98338 3.25585 7.63378 2.90624L7.43891 2.71137C6.90747 2.17993 6.64174 1.91421 6.35636 1.76987C5.7888 1.4828 5.11854 1.4828 4.55098 1.76987C4.2656 1.91421 3.99987 2.17993 3.46843 2.71137L3.3108 2.86901C2.78117 3.39863 2.51636 3.66344 2.31411 4.02348C2.08969 4.42298 1.92833 5.04347 1.9297 5.5017C1.93092 5.91464 2.01103 6.19687 2.17124 6.76131C3.03221 9.79471 4.65668 12.6571 7.04466 15.045C9.43264 17.433 12.295 19.0575 15.3284 19.9185C15.8928 20.0787 16.1751 20.1588 16.588 20.16C17.0462 20.1614 17.6667 20 18.0662 19.7756C18.4263 19.5733 18.6911 19.3085 19.2207 18.7789L19.3783 18.6213C19.9098 18.0898 20.1755 17.8241 20.3198 17.5387C20.6069 16.9712 20.6069 16.3009 20.3198 15.7333C20.1755 15.448 19.9098 15.1822 19.3783 14.6508L19.1835 14.4559C18.8339 14.1063 18.6591 13.9315 18.4833 13.8172C17.8204 13.3862 16.9658 13.3862 16.3029 13.8172C16.1271 13.9315 15.9523 14.1063 15.6027 14.4559C15.4884 14.5702 15.4313 14.6274 15.3644 14.6752C15.127 14.8453 14.7828 14.904 14.5024 14.8222C14.4235 14.7992 14.3612 14.7693 14.2365 14.7094C12.7869 14.0134 11.4282 13.0646 10.2266 11.8631Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M14.9996 2V7M14.9996 7L19.9996 7M14.9996 7L20.9996 1M10.2266 11.8631C9.02506 10.6615 8.07627 9.30285 7.38028 7.85323C7.32041 7.72854 7.29048 7.66619 7.26748 7.5873C7.18576 7.30695 7.24446 6.96269 7.41447 6.72526C7.46231 6.65845 7.51947 6.60129 7.63378 6.48698C7.98338 6.13737 8.15819 5.96257 8.27247 5.78679C8.70347 5.1239 8.70347 4.26932 8.27247 3.60643C8.15819 3.43065 7.98338 3.25585 7.63378 2.90624L7.43891 2.71137C6.90747 2.17993 6.64174 1.91421 6.35636 1.76987C5.7888 1.4828 5.11854 1.4828 4.55098 1.76987C4.2656 1.91421 3.99987 2.17993 3.46843 2.71137L3.3108 2.86901C2.78117 3.39863 2.51636 3.66344 2.31411 4.02348C2.08969 4.42298 1.92833 5.04347 1.9297 5.5017C1.93092 5.91464 2.01103 6.19687 2.17124 6.76131C3.03221 9.79471 4.65668 12.6571 7.04466 15.045C9.43264 17.433 12.295 19.0575 15.3284 19.9185C15.8928 20.0787 16.1751 20.1588 16.588 20.16C17.0462 20.1614 17.6667 20 18.0662 19.7756C18.4263 19.5733 18.6911 19.3085 19.2207 18.7789L19.3783 18.6213C19.9098 18.0898 20.1755 17.8241 20.3198 17.5387C20.6069 16.9712 20.6069 16.3009 20.3198 15.7333C20.1755 15.448 19.9098 15.1822 19.3783 14.6508L19.1835 14.4559C18.8339 14.1063 18.6591 13.9315 18.4833 13.8172C17.8204 13.3862 16.9658 13.3862 16.3029 13.8172C16.1271 13.9315 15.9523 14.1063 15.6027 14.4559C15.4884 14.5702 15.4313 14.6274 15.3644 14.6752C15.127 14.8453 14.7828 14.904 14.5024 14.8222C14.4235 14.7992 14.3612 14.7693 14.2365 14.7094C12.7869 14.0134 11.4282 13.0646 10.2266 11.8631Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="relay" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M21 9.5L20.5256 5.70463C20.3395 4.21602 20.2465 3.47169 19.8961 2.9108C19.5875 2.41662 19.1416 2.02301 18.613 1.77804C18.013 1.5 17.2629 1.5 15.7626 1.5H6.23735C4.73714 1.5 3.98704 1.5 3.38702 1.77804C2.85838 2.02301 2.4125 2.41662 2.10386 2.9108C1.75354 3.47169 1.6605 4.21601 1.47442 5.70463L1 9.5M4.5 13.5H17.5M4.5 13.5C2.567 13.5 1 11.933 1 10C1 8.067 2.567 6.5 4.5 6.5H17.5C19.433 6.5 21 8.067 21 10C21 11.933 19.433 13.5 17.5 13.5M4.5 13.5C2.567 13.5 1 15.067 1 17C1 18.933 2.567 20.5 4.5 20.5H17.5C19.433 20.5 21 18.933 21 17C21 15.067 19.433 13.5 17.5 13.5M5 10H5.01M5 17H5.01M11 10H17M11 17H17"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M21 9.5L20.5256 5.70463C20.3395 4.21602 20.2465 3.47169 19.8961 2.9108C19.5875 2.41662 19.1416 2.02301 18.613 1.77804C18.013 1.5 17.2629 1.5 15.7626 1.5H6.23735C4.73714 1.5 3.98704 1.5 3.38702 1.77804C2.85838 2.02301 2.4125 2.41662 2.10386 2.9108C1.75354 3.47169 1.6605 4.21601 1.47442 5.70463L1 9.5M4.5 13.5H17.5M4.5 13.5C2.567 13.5 1 11.933 1 10C1 8.067 2.567 6.5 4.5 6.5H17.5C19.433 6.5 21 8.067 21 10C21 11.933 19.433 13.5 17.5 13.5M4.5 13.5C2.567 13.5 1 15.067 1 17C1 18.933 2.567 20.5 4.5 20.5H17.5C19.433 20.5 21 18.933 21 17C21 15.067 19.433 13.5 17.5 13.5M5 10H5.01M5 17H5.01M11 10H17M11 17H17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="reply" viewBox="0 0 18 18" fill="none">
|
||||
<path
|
||||
d="M1.5 5.5C1.5 4.09987 1.5 3.3998 1.77248 2.86502C2.01217 2.39462 2.39462 2.01217 2.86502 1.77248C3.3998 1.5 4.09987 1.5 5.5 1.5H12.5C13.9001 1.5 14.6002 1.5 15.135 1.77248C15.6054 2.01217 15.9878 2.39462 16.2275 2.86502C16.5 3.3998 16.5 4.09987 16.5 5.5V10C16.5 11.4001 16.5 12.1002 16.2275 12.635C15.9878 13.1054 15.6054 13.4878 15.135 13.7275C14.6002 14 13.9001 14 12.5 14H10.4031C9.88308 14 9.62306 14 9.37435 14.051C9.15369 14.0963 8.94017 14.1712 8.73957 14.2737C8.51347 14.3892 8.31043 14.5517 7.90434 14.8765L5.91646 16.4668C5.56973 16.7442 5.39636 16.8829 5.25045 16.8831C5.12356 16.8832 5.00352 16.8255 4.92436 16.7263C4.83333 16.6123 4.83333 16.3903 4.83333 15.9463V14C4.05836 14 3.67087 14 3.35295 13.9148C2.49022 13.6836 1.81635 13.0098 1.58519 12.147C1.5 11.8291 1.5 11.4416 1.5 10.6667V5.5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M1.5 5.5C1.5 4.09987 1.5 3.3998 1.77248 2.86502C2.01217 2.39462 2.39462 2.01217 2.86502 1.77248C3.3998 1.5 4.09987 1.5 5.5 1.5H12.5C13.9001 1.5 14.6002 1.5 15.135 1.77248C15.6054 2.01217 15.9878 2.39462 16.2275 2.86502C16.5 3.3998 16.5 4.09987 16.5 5.5V10C16.5 11.4001 16.5 12.1002 16.2275 12.635C15.9878 13.1054 15.6054 13.4878 15.135 13.7275C14.6002 14 13.9001 14 12.5 14H10.4031C9.88308 14 9.62306 14 9.37435 14.051C9.15369 14.0963 8.94017 14.1712 8.73957 14.2737C8.51347 14.3892 8.31043 14.5517 7.90434 14.8765L5.91646 16.4668C5.56973 16.7442 5.39636 16.8829 5.25045 16.8831C5.12356 16.8832 5.00352 16.8255 4.92436 16.7263C4.83333 16.6123 4.83333 16.3903 4.83333 15.9463V14C4.05836 14 3.67087 14 3.35295 13.9148C2.49022 13.6836 1.81635 13.0098 1.58519 12.147C1.5 11.8291 1.5 11.4416 1.5 10.6667V5.5Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="repost" viewBox="0 0 22 20" fill="none">
|
||||
<path
|
||||
d="M1 12C1 12 1.12132 12.8492 4.63604 16.364C8.15076 19.8787 13.8492 19.8787 17.364 16.364C18.6092 15.1187 19.4133 13.5993 19.7762 12M1 12V18M1 12H7M21 8C21 8 20.8787 7.15076 17.364 3.63604C13.8492 0.12132 8.15076 0.12132 4.63604 3.63604C3.39076 4.88131 2.58669 6.40072 2.22383 8M21 8V2M21 8H15"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M1 12C1 12 1.12132 12.8492 4.63604 16.364C8.15076 19.8787 13.8492 19.8787 17.364 16.364C18.6092 15.1187 19.4133 13.5993 19.7762 12M1 12V18M1 12H7M21 8C21 8 20.8787 7.15076 17.364 3.63604C13.8492 0.12132 8.15076 0.12132 4.63604 3.63604C3.39076 4.88131 2.58669 6.40072 2.22383 8M21 8V2M21 8H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="share" viewBox="0 0 18 16" fill="none">
|
||||
<path
|
||||
d="M16.3261 8.50715C16.5296 8.33277 16.6313 8.24559 16.6686 8.14184C16.7013 8.05078 16.7013 7.95117 16.6686 7.86011C16.6313 7.75636 16.5296 7.66918 16.3261 7.4948L9.26719 1.44429C8.917 1.14412 8.74191 0.99404 8.59367 0.990363C8.46483 0.987167 8.34177 1.04377 8.26035 1.14367C8.16667 1.25861 8.16667 1.48923 8.16667 1.95045V5.52984C6.38777 5.84105 4.75966 6.74244 3.54976 8.09586C2.23069 9.5714 1.50103 11.4809 1.5 13.4601V13.9701C2.37445 12.9167 3.46626 12.0647 4.70063 11.4726C5.78891 10.9505 6.96535 10.6412 8.16667 10.5597V14.0515C8.16667 14.5127 8.16667 14.7433 8.26035 14.8583C8.34177 14.9582 8.46483 15.0148 8.59367 15.0116C8.74191 15.0079 8.917 14.8578 9.26719 14.5577L16.3261 8.50715Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M16.3261 8.50715C16.5296 8.33277 16.6313 8.24559 16.6686 8.14184C16.7013 8.05078 16.7013 7.95117 16.6686 7.86011C16.6313 7.75636 16.5296 7.66918 16.3261 7.4948L9.26719 1.44429C8.917 1.14412 8.74191 0.99404 8.59367 0.990363C8.46483 0.987167 8.34177 1.04377 8.26035 1.14367C8.16667 1.25861 8.16667 1.48923 8.16667 1.95045V5.52984C6.38777 5.84105 4.75966 6.74244 3.54976 8.09586C2.23069 9.5714 1.50103 11.4809 1.5 13.4601V13.9701C2.37445 12.9167 3.46626 12.0647 4.70063 11.4726C5.78891 10.9505 6.96535 10.6412 8.16667 10.5597V14.0515C8.16667 14.5127 8.16667 14.7433 8.26035 14.8583C8.34177 14.9582 8.46483 15.0148 8.59367 15.0116C8.74191 15.0079 8.917 14.8578 9.26719 14.5577L16.3261 8.50715Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="translate" viewBox="0 0 20 18" fill="none">
|
||||
<path
|
||||
d="M10.7608 13.1667H16.7391M10.7608 13.1667L9.16663 16.5M10.7608 13.1667L13.1485 8.17419C13.3409 7.77189 13.4371 7.57075 13.5688 7.50718C13.6832 7.4519 13.8167 7.4519 13.9311 7.50718C14.0628 7.57075 14.159 7.77189 14.3514 8.17419L16.7391 13.1667M16.7391 13.1667L18.3333 16.5M1.66663 3.16667H6.66663M6.66663 3.16667H9.58329M6.66663 3.16667V1.5M9.58329 3.16667H11.6666M9.58329 3.16667C9.16984 5.63107 8.21045 7.86349 6.80458 9.73702M8.33329 10.6667C7.82285 10.4373 7.30217 10.1184 6.80458 9.73702M6.80458 9.73702C5.67748 8.87314 4.66893 7.68886 4.16663 6.5M6.80458 9.73702C5.46734 11.5191 3.72615 12.9765 1.66663 14"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M10.7608 13.1667H16.7391M10.7608 13.1667L9.16663 16.5M10.7608 13.1667L13.1485 8.17419C13.3409 7.77189 13.4371 7.57075 13.5688 7.50718C13.6832 7.4519 13.8167 7.4519 13.9311 7.50718C14.0628 7.57075 14.159 7.77189 14.3514 8.17419L16.7391 13.1667M16.7391 13.1667L18.3333 16.5M1.66663 3.16667H6.66663M6.66663 3.16667H9.58329M6.66663 3.16667V1.5M9.58329 3.16667H11.6666M9.58329 3.16667C9.16984 5.63107 8.21045 7.86349 6.80458 9.73702M8.33329 10.6667C7.82285 10.4373 7.30217 10.1184 6.80458 9.73702M6.80458 9.73702C5.67748 8.87314 4.66893 7.68886 4.16663 6.5M6.80458 9.73702C5.46734 11.5191 3.72615 12.9765 1.66663 14" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="trash" viewBox="0 0 20 22" fill="none">
|
||||
<path
|
||||
d="M14 5V4.2C14 3.0799 14 2.51984 13.782 2.09202C13.5903 1.71569 13.2843 1.40973 12.908 1.21799C12.4802 1 11.9201 1 10.8 1H9.2C8.07989 1 7.51984 1 7.09202 1.21799C6.71569 1.40973 6.40973 1.71569 6.21799 2.09202C6 2.51984 6 3.0799 6 4.2V5M1 5H19M17 5V16.2C17 17.8802 17 18.7202 16.673 19.362C16.3854 19.9265 15.9265 20.3854 15.362 20.673C14.7202 21 13.8802 21 12.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M14 5V4.2C14 3.0799 14 2.51984 13.782 2.09202C13.5903 1.71569 13.2843 1.40973 12.908 1.21799C12.4802 1 11.9201 1 10.8 1H9.2C8.07989 1 7.51984 1 7.09202 1.21799C6.71569 1.40973 6.40973 1.71569 6.21799 2.09202C6 2.51984 6 3.0799 6 4.2V5M1 5H19M17 5V16.2C17 17.8802 17 18.7202 16.673 19.362C16.3854 19.9265 15.9265 20.3854 15.362 20.673C14.7202 21 13.8802 21 12.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="write" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M20.9996 6V1M20.9996 1H15.9996M20.9996 1L14.9996 7M10.2266 11.8631C9.02506 10.6615 8.07627 9.30285 7.38028 7.85323C7.32041 7.72854 7.29048 7.66619 7.26748 7.5873C7.18576 7.30695 7.24446 6.96269 7.41447 6.72526C7.46231 6.65845 7.51947 6.60129 7.63378 6.48698C7.98338 6.13737 8.15819 5.96257 8.27247 5.78679C8.70347 5.1239 8.70347 4.26932 8.27247 3.60643C8.15819 3.43065 7.98338 3.25585 7.63378 2.90624L7.43891 2.71137C6.90747 2.17993 6.64174 1.91421 6.35636 1.76987C5.7888 1.4828 5.11854 1.4828 4.55098 1.76987C4.2656 1.91421 3.99987 2.17993 3.46843 2.71137L3.3108 2.86901C2.78117 3.39863 2.51636 3.66344 2.31411 4.02348C2.08969 4.42298 1.92833 5.04347 1.9297 5.5017C1.93092 5.91464 2.01103 6.19687 2.17124 6.76131C3.03221 9.79471 4.65668 12.6571 7.04466 15.045C9.43264 17.433 12.295 19.0575 15.3284 19.9185C15.8928 20.0787 16.1751 20.1588 16.588 20.16C17.0462 20.1614 17.6667 20 18.0662 19.7756C18.4263 19.5733 18.6911 19.3085 19.2207 18.7789L19.3783 18.6213C19.9098 18.0898 20.1755 17.8241 20.3198 17.5387C20.6069 16.9712 20.6069 16.3009 20.3198 15.7333C20.1755 15.448 19.9098 15.1822 19.3783 14.6508L19.1835 14.4559C18.8339 14.1063 18.6591 13.9315 18.4833 13.8172C17.8204 13.3862 16.9658 13.3862 16.3029 13.8172C16.1271 13.9315 15.9523 14.1063 15.6027 14.4559C15.4884 14.5702 15.4313 14.6274 15.3644 14.6752C15.127 14.8453 14.7828 14.904 14.5024 14.8222C14.4235 14.7992 14.3612 14.7693 14.2365 14.7094C12.7869 14.0134 11.4282 13.0646 10.2266 11.8631Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M20.9996 6V1M20.9996 1H15.9996M20.9996 1L14.9996 7M10.2266 11.8631C9.02506 10.6615 8.07627 9.30285 7.38028 7.85323C7.32041 7.72854 7.29048 7.66619 7.26748 7.5873C7.18576 7.30695 7.24446 6.96269 7.41447 6.72526C7.46231 6.65845 7.51947 6.60129 7.63378 6.48698C7.98338 6.13737 8.15819 5.96257 8.27247 5.78679C8.70347 5.1239 8.70347 4.26932 8.27247 3.60643C8.15819 3.43065 7.98338 3.25585 7.63378 2.90624L7.43891 2.71137C6.90747 2.17993 6.64174 1.91421 6.35636 1.76987C5.7888 1.4828 5.11854 1.4828 4.55098 1.76987C4.2656 1.91421 3.99987 2.17993 3.46843 2.71137L3.3108 2.86901C2.78117 3.39863 2.51636 3.66344 2.31411 4.02348C2.08969 4.42298 1.92833 5.04347 1.9297 5.5017C1.93092 5.91464 2.01103 6.19687 2.17124 6.76131C3.03221 9.79471 4.65668 12.6571 7.04466 15.045C9.43264 17.433 12.295 19.0575 15.3284 19.9185C15.8928 20.0787 16.1751 20.1588 16.588 20.16C17.0462 20.1614 17.6667 20 18.0662 19.7756C18.4263 19.5733 18.6911 19.3085 19.2207 18.7789L19.3783 18.6213C19.9098 18.0898 20.1755 17.8241 20.3198 17.5387C20.6069 16.9712 20.6069 16.3009 20.3198 15.7333C20.1755 15.448 19.9098 15.1822 19.3783 14.6508L19.1835 14.4559C18.8339 14.1063 18.6591 13.9315 18.4833 13.8172C17.8204 13.3862 16.9658 13.3862 16.3029 13.8172C16.1271 13.9315 15.9523 14.1063 15.6027 14.4559C15.4884 14.5702 15.4313 14.6274 15.3644 14.6752C15.127 14.8453 14.7828 14.904 14.5024 14.8222C14.4235 14.7992 14.3612 14.7693 14.2365 14.7094C12.7869 14.0134 11.4282 13.0646 10.2266 11.8631Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="bitcoin" viewBox="0 0 16 22" fill="none">
|
||||
<path
|
||||
d="M5.5 1V3M5.5 19V21M9.5 1V3M9.5 19V21M3.5 3H10C12.2091 3 14 4.79086 14 7C14 9.20914 12.2091 11 10 11H3.5H11C13.2091 11 15 12.7909 15 15C15 17.2091 13.2091 19 11 19H3.5M3.5 3H1.5M3.5 3V19M3.5 19H1.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M5.5 1V3M5.5 19V21M9.5 1V3M9.5 19V21M3.5 3H10C12.2091 3 14 4.79086 14 7C14 9.20914 12.2091 11 10 11H3.5H11C13.2091 11 15 12.7909 15 15C15 17.2091 13.2091 19 11 19H3.5M3.5 3H1.5M3.5 3V19M3.5 19H1.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="lightbulb" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M9 16.6586V19C9 20.1046 9.89543 21 11 21C12.1046 21 13 20.1046 13 19V16.6586M11 1V2M2 11H1M4.5 4.5L3.8999 3.8999M17.5 4.5L18.1002 3.8999M21 11H20M17 11C17 14.3137 14.3137 17 11 17C7.68629 17 5 14.3137 5 11C5 7.68629 7.68629 5 11 5C14.3137 5 17 7.68629 17 11Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M9 16.6586V19C9 20.1046 9.89543 21 11 21C12.1046 21 13 20.1046 13 19V16.6586M11 1V2M2 11H1M4.5 4.5L3.8999 3.8999M17.5 4.5L18.1002 3.8999M21 11H20M17 11C17 14.3137 14.3137 17 11 17C7.68629 17 5 14.3137 5 11C5 7.68629 7.68629 5 11 5C14.3137 5 17 7.68629 17 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="openeye" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M1.42012 8.71318C1.28394 8.49754 1.21584 8.38972 1.17772 8.22342C1.14909 8.0985 1.14909 7.9015 1.17772 7.77658C1.21584 7.61028 1.28394 7.50246 1.42012 7.28682C2.54553 5.50484 5.8954 1 11.0004 1C16.1054 1 19.4553 5.50484 20.5807 7.28682C20.7169 7.50246 20.785 7.61028 20.8231 7.77658C20.8517 7.9015 20.8517 8.0985 20.8231 8.22342C20.785 8.38972 20.7169 8.49754 20.5807 8.71318C19.4553 10.4952 16.1054 15 11.0004 15C5.8954 15 2.54553 10.4952 1.42012 8.71318Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M11.0004 11C12.6573 11 14.0004 9.65685 14.0004 8C14.0004 6.34315 12.6573 5 11.0004 5C9.34355 5 8.0004 6.34315 8.0004 8C8.0004 9.65685 9.34355 11 11.0004 11Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M1.42012 8.71318C1.28394 8.49754 1.21584 8.38972 1.17772 8.22342C1.14909 8.0985 1.14909 7.9015 1.17772 7.77658C1.21584 7.61028 1.28394 7.50246 1.42012 7.28682C2.54553 5.50484 5.8954 1 11.0004 1C16.1054 1 19.4553 5.50484 20.5807 7.28682C20.7169 7.50246 20.785 7.61028 20.8231 7.77658C20.8517 7.9015 20.8517 8.0985 20.8231 8.22342C20.785 8.38972 20.7169 8.49754 20.5807 8.71318C19.4553 10.4952 16.1054 15 11.0004 15C5.8954 15 2.54553 10.4952 1.42012 8.71318Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M11.0004 11C12.6573 11 14.0004 9.65685 14.0004 8C14.0004 6.34315 12.6573 5 11.0004 5C9.34355 5 8.0004 6.34315 8.0004 8C8.0004 9.65685 9.34355 11 11.0004 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="closedeye" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9.74294 3.09232C10.1494 3.03223 10.5686 3 11.0004 3C16.1054 3 19.4553 7.50484 20.5807 9.28682C20.7169 9.5025 20.785 9.61034 20.8231 9.77667C20.8518 9.90159 20.8517 10.0987 20.8231 10.2236C20.7849 10.3899 20.7164 10.4985 20.5792 10.7156C20.2793 11.1901 19.8222 11.8571 19.2165 12.5805M5.72432 4.71504C3.56225 6.1817 2.09445 8.21938 1.42111 9.28528C1.28428 9.50187 1.21587 9.61016 1.17774 9.77648C1.1491 9.9014 1.14909 10.0984 1.17771 10.2234C1.21583 10.3897 1.28393 10.4975 1.42013 10.7132C2.54554 12.4952 5.89541 17 11.0004 17C13.0588 17 14.8319 16.2676 16.2888 15.2766M2.00042 1L20.0004 19M8.8791 7.87868C8.3362 8.42157 8.00042 9.17157 8.00042 10C8.00042 11.6569 9.34356 13 11.0004 13C11.8288 13 12.5788 12.6642 13.1217 12.1213"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M9.74294 3.09232C10.1494 3.03223 10.5686 3 11.0004 3C16.1054 3 19.4553 7.50484 20.5807 9.28682C20.7169 9.5025 20.785 9.61034 20.8231 9.77667C20.8518 9.90159 20.8517 10.0987 20.8231 10.2236C20.7849 10.3899 20.7164 10.4985 20.5792 10.7156C20.2793 11.1901 19.8222 11.8571 19.2165 12.5805M5.72432 4.71504C3.56225 6.1817 2.09445 8.21938 1.42111 9.28528C1.28428 9.50187 1.21587 9.61016 1.17774 9.77648C1.1491 9.9014 1.14909 10.0984 1.17771 10.2234C1.21583 10.3897 1.28393 10.4975 1.42013 10.7132C2.54554 12.4952 5.89541 17 11.0004 17C13.0588 17 14.8319 16.2676 16.2888 15.2766M2.00042 1L20.0004 19M8.8791 7.87868C8.3362 8.42157 8.00042 9.17157 8.00042 10C8.00042 11.6569 9.34356 13 11.0004 13C11.8288 13 12.5788 12.6642 13.1217 12.1213" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
<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"
|
||||
/>
|
||||
<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" />
|
||||
</symbol>
|
||||
<symbol id="code-circle" viewBox="0 0 22 22" fill="none">
|
||||
<path d="M13 16L16 13L13 10M9 6L6 9L9 12M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol id="key" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M15 6.99994C15 6.48812 14.8047 5.9763 14.4142 5.58579C14.0237 5.19526 13.5118 5 13 5M13 13C16.3137 13 19 10.3137 19 7C19 3.68629 16.3137 1 13 1C9.68629 1 7 3.68629 7 7C7 7.27368 7.01832 7.54308 7.05381 7.80704C7.11218 8.24118 7.14136 8.45825 7.12172 8.59559C7.10125 8.73865 7.0752 8.81575 7.00469 8.9419C6.937 9.063 6.81771 9.18229 6.57913 9.42087L1.46863 14.5314C1.29568 14.7043 1.2092 14.7908 1.14736 14.8917C1.09253 14.9812 1.05213 15.0787 1.02763 15.1808C1 15.2959 1 15.4182 1 15.6627V17.4C1 17.9601 1 18.2401 1.10899 18.454C1.20487 18.6422 1.35785 18.7951 1.54601 18.891C1.75992 19 2.03995 19 2.6 19H5V17H7V15H9L10.5791 13.4209C10.8177 13.1823 10.937 13.063 11.0581 12.9953C11.1843 12.9248 11.2613 12.8987 11.4044 12.8783C11.5417 12.8586 11.7588 12.8878 12.193 12.9462C12.4569 12.9817 12.7263 13 13 13Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol id="user" viewBox="0 0 18 20" fill="none">
|
||||
<path d="M17 19C17 17.6044 17 16.9067 16.8278 16.3389C16.44 15.0605 15.4395 14.06 14.1611 13.6722C13.5933 13.5 12.8956 13.5 11.5 13.5H6.5C5.10444 13.5 4.40665 13.5 3.83886 13.6722C2.56045 14.06 1.56004 15.0605 1.17224 16.3389C1 16.9067 1 17.6044 1 19M13.5 5.5C13.5 7.98528 11.4853 10 9 10C6.51472 10 4.5 7.98528 4.5 5.5C4.5 3.01472 6.51472 1 9 1C11.4853 1 13.5 3.01472 13.5 5.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol id="camera-plus" viewBox="0 0 22 21" fill="none">
|
||||
<path d="M21 10.5V13.6C21 15.8402 21 16.9603 20.564 17.816C20.1805 18.5686 19.5686 19.1805 18.816 19.564C17.9603 20 16.8402 20 14.6 20H7.4C5.15979 20 4.03969 20 3.18404 19.564C2.43139 19.1805 1.81947 18.5686 1.43597 17.816C1 16.9603 1 15.8402 1 13.6V8.4C1 6.15979 1 5.03969 1.43597 4.18404C1.81947 3.43139 2.43139 2.81947 3.18404 2.43597C4.03969 2 5.15979 2 7.4 2H11.5M18 7V1M15 4H21M15 11C15 13.2091 13.2091 15 11 15C8.79086 15 7 13.2091 7 11C7 8.79086 8.79086 7 11 7C13.2091 7 15 8.79086 15 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol id="edit" viewBox="0 0 23 23" fill="none">
|
||||
<path d="M10 3.99998H5.8C4.11984 3.99998 3.27976 3.99998 2.63803 4.32696C2.07354 4.61458 1.6146 5.07353 1.32698 5.63801C1 6.27975 1 7.11983 1 8.79998V17.2C1 18.8801 1 19.7202 1.32698 20.362C1.6146 20.9264 2.07354 21.3854 2.63803 21.673C3.27976 22 4.11984 22 5.8 22H14.2C15.8802 22 16.7202 22 17.362 21.673C17.9265 21.3854 18.3854 20.9264 18.673 20.362C19 19.7202 19 18.8801 19 17.2V13M6.99997 16H8.67452C9.1637 16 9.40829 16 9.63846 15.9447C9.84254 15.8957 10.0376 15.8149 10.2166 15.7053C10.4184 15.5816 10.5914 15.4086 10.9373 15.0627L20.5 5.49998C21.3284 4.67156 21.3284 3.32841 20.5 2.49998C19.6716 1.67156 18.3284 1.67155 17.5 2.49998L7.93723 12.0627C7.59133 12.4086 7.41838 12.5816 7.29469 12.7834C7.18504 12.9624 7.10423 13.1574 7.05523 13.3615C6.99997 13.5917 6.99997 13.8363 6.99997 14.3255V16Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
|
@ -0,0 +1,44 @@
|
|||
import { db, EventInteraction } from "Db";
|
||||
import { LoginStore } from "Login";
|
||||
import { sha256 } from "Util";
|
||||
import FeedCache from "./FeedCache";
|
||||
|
||||
class EventInteractionCache extends FeedCache<EventInteraction> {
|
||||
constructor() {
|
||||
super("EventInteraction", db.eventInteraction);
|
||||
}
|
||||
|
||||
key(of: EventInteraction): string {
|
||||
return sha256(of.event + of.by);
|
||||
}
|
||||
|
||||
override async preload(): Promise<void> {
|
||||
await super.preload();
|
||||
|
||||
const data = window.localStorage.getItem("zap-cache");
|
||||
if (data) {
|
||||
const toImport = [...new Set<string>(JSON.parse(data) as Array<string>)].map(a => {
|
||||
const ret = {
|
||||
event: a,
|
||||
by: LoginStore.takeSnapshot().publicKey,
|
||||
zapped: true,
|
||||
reacted: false,
|
||||
reposted: false,
|
||||
} as EventInteraction;
|
||||
ret.id = this.key(ret);
|
||||
return ret;
|
||||
});
|
||||
await this.bulkSet(toImport);
|
||||
|
||||
console.debug(`Imported dumb-zap-cache events: `, toImport.length);
|
||||
window.localStorage.removeItem("zap-cache");
|
||||
}
|
||||
await this.buffer([...this.onTable]);
|
||||
}
|
||||
|
||||
takeSnapshot(): EventInteraction[] {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
}
|
||||
|
||||
export const InteractionCache = new EventInteractionCache();
|
|
@ -15,6 +15,8 @@ export default abstract class FeedCache<TCached> {
|
|||
#hooks: Array<HookFilter> = [];
|
||||
#snapshot: Readonly<Array<TCached>> = [];
|
||||
#changed = true;
|
||||
#hits = 0;
|
||||
#miss = 0;
|
||||
protected onTable: Set<string> = new Set();
|
||||
protected cache: Map<string, TCached> = new Map();
|
||||
|
||||
|
@ -23,7 +25,10 @@ export default abstract class FeedCache<TCached> {
|
|||
this.#table = table;
|
||||
setInterval(() => {
|
||||
console.debug(
|
||||
`[${this.#name}] ${this.cache.size} loaded, ${this.onTable.size} on-disk, ${this.#hooks.length} hooks`
|
||||
`[${this.#name}] ${this.cache.size} loaded, ${this.onTable.size} on-disk, ${this.#hooks.length} hooks, ${(
|
||||
(this.#hits / (this.#hits + this.#miss)) *
|
||||
100
|
||||
).toFixed(1)} % hit`
|
||||
);
|
||||
}, 5_000);
|
||||
}
|
||||
|
@ -56,7 +61,13 @@ export default abstract class FeedCache<TCached> {
|
|||
|
||||
getFromCache(key?: string) {
|
||||
if (key) {
|
||||
return this.cache.get(key);
|
||||
const ret = this.cache.get(key);
|
||||
if (ret) {
|
||||
this.#hits++;
|
||||
} else {
|
||||
this.#miss++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,7 +165,7 @@ export default abstract class FeedCache<TCached> {
|
|||
|
||||
protected notifyChange(keys: Array<string>) {
|
||||
this.#changed = true;
|
||||
this.#hooks.filter(a => keys.includes(a.key)).forEach(h => h.fn());
|
||||
this.#hooks.filter(a => keys.includes(a.key) || a.key === "*").forEach(h => h.fn());
|
||||
}
|
||||
|
||||
abstract key(of: TCached): string;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import FeedCache from "Cache/FeedCache";
|
||||
import { db } from "Db";
|
||||
import { LNURL } from "LNURL";
|
||||
import { MetadataCache } from "Cache";
|
||||
import { LNURL } from "LNURL";
|
||||
|
||||
class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
#zapperQueue: Array<{ pubkey: string; lnurl: string }> = [];
|
||||
|
||||
constructor() {
|
||||
super("UserCache", db.users);
|
||||
this.#processZapperQueue();
|
||||
}
|
||||
|
||||
key(of: MetadataCache): string {
|
||||
|
@ -63,38 +66,59 @@ class UserProfileCache extends FeedCache<MetadataCache> {
|
|||
})();
|
||||
console.debug(`Updating ${m.pubkey} ${updateType}`, m);
|
||||
if (updateType !== "no_change") {
|
||||
if (updateType !== "refresh_profile") {
|
||||
// fetch zapper key
|
||||
const lnurl = m.lud16 || m.lud06;
|
||||
if (lnurl) {
|
||||
try {
|
||||
const svc = new LNURL(lnurl);
|
||||
await svc.load();
|
||||
m.zapService = svc.zapperPubkey;
|
||||
} catch {
|
||||
console.warn("Failed to load LNURL for zapper pubkey", lnurl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const writeProfile = {
|
||||
...existing,
|
||||
...m,
|
||||
};
|
||||
this.cache.set(m.pubkey, writeProfile);
|
||||
if (db.ready) {
|
||||
await db.users.put(writeProfile);
|
||||
this.onTable.add(m.pubkey);
|
||||
await this.#setItem(writeProfile);
|
||||
if (updateType !== "refresh_profile") {
|
||||
const lnurl = m.lud16 ?? m.lud06;
|
||||
if (lnurl) {
|
||||
this.#zapperQueue.push({
|
||||
pubkey: m.pubkey,
|
||||
lnurl,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.notifyChange([m.pubkey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return updateType;
|
||||
}
|
||||
|
||||
takeSnapshot(): MetadataCache[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async #setItem(m: MetadataCache) {
|
||||
this.cache.set(m.pubkey, m);
|
||||
if (db.ready) {
|
||||
await db.users.put(m);
|
||||
this.onTable.add(m.pubkey);
|
||||
}
|
||||
this.notifyChange([m.pubkey]);
|
||||
}
|
||||
|
||||
async #processZapperQueue() {
|
||||
while (this.#zapperQueue.length > 0) {
|
||||
const i = this.#zapperQueue.shift();
|
||||
if (i) {
|
||||
try {
|
||||
const svc = new LNURL(i.lnurl);
|
||||
await svc.load();
|
||||
const p = this.getFromCache(i.pubkey);
|
||||
if (p) {
|
||||
this.#setItem({
|
||||
...p,
|
||||
zapService: svc.zapperPubkey,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.warn("Failed to load LNURL for zapper pubkey", i.lnurl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => this.#processZapperQueue(), 1_000);
|
||||
}
|
||||
}
|
||||
|
||||
export const UserCache = new UserProfileCache();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { HexKey, TaggedRawEvent, UserMetadata } from "@snort/nostr";
|
||||
import { HexKey, RawEvent, UserMetadata } from "@snort/nostr";
|
||||
import { hexToBech32, unixNowMs } from "Util";
|
||||
import { DmCache } from "./DMCache";
|
||||
import { InteractionCache } from "./EventInteractionCache";
|
||||
import { UserCache } from "./UserCache";
|
||||
|
||||
export interface MetadataCache extends UserMetadata {
|
||||
|
@ -30,7 +31,7 @@ export interface MetadataCache extends UserMetadata {
|
|||
zapService?: HexKey;
|
||||
}
|
||||
|
||||
export function mapEventToProfile(ev: TaggedRawEvent) {
|
||||
export function mapEventToProfile(ev: RawEvent) {
|
||||
try {
|
||||
const data: UserMetadata = JSON.parse(ev.content);
|
||||
return {
|
||||
|
@ -48,6 +49,7 @@ export function mapEventToProfile(ev: TaggedRawEvent) {
|
|||
export async function preload() {
|
||||
await UserCache.preload();
|
||||
await DmCache.preload();
|
||||
await InteractionCache.preload();
|
||||
}
|
||||
|
||||
export { UserCache, DmCache };
|
||||
|
|
|
@ -98,7 +98,7 @@ export const EmailRegex =
|
|||
/**
|
||||
* Regex to match a mnemonic seed
|
||||
*/
|
||||
export const MnemonicRegex = /^([^\s]+\s){11}[^\s]+$/;
|
||||
export const MnemonicRegex = /(\w+)/g;
|
||||
|
||||
/**
|
||||
* Extract file extensions regex
|
||||
|
@ -177,4 +177,10 @@ export const MagnetRegex = /(magnet:[\S]+)/i;
|
|||
/**
|
||||
* Wavlake embed regex
|
||||
*/
|
||||
export const WavlakeRegex = /(?:player\.)?wavlake\.com\/(track\/[.a-zA-Z0-9-]+|album\/[.a-zA-Z0-9-]+|[.a-zA-Z0-9-]+)/i;
|
||||
export const WavlakeRegex =
|
||||
/https?:\/\/(?:player\.|www\.)?wavlake\.com\/(?!top|new|artists|account|activity|login|preferences|feed)(?:(?:track|album)\/[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}|[a-z-]+)/i;
|
||||
|
||||
/*
|
||||
* Regex to match any base64 string
|
||||
*/
|
||||
export const CashuRegex = /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/i;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { FullRelaySettings, HexKey, RawEvent, u256 } from "@snort/nostr";
|
|||
import { MetadataCache } from "Cache";
|
||||
|
||||
export const NAME = "snortDB";
|
||||
export const VERSION = 7;
|
||||
export const VERSION = 8;
|
||||
|
||||
export interface SubCache {
|
||||
id: string;
|
||||
|
@ -24,12 +24,22 @@ export interface UsersRelays {
|
|||
relays: FullRelaySettings[];
|
||||
}
|
||||
|
||||
export interface EventInteraction {
|
||||
id: u256;
|
||||
event: u256;
|
||||
by: HexKey;
|
||||
reacted: boolean;
|
||||
zapped: boolean;
|
||||
reposted: boolean;
|
||||
}
|
||||
|
||||
const STORES = {
|
||||
users: "++pubkey, name, display_name, picture, nip05, npub",
|
||||
relays: "++addr",
|
||||
userRelays: "++pubkey",
|
||||
events: "++id, pubkey, created_at",
|
||||
dms: "++id, pubkey",
|
||||
eventInteraction: "++id",
|
||||
};
|
||||
|
||||
export class SnortDB extends Dexie {
|
||||
|
@ -39,6 +49,7 @@ export class SnortDB extends Dexie {
|
|||
userRelays!: Table<UsersRelays>;
|
||||
events!: Table<RawEvent>;
|
||||
dms!: Table<RawEvent>;
|
||||
eventInteraction!: Table<EventInteraction>;
|
||||
|
||||
constructor() {
|
||||
super(NAME);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
background-clip: content-box, border-box;
|
||||
background-size: cover;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.avatar[data-domain="snort.social"] {
|
||||
|
|
|
@ -12,9 +12,8 @@ const Avatar = ({ user, ...rest }: { user?: UserMetadata; onClick?: () => void }
|
|||
|
||||
useEffect(() => {
|
||||
if (user?.picture) {
|
||||
proxy(user.picture, 120)
|
||||
.then(a => setUrl(a))
|
||||
.catch(console.warn);
|
||||
const url = proxy(user.picture, 120);
|
||||
setUrl(url);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import Icon from "Icons/Icon";
|
||||
import { useState } from "react";
|
||||
import useFileUpload from "Upload";
|
||||
import { openFile, unwrap } from "Util";
|
||||
|
||||
interface AvatarEditorProps {
|
||||
picture?: string;
|
||||
onPictureChange?: (newPicture: string) => void;
|
||||
}
|
||||
|
||||
export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorProps) {
|
||||
const uploader = useFileUpload();
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function uploadFile() {
|
||||
setError("");
|
||||
try {
|
||||
const f = await openFile();
|
||||
if (f) {
|
||||
const rsp = await uploader.upload(f, f.name);
|
||||
console.log(rsp);
|
||||
if (typeof rsp?.error === "string") {
|
||||
setError(`Upload failed: ${rsp.error}`);
|
||||
} else {
|
||||
onPictureChange?.(unwrap(rsp.url));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`Upload failed: ${e.message}`);
|
||||
} else {
|
||||
setError(`Upload failed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex f-center">
|
||||
<div style={{ backgroundImage: `url(${picture})` }} className="avatar">
|
||||
<div className={`edit${picture ? "" : " new"}`} onClick={() => uploadFile().catch(console.error)}>
|
||||
<Icon name={picture ? "edit" : "camera-plus"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <b className="error">{error}</b>}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { getDecodedToken } from "@cashu/cashu-ts";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
|
||||
export default function CashuNuts({ token }: { token: string }) {
|
||||
const login = useLogin();
|
||||
const profile = useUserProfile(login.publicKey);
|
||||
|
||||
async function copyToken(e: React.MouseEvent<HTMLButtonElement>, token: string) {
|
||||
e.stopPropagation();
|
||||
await navigator.clipboard.writeText(token);
|
||||
}
|
||||
async function redeemToken(e: React.MouseEvent<HTMLButtonElement>, token: string) {
|
||||
e.stopPropagation();
|
||||
const lnurl = profile?.lud16 ?? "";
|
||||
const url = `https://redeem.cashu.me?token=${encodeURIComponent(token)}&lightning=${encodeURIComponent(
|
||||
lnurl
|
||||
)}&autopay=yes`;
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
const cashu = useMemo(() => {
|
||||
try {
|
||||
if (!token.startsWith("cashuA") || token.length < 10) {
|
||||
return;
|
||||
}
|
||||
return getDecodedToken(token);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
if (!cashu) return <>{token}</>;
|
||||
|
||||
return (
|
||||
<div className="note-invoice">
|
||||
<div className="flex f-between">
|
||||
<div>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Cashu token" />
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Amount: {amount} sats"
|
||||
values={{
|
||||
amount: cashu.token[0].proofs.reduce((acc, v) => acc + v.amount, 0),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<small className="xs">
|
||||
<FormattedMessage defaultMessage="Mint: {url}" values={{ url: cashu.token[0].mint }} />
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={e => copyToken(e, token)} className="mr5">
|
||||
<FormattedMessage defaultMessage="Copy" description="Button: Copy Cashu token" />
|
||||
</button>
|
||||
<button onClick={e => redeemToken(e, token)}>
|
||||
<FormattedMessage defaultMessage="Redeem" description="Button: Redeem Cashu token" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -32,23 +32,25 @@ export const CollapsedIcon = ({ icon, collapsed }: CollapsedIconProps) => {
|
|||
interface CollapsedSectionProps {
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CollapsedSection = ({ title, children }: CollapsedSectionProps) => {
|
||||
export const CollapsedSection = ({ title, children, className }: CollapsedSectionProps) => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const icon = (
|
||||
<div className={`collapse-icon ${collapsed ? "" : "flip"}`} onClick={() => setCollapsed(!collapsed)}>
|
||||
<Icon name="chevronDown" />
|
||||
<div className={`collapse-icon ${collapsed ? "" : "flip"}`}>
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="collapsable-section">
|
||||
<h3 onClick={() => setCollapsed(!collapsed)}>{title}</h3>
|
||||
<div
|
||||
className={`collapsable-section${className ? ` ${className}` : ""}`}
|
||||
onClick={() => setCollapsed(!collapsed)}>
|
||||
{title}
|
||||
<CollapsedIcon icon={icon} collapsed={collapsed} />
|
||||
</div>
|
||||
|
||||
{collapsed ? null : children}
|
||||
{!collapsed && children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { ReactNode } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export interface FollowListBaseProps {
|
||||
pubkeys: HexKey[];
|
||||
|
@ -26,7 +26,7 @@ export default function FollowListBase({ pubkeys, title, showFollowAll, showAbou
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<>
|
||||
{(showFollowAll ?? true) && (
|
||||
<div className="flex mt10 mb10">
|
||||
<div className="f-grow bold">{title}</div>
|
||||
|
@ -38,6 +38,6 @@ export default function FollowListBase({ pubkeys, title, showFollowAll, showAbou
|
|||
{pubkeys?.map(a => (
|
||||
<ProfilePreview pubkey={a} key={a} options={{ about: showAbout }} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.hashtag {
|
||||
color: var(--highlight);
|
||||
}
|
||||
|
||||
.hashtag > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import NostrLink from "Element/NostrLink";
|
|||
import RevealMedia from "Element/RevealMedia";
|
||||
import MagnetLink from "Element/MagnetLink";
|
||||
|
||||
export default function HyperText({ link, creator }: { link: string; creator: string }) {
|
||||
export default function HyperText({ link, creator, depth }: { link: string; creator: string; depth?: number }) {
|
||||
const a = link;
|
||||
try {
|
||||
const url = new URL(a);
|
||||
|
@ -85,21 +85,14 @@ export default function HyperText({ link, creator }: { link: string; creator: st
|
|||
} else if (isWavlakeLink) {
|
||||
return <WavlakeEmbed link={a} />;
|
||||
} else if (url.protocol === "nostr:" || url.protocol === "web+nostr:") {
|
||||
return <NostrLink link={a} />;
|
||||
return <NostrLink link={a} depth={depth} />;
|
||||
} else if (url.protocol === "magnet:") {
|
||||
const parsed = magnetURIDecode(a);
|
||||
if (parsed) {
|
||||
return <MagnetLink magnet={parsed} />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>
|
||||
<LinkPreview url={a} />
|
||||
</>
|
||||
);
|
||||
return <LinkPreview url={a} />;
|
||||
}
|
||||
} catch {
|
||||
// Ignore the error.
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.link-preview-container {
|
||||
border: 1px solid var(--gray);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link-preview-container:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-preview-container > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-preview-title {
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.link-preview-title > small {
|
||||
color: var(--font-secondary-color);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.link-preview-image {
|
||||
margin: 0 0 15px 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background-image: var(--img-url);
|
||||
min-height: 250px;
|
||||
max-height: 500px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
|
@ -1,21 +1,14 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import "./LinkPreview.css";
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
|
||||
import { ApiHost } from "Const";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
|
||||
interface LinkPreviewData {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
import SnortApi, { LinkPreviewData } from "SnortApi";
|
||||
import useImgProxy from "Hooks/useImgProxy";
|
||||
|
||||
async function fetchUrlPreviewInfo(url: string) {
|
||||
const api = new SnortApi();
|
||||
try {
|
||||
const res = await fetch(`${ApiHost}/api/v1/preview?url=${encodeURIComponent(url)}`);
|
||||
if (res.ok) {
|
||||
return (await res.json()) as LinkPreviewData;
|
||||
}
|
||||
return await api.linkPreview(url);
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load link preview`, url);
|
||||
}
|
||||
|
@ -23,11 +16,12 @@ async function fetchUrlPreviewInfo(url: string) {
|
|||
|
||||
const LinkPreview = ({ url }: { url: string }) => {
|
||||
const [preview, setPreview] = useState<LinkPreviewData | null>();
|
||||
const { proxy } = useImgProxy();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await fetchUrlPreviewInfo(url);
|
||||
if (data && data.title) {
|
||||
if (data && data.image) {
|
||||
setPreview(data);
|
||||
} else {
|
||||
setPreview(null);
|
||||
|
@ -35,19 +29,27 @@ const LinkPreview = ({ url }: { url: string }) => {
|
|||
})();
|
||||
}, [url]);
|
||||
|
||||
if (preview === null) return null;
|
||||
if (preview === null)
|
||||
return (
|
||||
<a href={url} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
|
||||
const backgroundImage = preview?.image ? `url(${proxy(preview?.image)})` : "";
|
||||
const style = { "--img-url": backgroundImage } as CSSProperties;
|
||||
|
||||
return (
|
||||
<div className="link-preview-container">
|
||||
{preview && (
|
||||
<a href={url} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{preview?.image && <ProxyImg src={preview?.image} className="link-preview-image" />}
|
||||
{preview?.image && <div className="link-preview-image" style={style} />}
|
||||
<p className="link-preview-title">
|
||||
{preview?.title}
|
||||
{preview?.description && (
|
||||
<>
|
||||
<br />
|
||||
<small>{preview?.description}</small>
|
||||
<small>{preview.description.slice(0, 160)}</small>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.modal.spotlight .modal-body {
|
||||
max-width: 100vw;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.modal.spotlight img,
|
||||
.modal.spotlight video {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
aspect-ratio: unset;
|
||||
}
|
||||
|
||||
.modal.spotlight .close {
|
||||
text-align: right;
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { ProxyImg } from "Element/ProxyImg";
|
||||
import React, { MouseEvent, useState } from "react";
|
||||
|
||||
import "./MediaElement.css";
|
||||
import Modal from "Element/Modal";
|
||||
import Icon from "Icons/Icon";
|
||||
|
||||
/*
|
||||
[
|
||||
"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;
|
||||
magnet?: string;
|
||||
sha256?: string;
|
||||
blurHash?: string;
|
||||
}
|
||||
|
||||
export function MediaElement(props: MediaElementProps) {
|
||||
if (props.mime.startsWith("image/")) {
|
||||
return (
|
||||
<SpotlightMedia>
|
||||
<ProxyImg key={props.url} src={props.url} />
|
||||
</SpotlightMedia>
|
||||
);
|
||||
} else if (props.mime.startsWith("audio/")) {
|
||||
return <audio key={props.url} src={props.url} controls />;
|
||||
} else if (props.mime.startsWith("video/")) {
|
||||
return (
|
||||
<SpotlightMedia>
|
||||
<video key={props.url} src={props.url} controls />
|
||||
</SpotlightMedia>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
key={props.url}
|
||||
href={props.url}
|
||||
onClick={e => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext">
|
||||
{props.url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function SpotlightMedia({ children }: { children: React.ReactNode }) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
function onClick(e: MouseEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowModal(s => !s);
|
||||
}
|
||||
|
||||
function onClose(e: MouseEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowModal(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal onClose={onClose} className="spotlight">
|
||||
<div className="close" onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
</div>
|
||||
{children}
|
||||
</Modal>
|
||||
)}
|
||||
<div onClick={onClick}>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import { FileExtensionRegex } from "Const";
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
|
||||
export default function MediaLink({ link }: { link: string }) {
|
||||
const url = new URL(link);
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "jfif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp": {
|
||||
return <ProxyImg key={url.toString()} src={url.toString()} />;
|
||||
}
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg": {
|
||||
return <audio key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm": {
|
||||
return <video key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<a
|
||||
key={url.toString()}
|
||||
href={url.toString()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext">
|
||||
{url.toString()}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,4 +19,5 @@
|
|||
border: 1px solid var(--font-tertiary-color);
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -1,32 +1,15 @@
|
|||
import "./Modal.css";
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as React from "react";
|
||||
import { useEffect, MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
export interface ModalProps {
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function useOnClickOutside(ref: React.MutableRefObject<Element | null>, onClickOutside: () => void) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(ev: MouseEvent) {
|
||||
if (ref && ref.current && !ref.current.contains(ev.target as Node)) {
|
||||
onClickOutside();
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [ref]);
|
||||
onClose?: MouseEventHandler;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Modal(props: ModalProps) {
|
||||
const ref = useRef(null);
|
||||
const onClose = props.onClose || (() => undefined);
|
||||
const className = props.className || "";
|
||||
useOnClickOutside(ref, onClose);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add("scroll-lock");
|
||||
|
@ -34,8 +17,8 @@ export default function Modal(props: ModalProps) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`modal ${className}`}>
|
||||
<div ref={ref} className="modal-body">
|
||||
<div className={`modal ${className}`} onClick={onClose}>
|
||||
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import "./Nip05.css";
|
||||
import { useQuery } from "react-query";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import DnsOverHttpResolver from "dns-over-http-resolver";
|
||||
|
||||
import Icon from "Icons/Icon";
|
||||
import { bech32ToHex } from "Util";
|
||||
|
||||
interface NostrJson {
|
||||
names: Record<string, string>;
|
||||
}
|
||||
|
||||
const resolver = new DnsOverHttpResolver();
|
||||
async function fetchNip05Pubkey(name: string, domain: string) {
|
||||
if (!name || !domain) {
|
||||
return undefined;
|
||||
|
@ -18,9 +22,18 @@ async function fetchNip05Pubkey(name: string, domain: string) {
|
|||
return n.toLowerCase() === name.toLowerCase();
|
||||
});
|
||||
return match ? data.names[match] : undefined;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
// Check as DoH TXT entry
|
||||
try {
|
||||
const resDns = await resolver.resolveTxt(`${name}._nostr.${domain}`);
|
||||
return bech32ToHex(resDns[0][0]);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const VERIFICATION_CACHE_TIME = 24 * 60 * 60 * 1000;
|
||||
|
|
|
@ -22,6 +22,7 @@ import useEventPublisher from "Feed/EventPublisher";
|
|||
import { debounce } from "Util";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import SnortServiceProvider from "Nip05/SnortServiceProvider";
|
||||
import { mapEventToProfile, UserCache } from "Cache";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
|
@ -218,6 +219,10 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||
if (props.onSuccess) {
|
||||
props.onSuccess(nip05);
|
||||
}
|
||||
const newMeta = mapEventToProfile(ev);
|
||||
if (newMeta) {
|
||||
UserCache.set(newMeta);
|
||||
}
|
||||
if (helpText) {
|
||||
navigate("/settings");
|
||||
}
|
||||
|
|
|
@ -1,24 +1,39 @@
|
|||
import useEventFeed from "Feed/EventFeed";
|
||||
import { NostrLink } from "Util";
|
||||
import HyperText from "Element/HyperText";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { RawEvent } from "@snort/nostr";
|
||||
|
||||
import { findTag, NostrLink } from "Util";
|
||||
import useEventFeed from "Feed/EventFeed";
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
import Reveal from "Element/Reveal";
|
||||
import { MediaElement } from "Element/MediaElement";
|
||||
|
||||
export default function NostrFileHeader({ link }: { link: NostrLink }) {
|
||||
const ev = useEventFeed(link);
|
||||
|
||||
if (!ev.data?.length) return <Spinner />;
|
||||
if (!ev.data) return <PageSpinner />;
|
||||
return <NostrFileElement ev={ev.data} />;
|
||||
}
|
||||
|
||||
export function NostrFileElement({ ev }: { ev: RawEvent }) {
|
||||
// assume image or embed which can be rendered by the hypertext kind
|
||||
// todo: make use of hash
|
||||
// todo: use magnet or other links if present
|
||||
const u = ev.data?.[0]?.tags.find(a => a[0] === "u")?.[1] ?? "";
|
||||
if (u) {
|
||||
return <HyperText link={u} creator={ev.data?.[0]?.pubkey ?? ""} />;
|
||||
const u = findTag(ev, "url");
|
||||
const x = findTag(ev, "x");
|
||||
const m = findTag(ev, "m");
|
||||
const blurHash = findTag(ev, "blurhash");
|
||||
const magnet = findTag(ev, "magnet");
|
||||
|
||||
if (u && m) {
|
||||
return (
|
||||
<Reveal message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: u }} />}>
|
||||
<MediaElement mime={m} url={u} sha256={x} magnet={magnet} blurHash={blurHash} />
|
||||
</Reveal>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<b className="error">
|
||||
<FormattedMessage defaultMessage="Unknown file header: {name}" values={{ name: ev.data?.[0]?.content }} />
|
||||
<FormattedMessage defaultMessage="Unknown file header: {name}" values={{ name: ev.content }} />
|
||||
</b>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { EventKind, NostrPrefix } from "@snort/nostr";
|
||||
import { Link } from "react-router-dom";
|
||||
import { EventKind, NostrPrefix } from "@snort/nostr";
|
||||
|
||||
import Mention from "Element/Mention";
|
||||
import NostrFileHeader from "Element/NostrFileHeader";
|
||||
import { parseNostrLink } from "Util";
|
||||
import NoteQuote from "Element/NoteQuote";
|
||||
|
||||
export default function NostrLink({ link }: { link: string }) {
|
||||
export default function NostrLink({ link, depth }: { link: string; depth?: number }) {
|
||||
const nav = parseNostrLink(link);
|
||||
|
||||
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
|
||||
|
@ -14,12 +15,16 @@ export default function NostrLink({ link }: { link: string }) {
|
|||
if (nav.kind === EventKind.FileHeader) {
|
||||
return <NostrFileHeader link={nav} />;
|
||||
}
|
||||
const evLink = nav.encode();
|
||||
return (
|
||||
<Link to={`/e/${evLink}`} onClick={e => e.stopPropagation()} state={{ from: location.pathname }}>
|
||||
#{evLink.substring(0, 12)}
|
||||
</Link>
|
||||
);
|
||||
if ((depth ?? 0) > 0) {
|
||||
const evLink = nav.encode();
|
||||
return (
|
||||
<Link to={`/e/${evLink}`} onClick={e => e.stopPropagation()} state={{ from: location.pathname }}>
|
||||
#{evLink.substring(0, 12)}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return <NoteQuote link={nav} depth={depth} />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<a href={link} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
|
|
|
@ -57,6 +57,14 @@
|
|||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.note-quote {
|
||||
border: 1px solid var(--gray);
|
||||
}
|
||||
|
||||
.note-quote.note > .body {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.note > .body {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 24px;
|
||||
|
@ -98,9 +106,6 @@
|
|||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.light .note > .footer .ctx-menu {
|
||||
}
|
||||
|
||||
.note > .footer .ctx-menu li {
|
||||
background: #1e1e1e;
|
||||
padding-top: 8px;
|
||||
|
@ -263,26 +268,3 @@
|
|||
.close-menu-container {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.link-preview-container {
|
||||
border: 1px solid var(--gray);
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link-preview-container:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-preview-title {
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.link-preview-title > small {
|
||||
color: var(--font-secondary-color);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.link-preview-image {
|
||||
margin: 0 0 15px 0 !important;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import Poll from "Element/Poll";
|
|||
import { EventExt } from "System/EventExt";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setBookmarked, setPinned } from "Login";
|
||||
import { NostrFileElement } from "Element/NostrFileHeader";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
|
@ -38,6 +39,8 @@ export interface NoteProps {
|
|||
related: readonly TaggedRawEvent[];
|
||||
highlight?: boolean;
|
||||
ignoreModeration?: boolean;
|
||||
onClick?: (e: TaggedRawEvent) => void;
|
||||
depth?: number;
|
||||
options?: {
|
||||
showHeader?: boolean;
|
||||
showTime?: boolean;
|
||||
|
@ -70,8 +73,13 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
|||
};
|
||||
|
||||
export default function Note(props: NoteProps) {
|
||||
const navigate = useNavigate();
|
||||
const { data: ev, related, highlight, options: opt, ignoreModeration = false } = props;
|
||||
|
||||
if (ev.kind === EventKind.FileHeader) {
|
||||
return <NostrFileElement ev={ev} />;
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
|
||||
const { isMuted } = useModeration();
|
||||
|
@ -186,7 +194,7 @@ export default function Note(props: NoteProps) {
|
|||
</Reveal>
|
||||
);
|
||||
}
|
||||
return <Text content={body} tags={ev.tags} creator={ev.pubkey} />;
|
||||
return <Text content={body} tags={ev.tags} creator={ev.pubkey} depth={props.depth} />;
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -208,6 +216,11 @@ export default function Note(props: NoteProps) {
|
|||
}
|
||||
|
||||
e.stopPropagation();
|
||||
if (props.onClick) {
|
||||
props.onClick(eTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
const link = eventLink(eTarget.id, eTarget.relays);
|
||||
// detect cmd key and open in new tab
|
||||
if (e.metaKey) {
|
||||
|
@ -314,10 +327,9 @@ export default function Note(props: NoteProps) {
|
|||
{options.showHeader && (
|
||||
<div className="header flex">
|
||||
<ProfileImage
|
||||
autoWidth={false}
|
||||
pubkey={ev.pubkey}
|
||||
subHeader={replyTag() ?? undefined}
|
||||
linkToProfile={opt?.canClick === undefined}
|
||||
link={opt?.canClick === undefined ? undefined : ""}
|
||||
/>
|
||||
{(options.showTime || options.showBookmarked) && (
|
||||
<div className="info">
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
setActive,
|
||||
setPreview,
|
||||
setShowAdvanced,
|
||||
setSelectedCustomRelays,
|
||||
setZapForward,
|
||||
setSensitive,
|
||||
reset,
|
||||
|
@ -31,6 +32,10 @@ import messages from "./messages";
|
|||
import { ClipboardEventHandler, useState } from "react";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { EventBuilder } from "System";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { LoginStore } from "Login";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
interface NotePreviewProps {
|
||||
note: TaggedRawEvent;
|
||||
|
@ -52,10 +57,25 @@ export function NoteCreator() {
|
|||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
const { note, zapForward, sensitive, pollOptions, replyTo, otherEvents, preview, active, show, showAdvanced, error } =
|
||||
useSelector((s: RootState) => s.noteCreator);
|
||||
const {
|
||||
note,
|
||||
zapForward,
|
||||
sensitive,
|
||||
pollOptions,
|
||||
replyTo,
|
||||
otherEvents,
|
||||
preview,
|
||||
active,
|
||||
show,
|
||||
showAdvanced,
|
||||
selectedCustomRelays,
|
||||
error,
|
||||
} = useSelector((s: RootState) => s.noteCreator);
|
||||
const [uploadInProgress, setUploadInProgress] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
||||
const login = useLogin();
|
||||
const relays = login.relays;
|
||||
|
||||
async function sendNote() {
|
||||
if (note && publisher) {
|
||||
|
@ -92,10 +112,12 @@ export function NoteCreator() {
|
|||
return eb;
|
||||
};
|
||||
const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk);
|
||||
publisher.broadcast(ev);
|
||||
if (selectedCustomRelays) publisher.broadcastAll(ev, selectedCustomRelays);
|
||||
else publisher.broadcast(ev);
|
||||
dispatch(reset());
|
||||
for (const oe of otherEvents) {
|
||||
publisher.broadcast(oe);
|
||||
if (selectedCustomRelays) publisher.broadcastAll(oe, selectedCustomRelays);
|
||||
else publisher.broadcast(oe);
|
||||
}
|
||||
dispatch(reset());
|
||||
}
|
||||
|
@ -229,6 +251,53 @@ export function NoteCreator() {
|
|||
}
|
||||
}
|
||||
|
||||
function renderRelayCustomisation() {
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(relays.item || {})
|
||||
.filter(el => relays.item[el].write)
|
||||
.map((r, i, a) => (
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>{r}</div>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setSelectedCustomRelays(
|
||||
// set false if all relays selected
|
||||
e.target.checked && selectedCustomRelays && selectedCustomRelays.length == a.length - 1
|
||||
? false
|
||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||
a.filter(el =>
|
||||
el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function listAccounts() {
|
||||
return LoginStore.getSessions().map(a => (
|
||||
<MenuItem
|
||||
onClick={ev => {
|
||||
ev.stopPropagation = true;
|
||||
LoginStore.switchAccount(a);
|
||||
}}>
|
||||
<ProfileImage pubkey={a} link={""} />
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
|
||||
const handlePaste: ClipboardEventHandler<HTMLDivElement> = evt => {
|
||||
if (evt.clipboardData) {
|
||||
const clipboardItems = evt.clipboardData.items;
|
||||
|
@ -273,12 +342,23 @@ export function NoteCreator() {
|
|||
/>
|
||||
{renderPollOptions()}
|
||||
<div className="insert">
|
||||
{sub && (
|
||||
<Menu
|
||||
menuButton={
|
||||
<button>
|
||||
<Icon name="code-circle" />
|
||||
</button>
|
||||
}
|
||||
menuClassName="ctx-menu">
|
||||
{listAccounts()}
|
||||
</Menu>
|
||||
)}
|
||||
{pollOptions === undefined && !replyTo && (
|
||||
<button type="button" onClick={() => dispatch(setPollOptions(["A", "B"]))}>
|
||||
<button onClick={() => dispatch(setPollOptions(["A", "B"]))}>
|
||||
<Icon name="pie-chart" />
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={attachFile}>
|
||||
<button onClick={attachFile}>
|
||||
<Icon name="attachment" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -288,21 +368,28 @@ export function NoteCreator() {
|
|||
)}
|
||||
<div className="note-creator-actions">
|
||||
{uploadInProgress && <Spinner />}
|
||||
<button className="secondary" type="button" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
||||
<button className="secondary" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
||||
<FormattedMessage defaultMessage="Advanced" />
|
||||
</button>
|
||||
<button className="secondary" type="button" onClick={cancel}>
|
||||
<button className="secondary" onClick={cancel}>
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
</button>
|
||||
<button type="button" onClick={onSubmit}>
|
||||
<button onClick={onSubmit}>
|
||||
{replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
|
||||
</button>
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<div>
|
||||
<button className="secondary" type="button" onClick={loadPreview}>
|
||||
<button className="secondary" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
</button>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Custom Relays" />
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
||||
</p>
|
||||
{renderRelayCustomisation()}
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Forward Zaps" />
|
||||
</h4>
|
||||
|
|
|
@ -12,12 +12,18 @@ import { formatShort } from "Number";
|
|||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { bech32ToHex, delay, normalizeReaction, unwrap } from "Util";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import { ReBroadcaster } from "Element/ReBroadcaster";
|
||||
import Reactions from "Element/Reactions";
|
||||
import SendSats from "Element/SendSats";
|
||||
import { ParsedZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { RootState } from "State/Store";
|
||||
import { setReplyTo, setShow, reset } from "State/NoteCreator";
|
||||
import {
|
||||
setNote as setReBroadcastNote,
|
||||
setShow as setReBroadcastShow,
|
||||
reset as resetReBroadcast,
|
||||
} from "State/ReBroadcast";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { SnortPubKey, TranslateHost } from "Const";
|
||||
import { LNURL } from "LNURL";
|
||||
|
@ -25,42 +31,10 @@ import { DonateLNURL } from "Pages/DonatePage";
|
|||
import { useWallet } from "Wallet";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setBookmarked, setPinned } from "Login";
|
||||
import { useInteractionCache } from "Hooks/useInteractionCache";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
// a dumb cache to remember which notes we zapped
|
||||
class DumbZapCache {
|
||||
#set: Set<u256> = new Set();
|
||||
constructor() {
|
||||
this.#load();
|
||||
}
|
||||
|
||||
add(id: u256) {
|
||||
this.#set.add(this.#truncId(id));
|
||||
this.#save();
|
||||
}
|
||||
|
||||
has(id: u256) {
|
||||
return this.#set.has(this.#truncId(id));
|
||||
}
|
||||
|
||||
#truncId(id: u256) {
|
||||
return id.slice(0, 12);
|
||||
}
|
||||
|
||||
#save() {
|
||||
window.localStorage.setItem("zap-cache", JSON.stringify([...this.#set]));
|
||||
}
|
||||
|
||||
#load() {
|
||||
const data = window.localStorage.getItem("zap-cache");
|
||||
if (data) {
|
||||
this.#set = new Set<u256>(JSON.parse(data) as Array<u256>);
|
||||
}
|
||||
}
|
||||
}
|
||||
const ZapCache = new DumbZapCache();
|
||||
|
||||
let isZapperBusy = false;
|
||||
const barrierZapper = async <T,>(then: () => Promise<T>): Promise<T> => {
|
||||
while (isZapperBusy) {
|
||||
|
@ -99,10 +73,14 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
const { pinned, bookmarked, publicKey, preferences: prefs, relays } = login;
|
||||
const { mute, block } = useModeration();
|
||||
const author = useUserProfile(ev.pubkey);
|
||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||
const publisher = useEventPublisher();
|
||||
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
|
||||
const showReBroadcastModal = useSelector((s: RootState) => s.reBroadcast.show);
|
||||
const reBroadcastNote = useSelector((s: RootState) => s.reBroadcast.note);
|
||||
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
||||
const willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
|
||||
const willRenderReBroadcast = showReBroadcastModal && reBroadcastNote && reBroadcastNote?.id === ev.id;
|
||||
const [tip, setTip] = useState(false);
|
||||
const [zapping, setZapping] = useState(false);
|
||||
const walletState = useWallet();
|
||||
|
@ -114,7 +92,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
type: "language",
|
||||
});
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const didZap = ZapCache.has(ev.id) || zaps.some(a => a.sender === publicKey);
|
||||
const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey);
|
||||
const longPress = useLongPress(
|
||||
e => {
|
||||
e.stopPropagation();
|
||||
|
@ -126,17 +104,21 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
);
|
||||
|
||||
function hasReacted(emoji: string) {
|
||||
return positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey);
|
||||
return (
|
||||
interactionCache.data.reacted ||
|
||||
positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey)
|
||||
);
|
||||
}
|
||||
|
||||
function hasReposted() {
|
||||
return reposts.some(a => a.pubkey === publicKey);
|
||||
return interactionCache.data.reposted || reposts.some(a => a.pubkey === publicKey);
|
||||
}
|
||||
|
||||
async function react(content: string) {
|
||||
if (!hasReacted(content) && publisher) {
|
||||
const evLike = await publisher.react(ev, content);
|
||||
publisher.broadcast(evLike);
|
||||
await interactionCache.react();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,6 +134,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
|
||||
const evRepost = await publisher.repost(ev);
|
||||
publisher.broadcast(evRepost);
|
||||
await interactionCache.repost();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +184,8 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
const zap = handler.canZap && publisher ? await publisher.zap(amount * 1000, key, zr, id) : undefined;
|
||||
const invoice = await handler.getInvoice(amount, undefined, zap);
|
||||
await wallet?.payInvoice(unwrap(invoice.pr));
|
||||
|
||||
await interactionCache.zap();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -220,14 +205,13 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (prefs.autoZap && !ZapCache.has(ev.id) && !isMine && !zapping) {
|
||||
if (prefs.autoZap && !didZap && !isMine && !zapping) {
|
||||
const lnurl = getLNURL();
|
||||
if (wallet?.isReady() && lnurl) {
|
||||
setZapping(true);
|
||||
queueMicrotask(async () => {
|
||||
try {
|
||||
await fastZapInner(lnurl, prefs.defaultZapAmount, ev.pubkey, ev.id);
|
||||
ZapCache.add(ev.id);
|
||||
fastZapDonate();
|
||||
} catch {
|
||||
// ignored
|
||||
|
@ -275,7 +259,6 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
<Icon name="heart" />
|
||||
<div className="reaction-pill-number">{formatShort(positive.length)}</div>
|
||||
</div>
|
||||
{repostIcon()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -387,10 +370,18 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
<FormattedMessage {...messages.DislikeAction} />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => block(ev.pubkey)}>
|
||||
<Icon name="block" />
|
||||
<FormattedMessage {...messages.Block} />
|
||||
</MenuItem>
|
||||
{ev.pubkey === publicKey && (
|
||||
<MenuItem onClick={handleReBroadcastButtonClick}>
|
||||
<Icon name="relay" />
|
||||
<FormattedMessage {...messages.ReBroadcast} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{ev.pubkey !== publicKey && (
|
||||
<MenuItem onClick={() => block(ev.pubkey)}>
|
||||
<Icon name="block" />
|
||||
<FormattedMessage {...messages.Block} />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => translate()}>
|
||||
<Icon name="translate" />
|
||||
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
|
||||
|
@ -420,12 +411,22 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
dispatch(setShow(!showNoteCreatorModal));
|
||||
};
|
||||
|
||||
const handleReBroadcastButtonClick = () => {
|
||||
if (reBroadcastNote?.id !== ev.id) {
|
||||
dispatch(resetReBroadcast());
|
||||
}
|
||||
|
||||
dispatch(setReBroadcastNote(ev));
|
||||
dispatch(setReBroadcastShow(!showReBroadcastModal));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="footer">
|
||||
<div className="footer-reactions">
|
||||
{tipButton()}
|
||||
{reactionIcons()}
|
||||
{repostIcon()}
|
||||
<div className={`reaction-pill ${showNoteCreatorModal ? "reacted" : ""}`} onClick={handleReplyButtonClick}>
|
||||
<Icon name="reply" size={17} />
|
||||
</div>
|
||||
|
@ -440,6 +441,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||
</Menu>
|
||||
</div>
|
||||
{willRenderNoteCreator && <NoteCreator />}
|
||||
{willRenderReBroadcast && <ReBroadcaster />}
|
||||
<Reactions
|
||||
show={showReactions}
|
||||
setShow={setShowReactions}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import useEventFeed from "Feed/EventFeed";
|
||||
import { NostrLink } from "Util";
|
||||
import Note from "Element/Note";
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
|
||||
export default function NoteQuote({ link, depth }: { link: NostrLink; depth?: number }) {
|
||||
const ev = useEventFeed(link);
|
||||
if (!ev.data) return <PageSpinner />;
|
||||
return (
|
||||
<Note
|
||||
data={ev.data}
|
||||
related={[]}
|
||||
className="note-quote"
|
||||
depth={(depth ?? 0) + 1}
|
||||
options={{
|
||||
showFooter: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
.pfp {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pfp .avatar-wrapper {
|
||||
|
@ -14,7 +18,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pfp a {
|
||||
a.pfp {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -25,6 +29,10 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pfp .subheader .about {
|
||||
max-width: calc(100vw - 140px);
|
||||
.pfp .profile-name {
|
||||
max-width: stretch;
|
||||
}
|
||||
|
||||
.pfp a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import "./ProfileImage.css";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { HexKey, NostrPrefix } from "@snort/nostr";
|
||||
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { hexToBech32, profileLink } from "Util";
|
||||
import Avatar from "Element/Avatar";
|
||||
import Nip05 from "Element/Nip05";
|
||||
import { HexKey, NostrPrefix } from "@snort/nostr";
|
||||
import { MetadataCache } from "Cache";
|
||||
import usePageWidth from "Hooks/usePageWidth";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export interface ProfileImageProps {
|
||||
pubkey: HexKey;
|
||||
|
@ -16,10 +16,8 @@ export interface ProfileImageProps {
|
|||
showUsername?: boolean;
|
||||
className?: string;
|
||||
link?: string;
|
||||
autoWidth?: boolean;
|
||||
defaultNip?: string;
|
||||
verifyNip?: boolean;
|
||||
linkToProfile?: boolean;
|
||||
overrideUsername?: string;
|
||||
}
|
||||
|
||||
|
@ -29,58 +27,40 @@ export default function ProfileImage({
|
|||
showUsername = true,
|
||||
className,
|
||||
link,
|
||||
autoWidth = true,
|
||||
defaultNip,
|
||||
verifyNip,
|
||||
linkToProfile = true,
|
||||
overrideUsername,
|
||||
}: ProfileImageProps) {
|
||||
const navigate = useNavigate();
|
||||
const user = useUserProfile(pubkey);
|
||||
const nip05 = defaultNip ? defaultNip : user?.nip05;
|
||||
const width = usePageWidth();
|
||||
|
||||
const name = useMemo(() => {
|
||||
return overrideUsername ?? getDisplayName(user, pubkey);
|
||||
}, [user, pubkey, overrideUsername]);
|
||||
|
||||
if (!pubkey && !link) {
|
||||
link = "#";
|
||||
}
|
||||
|
||||
const onAvatarClick = () => {
|
||||
if (linkToProfile) {
|
||||
navigate(link ?? profileLink(pubkey));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`pfp f-ellipsis${className ? ` ${className}` : ""}`}>
|
||||
<Link className={`pfp${className ? ` ${className}` : ""}`} to={link === undefined ? profileLink(pubkey) : link}>
|
||||
<div className="avatar-wrapper">
|
||||
<Avatar user={user} onClick={onAvatarClick} />
|
||||
<Avatar user={user} />
|
||||
</div>
|
||||
{showUsername && (
|
||||
<div className="profile-name">
|
||||
<div className="f-ellipsis">
|
||||
<div className="username">
|
||||
<Link className="display-name" key={pubkey} to={link ?? profileLink(pubkey)}>
|
||||
<div>{name.trim()}</div>
|
||||
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} verifyNip={verifyNip} />}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="subheader" style={{ width: autoWidth ? width - 190 : "" }}>
|
||||
{subHeader}
|
||||
<div>{name.trim()}</div>
|
||||
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} verifyNip={verifyNip} />}
|
||||
</div>
|
||||
<div className="subheader">{subHeader}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function getDisplayName(user: MetadataCache | undefined, pubkey: HexKey) {
|
||||
let name = hexToBech32(NostrPrefix.PublicKey, pubkey).substring(0, 12);
|
||||
if (user?.display_name !== undefined && user.display_name.length > 0) {
|
||||
if (typeof user?.display_name === "string" && user.display_name.length > 0) {
|
||||
name = user.display_name;
|
||||
} else if (user?.name !== undefined && user.name.length > 0) {
|
||||
} else if (typeof user?.name === "string" && user.name.length > 0) {
|
||||
name = user.name;
|
||||
}
|
||||
return name;
|
||||
|
|
|
@ -17,8 +17,8 @@ export interface ProfilePreviewProps {
|
|||
}
|
||||
export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||
const pubkey = props.pubkey;
|
||||
const user = useUserProfile(pubkey);
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const user = useUserProfile(inView ? pubkey : undefined);
|
||||
const options = {
|
||||
about: true,
|
||||
...props.options,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import useImgProxy from "Hooks/useImgProxy";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {
|
||||
size?: number;
|
||||
|
@ -8,15 +9,36 @@ interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<
|
|||
export const ProxyImg = (props: ProxyImgProps) => {
|
||||
const { src, size, ...rest } = props;
|
||||
const [url, setUrl] = useState<string>();
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [bypass, setBypass] = useState(false);
|
||||
const { proxy } = useImgProxy();
|
||||
|
||||
useEffect(() => {
|
||||
if (src) {
|
||||
proxy(src, size)
|
||||
.then(a => setUrl(a))
|
||||
.catch(console.warn);
|
||||
const url = proxy(src, size);
|
||||
setUrl(url);
|
||||
}
|
||||
}, [src]);
|
||||
|
||||
return <img src={url} {...rest} />;
|
||||
if (loadFailed) {
|
||||
if (bypass) {
|
||||
return <img src={src} {...rest} />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="note-invoice error"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setBypass(true);
|
||||
}}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to proxy image from {host}, click here to load directly"
|
||||
values={{
|
||||
host: new URL(src ?? "").hostname,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <img src={url} {...rest} onError={() => setLoadFailed(true)} />;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { FormattedMessage } from "react-intl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import Modal from "Element/Modal";
|
||||
import type { RootState } from "State/Store";
|
||||
import { setShow, reset, setSelectedCustomRelays } from "State/ReBroadcast";
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export function ReBroadcaster() {
|
||||
const publisher = useEventPublisher();
|
||||
const { note, show, selectedCustomRelays } = useSelector((s: RootState) => s.reBroadcast);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function sendReBroadcast() {
|
||||
if (note && publisher) {
|
||||
if (selectedCustomRelays) publisher.broadcastAll(note, selectedCustomRelays);
|
||||
else publisher.broadcast(note);
|
||||
dispatch(reset());
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
dispatch(reset());
|
||||
}
|
||||
|
||||
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||
ev.stopPropagation();
|
||||
sendReBroadcast().catch(console.warn);
|
||||
}
|
||||
|
||||
const login = useLogin();
|
||||
const relays = login.relays;
|
||||
|
||||
function renderRelayCustomisation() {
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(relays.item || {})
|
||||
.filter(el => relays.item[el].write)
|
||||
.map((r, i, a) => (
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>{r}</div>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setSelectedCustomRelays(
|
||||
// set false if all relays selected
|
||||
e.target.checked && selectedCustomRelays && selectedCustomRelays.length == a.length - 1
|
||||
? false
|
||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||
a.filter(el =>
|
||||
el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{show && (
|
||||
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
||||
{renderRelayCustomisation()}
|
||||
<div className="note-creator-actions">
|
||||
<button className="secondary" onClick={cancel}>
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
</button>
|
||||
<button onClick={onSubmit}>
|
||||
<FormattedMessage {...messages.ReBroadcast} />
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,27 +1,23 @@
|
|||
.reactions-modal .modal-body {
|
||||
padding: 0;
|
||||
max-width: 586px;
|
||||
}
|
||||
|
||||
.reactions-view {
|
||||
padding: 24px 32px;
|
||||
background-color: #1b1b1b;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
min-height: 33vh;
|
||||
}
|
||||
|
||||
.light .reactions-view {
|
||||
.light .reactions-modal .modal-body {
|
||||
background-color: var(--note-bg);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.reactions-view {
|
||||
.reactions-modal .modal-body {
|
||||
padding: 12px 16px;
|
||||
margin-top: -160px;
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
.reactions-view .close {
|
||||
.reactions-modal .modal-body .close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
|
@ -29,18 +25,18 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reactions-view .close:hover {
|
||||
.reactions-modal .modal-body .close:hover {
|
||||
color: var(--font-tertiary-color);
|
||||
}
|
||||
|
||||
.reactions-view .reactions-header {
|
||||
.reactions-modal .modal-body .reactions-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.reactions-view .reactions-header h2 {
|
||||
.reactions-modal .modal-body .reactions-header h2 {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
font-weight: 600;
|
||||
|
@ -48,26 +44,25 @@
|
|||
line-height: 19px;
|
||||
}
|
||||
|
||||
.reactions-view .body {
|
||||
.reactions-modal .modal-body .body {
|
||||
overflow: scroll;
|
||||
height: 320px;
|
||||
height: 40vh;
|
||||
-ms-overflow-style: none; /* for Internet Explorer, Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.reactions-view .body::-webkit-scrollbar {
|
||||
.reactions-modal .modal-body .body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reactions-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: grid;
|
||||
grid-template-columns: 52px auto;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.reactions-item .reaction-icon {
|
||||
width: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -92,12 +87,8 @@
|
|||
line-height: 17px;
|
||||
}
|
||||
|
||||
.reactions-item .zap-comment {
|
||||
width: 332px;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.reactions-view .tab.disabled {
|
||||
.reactions-modal .modal-body .tab.disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import "./Reactions.css";
|
|||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import { formatShort } from "Number";
|
||||
|
@ -75,72 +74,66 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
|
|||
|
||||
return show ? (
|
||||
<Modal className="reactions-modal" onClose={onClose}>
|
||||
<div className="reactions-view">
|
||||
<div className="close" onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
</div>
|
||||
<div className="reactions-header">
|
||||
<h2>
|
||||
<FormattedMessage {...messages.ReactionsCount} values={{ n: total }} />
|
||||
</h2>
|
||||
</div>
|
||||
<Tabs tabs={tabs} tab={tab} setTab={setTab} />
|
||||
<div className="body" key={tab.value}>
|
||||
{tab.value === 0 &&
|
||||
likes.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">{ev.content === "+" ? <Icon name="heart" /> : ev.content}</div>
|
||||
<ProfileImage autoWidth={false} pubkey={ev.pubkey} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{tab.value === 1 &&
|
||||
zaps.map(z => {
|
||||
return (
|
||||
z.sender && (
|
||||
<div key={z.id} className="reactions-item">
|
||||
<div className="zap-reaction-icon">
|
||||
<Icon name="zap" size={20} />
|
||||
<span className="zap-amount">{formatShort(z.amount)}</span>
|
||||
</div>
|
||||
<ProfileImage
|
||||
autoWidth={false}
|
||||
pubkey={z.anonZap ? "" : z.sender}
|
||||
subHeader={
|
||||
<div className="f-ellipsis zap-comment" title={z.content}>
|
||||
{z.content}
|
||||
</div>
|
||||
}
|
||||
overrideUsername={z.anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
|
||||
/>
|
||||
<div className="close" onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
</div>
|
||||
<div className="reactions-header">
|
||||
<h2>
|
||||
<FormattedMessage {...messages.ReactionsCount} values={{ n: total }} />
|
||||
</h2>
|
||||
</div>
|
||||
<Tabs tabs={tabs} tab={tab} setTab={setTab} />
|
||||
<div className="body" key={tab.value}>
|
||||
{tab.value === 0 &&
|
||||
likes.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">{ev.content === "+" ? <Icon name="heart" /> : ev.content}</div>
|
||||
<ProfileImage pubkey={ev.pubkey} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{tab.value === 1 &&
|
||||
zaps.map(z => {
|
||||
return (
|
||||
z.sender && (
|
||||
<div key={z.id} className="reactions-item">
|
||||
<div className="zap-reaction-icon">
|
||||
<Icon name="zap" size={20} />
|
||||
<span className="zap-amount">{formatShort(z.amount)}</span>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
{tab.value === 2 &&
|
||||
reposts.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">
|
||||
<Icon name="repost" size={16} />
|
||||
</div>
|
||||
<ProfileImage autoWidth={false} pubkey={ev.pubkey} />
|
||||
<ProfileImage
|
||||
pubkey={z.anonZap ? "" : z.sender}
|
||||
subHeader={<div title={z.content}>{z.content}</div>}
|
||||
link={z.anonZap ? "" : undefined}
|
||||
overrideUsername={z.anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{tab.value === 3 &&
|
||||
dislikes.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">
|
||||
<Icon name="dislike" />
|
||||
</div>
|
||||
<ProfileImage autoWidth={false} pubkey={ev.pubkey} />
|
||||
)
|
||||
);
|
||||
})}
|
||||
{tab.value === 2 &&
|
||||
reposts.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">
|
||||
<Icon name="repost" size={16} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ProfileImage pubkey={ev.pubkey} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{tab.value === 3 &&
|
||||
dislikes.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item f-ellipsis">
|
||||
<div className="reaction-icon">
|
||||
<Icon name="dislike" />
|
||||
</div>
|
||||
<ProfileImage pubkey={ev.pubkey} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
) : null;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import MediaLink from "Element/MediaLink";
|
||||
import { FileExtensionRegex } from "Const";
|
||||
import Reveal from "Element/Reveal";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { MediaElement } from "Element/MediaElement";
|
||||
|
||||
interface RevealMediaProps {
|
||||
creator: string;
|
||||
|
@ -18,14 +19,42 @@ export default function RevealMedia(props: RevealMediaProps) {
|
|||
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows);
|
||||
const hostname = new URL(props.link).hostname;
|
||||
|
||||
const url = new URL(props.link);
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
const type = (() => {
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "jfif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp":
|
||||
return "image";
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg":
|
||||
return "audio";
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm":
|
||||
return "video";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
|
||||
if (hideMedia) {
|
||||
return (
|
||||
<Reveal
|
||||
message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />}>
|
||||
<MediaLink link={props.link} />
|
||||
<MediaElement mime={`${type}/${extension}`} url={url.toString()} />
|
||||
</Reveal>
|
||||
);
|
||||
} else {
|
||||
return <MediaLink link={props.link} />;
|
||||
return <MediaElement mime={`${type}/${extension}`} url={url.toString()} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.text a {
|
||||
.text > a {
|
||||
color: var(--highlight);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
|||
|
||||
.text pre {
|
||||
margin: 0;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.text li {
|
||||
|
|
|
@ -6,12 +6,13 @@ import { visit, SKIP } from "unist-util-visit";
|
|||
import * as unist from "unist";
|
||||
import { HexKey, NostrPrefix } from "@snort/nostr";
|
||||
|
||||
import { MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
|
||||
import { eventLink, hexToBech32, splitByUrl, unwrap } from "Util";
|
||||
import { MentionRegex, InvoiceRegex, HashtagRegex, CashuRegex } from "Const";
|
||||
import { eventLink, hexToBech32, splitByUrl, unwrap, validateNostrLink } from "Util";
|
||||
import Invoice from "Element/Invoice";
|
||||
import Hashtag from "Element/Hashtag";
|
||||
import Mention from "Element/Mention";
|
||||
import HyperText from "Element/HyperText";
|
||||
import CashuNuts from "Element/CashuNuts";
|
||||
|
||||
export type Fragment = string | React.ReactNode;
|
||||
|
||||
|
@ -25,9 +26,10 @@ export interface TextProps {
|
|||
creator: HexKey;
|
||||
tags: Array<Array<string>>;
|
||||
disableMedia?: boolean;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export default function Text({ content, tags, creator, disableMedia }: TextProps) {
|
||||
export default function Text({ content, tags, creator, disableMedia, depth }: TextProps) {
|
||||
const location = useLocation();
|
||||
|
||||
function extractLinks(fragments: Fragment[]) {
|
||||
|
@ -35,7 +37,21 @@ export default function Text({ content, tags, creator, disableMedia }: TextProps
|
|||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return splitByUrl(f).map(a => {
|
||||
if (a.match(/^(?:https?|(?:web\+)?nostr|magnet):/i)) {
|
||||
const validateLink = () => {
|
||||
const normalizedStr = a.toLowerCase();
|
||||
|
||||
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
|
||||
return validateNostrLink(normalizedStr);
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedStr.startsWith("http:") ||
|
||||
normalizedStr.startsWith("https:") ||
|
||||
normalizedStr.startsWith("magnet:")
|
||||
);
|
||||
};
|
||||
|
||||
if (validateLink()) {
|
||||
if (disableMedia ?? false) {
|
||||
return (
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
|
@ -43,7 +59,7 @@ export default function Text({ content, tags, creator, disableMedia }: TextProps
|
|||
</a>
|
||||
);
|
||||
}
|
||||
return <HyperText link={a} creator={creator} />;
|
||||
return <HyperText link={a} creator={creator} depth={depth} />;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
|
@ -53,6 +69,19 @@ export default function Text({ content, tags, creator, disableMedia }: TextProps
|
|||
.flat();
|
||||
}
|
||||
|
||||
function extractCashuTokens(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string" && f.includes("cashuA")) {
|
||||
return f.split(CashuRegex).map(a => {
|
||||
return <CashuNuts token={a} />;
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractMentions(frag: TextFragment) {
|
||||
return frag.body
|
||||
.map(f => {
|
||||
|
@ -148,6 +177,7 @@ export default function Text({ content, tags, creator, disableMedia }: TextProps
|
|||
fragments = extractLinks(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
fragments = extractCashuTokens(fragments);
|
||||
return fragments;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ import "./Thread.css";
|
|||
import { useMemo, useState, ReactNode } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useNavigate, useLocation, Link, useParams } from "react-router-dom";
|
||||
import { TaggedRawEvent, u256, EventKind } from "@snort/nostr";
|
||||
import { TaggedRawEvent, u256, EventKind, NostrPrefix } from "@snort/nostr";
|
||||
import { EventExt, Thread as ThreadInfo } from "System/EventExt";
|
||||
|
||||
import { eventLink, unwrap, getReactions, parseNostrLink, getAllReactions } from "Util";
|
||||
import { eventLink, unwrap, getReactions, parseNostrLink, getAllReactions, findTag } from "Util";
|
||||
import BackButton from "Element/BackButton";
|
||||
import Note from "Element/Note";
|
||||
import NoteGhost from "Element/NoteGhost";
|
||||
|
@ -29,12 +29,11 @@ const Divider = ({ variant = "regular" }: DividerProps) => {
|
|||
|
||||
interface SubthreadProps {
|
||||
isLastSubthread?: boolean;
|
||||
from: u256;
|
||||
active: u256;
|
||||
notes: readonly TaggedRawEvent[];
|
||||
related: readonly TaggedRawEvent[];
|
||||
chains: Map<u256, Array<TaggedRawEvent>>;
|
||||
onNavigate: (e: u256) => void;
|
||||
onNavigate: (e: TaggedRawEvent) => void;
|
||||
}
|
||||
|
||||
const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
|
@ -51,6 +50,7 @@ const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProp
|
|||
data={a}
|
||||
key={a.id}
|
||||
related={related}
|
||||
onClick={onNavigate}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
|
@ -58,7 +58,6 @@ const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProp
|
|||
<TierTwo
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
from={a.id}
|
||||
notes={replies}
|
||||
related={related}
|
||||
chains={chains}
|
||||
|
@ -77,7 +76,7 @@ interface ThreadNoteProps extends Omit<SubthreadProps, "notes"> {
|
|||
isLast: boolean;
|
||||
}
|
||||
|
||||
const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chains, onNavigate }: ThreadNoteProps) => {
|
||||
const ThreadNote = ({ active, note, isLast, isLastSubthread, related, chains, onNavigate }: ThreadNoteProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const replies = getReplies(note.id, chains);
|
||||
const activeInReplies = replies.map(r => r.id).includes(active);
|
||||
|
@ -95,6 +94,7 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chai
|
|||
data={note}
|
||||
key={note.id}
|
||||
related={related}
|
||||
onClick={onNavigate}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
|
@ -103,7 +103,6 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chai
|
|||
<TierThree
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
from={from}
|
||||
notes={replies}
|
||||
related={related}
|
||||
chains={chains}
|
||||
|
@ -115,14 +114,13 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chai
|
|||
);
|
||||
};
|
||||
|
||||
const TierTwo = ({ active, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
const TierTwo = ({ active, isLastSubthread, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThreadNote
|
||||
active={active}
|
||||
from={from}
|
||||
onNavigate={onNavigate}
|
||||
note={first}
|
||||
chains={chains}
|
||||
|
@ -136,7 +134,6 @@ const TierTwo = ({ active, isLastSubthread, from, notes, related, chains, onNavi
|
|||
return (
|
||||
<ThreadNote
|
||||
active={active}
|
||||
from={from}
|
||||
onNavigate={onNavigate}
|
||||
note={r}
|
||||
chains={chains}
|
||||
|
@ -150,7 +147,7 @@ const TierTwo = ({ active, isLastSubthread, from, notes, related, chains, onNavi
|
|||
);
|
||||
};
|
||||
|
||||
const TierThree = ({ active, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
const replies = getReplies(first.id, chains);
|
||||
const hasMultipleNotes = rest.length > 0 || replies.length > 0;
|
||||
|
@ -176,7 +173,6 @@ const TierThree = ({ active, isLastSubthread, from, notes, related, chains, onNa
|
|||
<TierThree
|
||||
active={active}
|
||||
isLastSubthread={isLastSubthread}
|
||||
from={from}
|
||||
notes={replies}
|
||||
related={related}
|
||||
chains={chains}
|
||||
|
@ -200,6 +196,7 @@ const TierThree = ({ active, isLastSubthread, from, notes, related, chains, onNa
|
|||
data={r}
|
||||
key={r.id}
|
||||
related={related}
|
||||
onClick={onNavigate}
|
||||
/>
|
||||
<div className="line-container"></div>
|
||||
</div>
|
||||
|
@ -213,7 +210,7 @@ export default function Thread() {
|
|||
const params = useParams();
|
||||
const location = useLocation();
|
||||
|
||||
const link = parseNostrLink(params.id ?? "");
|
||||
const link = parseNostrLink(params.id ?? "", NostrPrefix.Note);
|
||||
const thread = useThreadFeed(unwrap(link));
|
||||
|
||||
const [currentId, setCurrentId] = useState(link?.id);
|
||||
|
@ -222,6 +219,11 @@ export default function Thread() {
|
|||
const isSingleNote = thread.data?.filter(a => a.kind === EventKind.TextNote).length === 1;
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
function navigateThread(e: TaggedRawEvent) {
|
||||
setCurrentId(e.id);
|
||||
//const link = encodeTLV(e.id, NostrPrefix.Event, e.relays);
|
||||
}
|
||||
|
||||
const chains = useMemo(() => {
|
||||
const chains = new Map<u256, Array<TaggedRawEvent>>();
|
||||
if (thread.data) {
|
||||
|
@ -229,16 +231,20 @@ export default function Thread() {
|
|||
?.filter(a => a.kind === EventKind.TextNote)
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
.forEach(v => {
|
||||
const thread = EventExt.extractThread(v);
|
||||
const replyTo = thread?.replyTo?.Event ?? thread?.root?.Event;
|
||||
const t = EventExt.extractThread(v);
|
||||
let replyTo = t?.replyTo?.Event ?? t?.root?.Event;
|
||||
if (t?.root?.ATag) {
|
||||
const parsed = t.root.ATag.split(":");
|
||||
replyTo = thread.data?.find(
|
||||
a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2]
|
||||
)?.id;
|
||||
}
|
||||
if (replyTo) {
|
||||
if (!chains.has(replyTo)) {
|
||||
chains.set(replyTo, [v]);
|
||||
} else {
|
||||
unwrap(chains.get(replyTo)).push(v);
|
||||
}
|
||||
} else if (v.tags.length > 0) {
|
||||
//console.log("Not replying to anything: ", v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -257,11 +263,19 @@ export default function Thread() {
|
|||
if (isRoot(currentThread)) {
|
||||
return currentNote;
|
||||
}
|
||||
const replyTo = currentThread?.replyTo?.Event ?? currentThread?.root?.Event;
|
||||
const replyTo = currentThread?.replyTo ?? currentThread?.root;
|
||||
|
||||
// sometimes the root event ID is missing, and we can only take the happy path if the root event ID exists
|
||||
if (replyTo) {
|
||||
return thread.data?.find(a => a.id === replyTo);
|
||||
if (replyTo.ATag) {
|
||||
const parsed = replyTo.ATag.split(":");
|
||||
return thread.data?.find(
|
||||
a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2]
|
||||
);
|
||||
}
|
||||
if (replyTo.Event) {
|
||||
return thread.data?.find(a => a.id === replyTo.Event);
|
||||
}
|
||||
}
|
||||
|
||||
const possibleRoots = thread.data?.filter(a => {
|
||||
|
@ -284,7 +298,7 @@ export default function Thread() {
|
|||
const parent = useMemo(() => {
|
||||
if (root) {
|
||||
const currentThread = EventExt.extractThread(root);
|
||||
return currentThread?.replyTo?.Event ?? currentThread?.root?.Event;
|
||||
return currentThread?.replyTo?.Event ?? currentThread?.root?.Event ?? currentThread?.root?.ATag;
|
||||
}
|
||||
}, [root]);
|
||||
|
||||
|
@ -300,6 +314,7 @@ export default function Thread() {
|
|||
data={note}
|
||||
related={getReactions(thread.data, note.id)}
|
||||
options={{ showReactionsLink: true }}
|
||||
onClick={navigateThread}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -316,16 +331,13 @@ export default function Thread() {
|
|||
return (
|
||||
<Subthread
|
||||
active={currentId}
|
||||
from={from}
|
||||
notes={replies}
|
||||
related={getAllReactions(
|
||||
thread.data,
|
||||
replies.map(a => a.id)
|
||||
)}
|
||||
chains={chains}
|
||||
onNavigate={() => {
|
||||
//nothing
|
||||
}}
|
||||
onNavigate={navigateThread}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ export interface TimelineProps {
|
|||
window?: number;
|
||||
relay?: string;
|
||||
now?: number;
|
||||
loadMore?: boolean;
|
||||
noSort?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,8 +47,9 @@ const Timeline = (props: TimelineProps) => {
|
|||
|
||||
const filterPosts = useCallback(
|
||||
(nts: readonly TaggedRawEvent[]) => {
|
||||
return [...nts]
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
const a = [...nts];
|
||||
props.noSort || a.sort((a, b) => b.created_at - a.created_at);
|
||||
return a
|
||||
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
|
||||
.filter(a => props.ignoreModeration || !isMuted(a.pubkey));
|
||||
},
|
||||
|
@ -87,7 +90,9 @@ const Timeline = (props: TimelineProps) => {
|
|||
if (eRef) {
|
||||
return <NoteReaction data={e} key={e.id} root={findRelated(eRef)} />;
|
||||
}
|
||||
return <Note key={e.id} data={e} related={relatedFeed(e.id)} ignoreModeration={props.ignoreModeration} />;
|
||||
return (
|
||||
<Note key={e.id} data={e} related={relatedFeed(e.id)} ignoreModeration={props.ignoreModeration} depth={0} />
|
||||
);
|
||||
}
|
||||
case EventKind.ZapReceipt: {
|
||||
const zap = parseZap(e);
|
||||
|
@ -109,12 +114,12 @@ const Timeline = (props: TimelineProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<>
|
||||
{latestFeed.length > 0 && (
|
||||
<>
|
||||
<div className="card latest-notes pointer" onClick={() => onShowLatest()} ref={ref}>
|
||||
{latestAuthors.slice(0, 3).map(p => {
|
||||
return <ProfileImage pubkey={p} showUsername={false} linkToProfile={false} />;
|
||||
return <ProfileImage pubkey={p} showUsername={false} link={""} />;
|
||||
})}
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
|
||||
|
@ -125,7 +130,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} linkToProfile={false} />;
|
||||
return <ProfileImage pubkey={p} showUsername={false} link={""} />;
|
||||
})}
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
|
||||
|
@ -137,12 +142,14 @@ const Timeline = (props: TimelineProps) => {
|
|||
</>
|
||||
)}
|
||||
{mainFeed.map(eventElement)}
|
||||
<LoadMore onLoadMore={feed.loadMore} shouldLoadMore={!feed.loading}>
|
||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
||||
</LoadMore>
|
||||
</div>
|
||||
{(props.loadMore === undefined || props.loadMore === true) && (
|
||||
<LoadMore onLoadMore={feed.loadMore} shouldLoadMore={!feed.loading}>
|
||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
||||
</LoadMore>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Timeline;
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
import Note from "Element/Note";
|
||||
import NostrBandApi from "NostrBand";
|
||||
|
||||
export default function TrendingNotes() {
|
||||
const [posts, setPosts] = useState<Array<RawEvent>>();
|
||||
|
||||
async function loadTrendingNotes() {
|
||||
const api = new NostrBandApi();
|
||||
const trending = await api.trendingNotes();
|
||||
setPosts(trending.notes.map(a => a.event));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadTrendingNotes().catch(console.error);
|
||||
}, []);
|
||||
|
||||
if (!posts) return <PageSpinner />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Trending Notes" />
|
||||
</h3>
|
||||
{posts.map(e => (
|
||||
<Note key={e.id} data={e as TaggedRawEvent} related={[]} depth={0} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,37 +4,20 @@ import { FormattedMessage } from "react-intl";
|
|||
|
||||
import FollowListBase from "Element/FollowListBase";
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
import NostrBandApi from "NostrBand";
|
||||
|
||||
interface TrendingUser {
|
||||
pubkey: HexKey;
|
||||
}
|
||||
|
||||
interface TrendingUserResponse {
|
||||
profiles: Array<TrendingUser>;
|
||||
}
|
||||
|
||||
async function fetchTrendingUsers() {
|
||||
try {
|
||||
const res = await fetch(`https://api.nostr.band/v0/trending/profiles`);
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as TrendingUserResponse;
|
||||
return data.profiles.map(a => a.pubkey);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load link preview`);
|
||||
}
|
||||
}
|
||||
|
||||
const TrendingUsers = () => {
|
||||
export default function TrendingUsers() {
|
||||
const [userList, setUserList] = useState<HexKey[]>();
|
||||
|
||||
async function loadTrendingUsers() {
|
||||
const api = new NostrBandApi();
|
||||
const users = await api.trendingProfiles();
|
||||
const keys = users.profiles.map(a => a.pubkey);
|
||||
setUserList(keys);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await fetchTrendingUsers();
|
||||
if (data) {
|
||||
setUserList(data);
|
||||
}
|
||||
})();
|
||||
loadTrendingUsers().catch(console.error);
|
||||
}, []);
|
||||
|
||||
if (!userList) return <PageSpinner />;
|
||||
|
@ -42,11 +25,9 @@ const TrendingUsers = () => {
|
|||
return (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Trending Users" />
|
||||
<FormattedMessage defaultMessage="Trending People" />
|
||||
</h3>
|
||||
<FollowListBase pubkeys={userList} showAbout={true} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendingUsers;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const WavlakeEmbed = ({ link }: { link: string }) => {
|
||||
const convertedUrl = link.replace(/(?:player\.)?wavlake\.com/, "embed.wavlake.com");
|
||||
const convertedUrl = link.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
||||
|
||||
return (
|
||||
<iframe
|
||||
|
|
|
@ -107,8 +107,8 @@ const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean
|
|||
return valid && sender ? (
|
||||
<div className="zap note card">
|
||||
<div className="header">
|
||||
<ProfileImage autoWidth={false} pubkey={sender} />
|
||||
{receiver !== pubKey && showZapped && <ProfileImage autoWidth={false} pubkey={unwrap(receiver)} />}
|
||||
<ProfileImage pubkey={sender} />
|
||||
{receiver !== pubKey && showZapped && <ProfileImage pubkey={unwrap(receiver)} />}
|
||||
<div className="amount">
|
||||
<span className="amount-number">
|
||||
<FormattedMessage {...messages.Sats} values={{ n: formatShort(amount ?? 0) }} />
|
||||
|
@ -151,7 +151,6 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
|||
<div className="summary">
|
||||
{sender && (
|
||||
<ProfileImage
|
||||
autoWidth={false}
|
||||
pubkey={anonZap ? "" : sender}
|
||||
overrideUsername={anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
|
||||
/>
|
||||
|
|
|
@ -104,4 +104,5 @@ export default defineMessages({
|
|||
ConfirmUnbookmark: { defaultMessage: "Are you sure you want to remove this note from bookmarks?" },
|
||||
ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" },
|
||||
ReactionsLink: { defaultMessage: "{n} Reactions" },
|
||||
ReBroadcast: { defaultMessage: "Broadcast Again" },
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from "react";
|
||||
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import { FlatNoteStore, RequestBuilder } from "System";
|
||||
import { RequestBuilder, ReplaceableNoteStore } from "System";
|
||||
import { NostrLink } from "Util";
|
||||
|
||||
export default function useEventFeed(link: NostrLink) {
|
||||
|
@ -11,5 +11,5 @@ export default function useEventFeed(link: NostrLink) {
|
|||
return b;
|
||||
}, [link]);
|
||||
|
||||
return useRequestBuilder<FlatNoteStore>(FlatNoteStore, sub);
|
||||
return useRequestBuilder<ReplaceableNoteStore>(ReplaceableNoteStore, sub);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function useLoginFeed() {
|
|||
const subLogin = useMemo(() => {
|
||||
if (!pubKey) return null;
|
||||
|
||||
const b = new RequestBuilder("login");
|
||||
const b = new RequestBuilder(`login:${pubKey.slice(0, 12)}`);
|
||||
b.withOptions({
|
||||
leaveOpen: true,
|
||||
});
|
||||
|
@ -46,7 +46,7 @@ export default function useLoginFeed() {
|
|||
|
||||
const subLists = useMemo(() => {
|
||||
if (!pubKey) return null;
|
||||
const b = new RequestBuilder("login:lists");
|
||||
const b = new RequestBuilder(`login:${pubKey.slice(0, 12)}:lists`);
|
||||
b.withOptions({
|
||||
leaveOpen: true,
|
||||
});
|
||||
|
@ -73,7 +73,7 @@ export default function useLoginFeed() {
|
|||
setFollows(login, pTags, contactList.created_at * 1000);
|
||||
}
|
||||
|
||||
const dms = loginFeed.data.filter(a => a.kind === EventKind.DirectMessage);
|
||||
const dms = loginFeed.data.filter(a => a.kind === EventKind.DirectMessage && a.tags.some(b => b[0] === "p"));
|
||||
DmCache.bulkSet(dms);
|
||||
|
||||
const subs = loginFeed.data.filter(
|
||||
|
|
|
@ -8,6 +8,7 @@ import useLogin from "Hooks/useLogin";
|
|||
|
||||
export default function useThreadFeed(link: NostrLink) {
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([link.id]);
|
||||
const [trackingATags, setTrackingATags] = useState<string[]>([]);
|
||||
const [allEvents, setAllEvents] = useState<u256[]>([link.id]);
|
||||
const pref = useLogin().preferences;
|
||||
|
||||
|
@ -22,15 +23,32 @@ export default function useThreadFeed(link: NostrLink) {
|
|||
.kinds(
|
||||
pref.enableReactions
|
||||
? [EventKind.Reaction, EventKind.TextNote, EventKind.Repost, EventKind.ZapReceipt]
|
||||
: [EventKind.TextNote, EventKind.ZapReceipt]
|
||||
: [EventKind.TextNote, EventKind.ZapReceipt, EventKind.Repost]
|
||||
)
|
||||
.tag("e", allEvents);
|
||||
|
||||
if (trackingATags.length > 0) {
|
||||
const parsed = trackingATags.map(a => a.split(":"));
|
||||
sub
|
||||
.withFilter()
|
||||
.kinds(parsed.map(a => Number(a[0])))
|
||||
.authors(parsed.map(a => a[1]))
|
||||
.tag(
|
||||
"d",
|
||||
parsed.map(a => a[2])
|
||||
);
|
||||
}
|
||||
return sub;
|
||||
}, [trackingEvents, allEvents, pref, link.id]);
|
||||
}, [trackingEvents, trackingATags, allEvents, pref]);
|
||||
|
||||
const store = useRequestBuilder<FlatNoteStore>(FlatNoteStore, sub);
|
||||
|
||||
useEffect(() => {
|
||||
setTrackingATags([]);
|
||||
setTrackingEvent([link.id]);
|
||||
setAllEvents([link.id]);
|
||||
}, [link.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (store.data) {
|
||||
const mainNotes = store.data?.filter(a => a.kind === EventKind.TextNote || a.kind === EventKind.Polls) ?? [];
|
||||
|
@ -39,6 +57,9 @@ export default function useThreadFeed(link: NostrLink) {
|
|||
const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a));
|
||||
setTrackingEvent(s => appendDedupe(s, eTagsMissing));
|
||||
setAllEvents(s => appendDedupe(s, eTags));
|
||||
|
||||
const aTags = mainNotes.map(a => a.tags.filter(b => b[0] === "a").map(b => b[1])).flat();
|
||||
setTrackingATags(s => appendDedupe(s, aTags));
|
||||
}
|
||||
}, [store]);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface TimelineFeedOptions {
|
|||
}
|
||||
|
||||
export interface TimelineSubject {
|
||||
type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword";
|
||||
type: "pubkey" | "hashtag" | "global" | "ptag" | "post_keyword" | "profile_keyword";
|
||||
discriminator: string;
|
||||
items: string[];
|
||||
}
|
||||
|
@ -37,7 +37,13 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||
}
|
||||
|
||||
const b = new RequestBuilder(`timeline:${subject.type}:${subject.discriminator}`);
|
||||
const f = b.withFilter().kinds([EventKind.TextNote, EventKind.Repost, EventKind.Polls]);
|
||||
const f = b
|
||||
.withFilter()
|
||||
.kinds(
|
||||
subject.type === "profile_keyword"
|
||||
? [EventKind.SetMetadata]
|
||||
: [EventKind.TextNote, EventKind.Repost, EventKind.Polls]
|
||||
);
|
||||
|
||||
if (options.relay) {
|
||||
b.withOptions({
|
||||
|
@ -58,7 +64,11 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||
f.tag("p", subject.items);
|
||||
break;
|
||||
}
|
||||
case "keyword": {
|
||||
case "profile_keyword": {
|
||||
f.search(subject.items[0] + " sort:popular");
|
||||
break;
|
||||
}
|
||||
case "post_keyword": {
|
||||
f.search(subject.items[0]);
|
||||
break;
|
||||
}
|
||||
|
@ -73,7 +83,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||
const rb = createBuilder();
|
||||
if (rb) {
|
||||
if (options.method === "LIMIT_UNTIL") {
|
||||
rb.filter.until(until).limit(10);
|
||||
rb.filter.until(until).limit(200);
|
||||
} else {
|
||||
rb.filter.since(since).until(until);
|
||||
if (since === undefined) {
|
||||
|
@ -105,7 +115,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||
|
||||
const subRealtime = useMemo(() => {
|
||||
const rb = createBuilder();
|
||||
if (rb && !pref.autoShowLatest) {
|
||||
if (rb && !pref.autoShowLatest && options.method !== "LIMIT_UNTIL") {
|
||||
rb.builder.withOptions({
|
||||
leaveOpen: true,
|
||||
});
|
||||
|
@ -128,7 +138,9 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||
if (trackingEvents.length > 0) {
|
||||
rb.withFilter()
|
||||
.kinds(
|
||||
pref.enableReactions ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.ZapReceipt]
|
||||
pref.enableReactions
|
||||
? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]
|
||||
: [EventKind.ZapReceipt, EventKind.Repost]
|
||||
)
|
||||
.tag("e", trackingEvents);
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import { useRef, useState, useEffect } from "react";
|
||||
|
||||
export default function useClientWidth() {
|
||||
const ref = useRef<HTMLDivElement | null>(document.querySelector(".page"));
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const updateSize = () => {
|
||||
if (ref.current) {
|
||||
setWidth(ref.current.offsetWidth);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", updateSize);
|
||||
updateSize();
|
||||
return () => window.removeEventListener("resize", updateSize);
|
||||
}, [ref]);
|
||||
|
||||
return width;
|
||||
}
|
|
@ -3,7 +3,7 @@ import { useSyncExternalStore } from "react";
|
|||
|
||||
export function useDmCache() {
|
||||
return useSyncExternalStore(
|
||||
c => DmCache.hook(c, undefined),
|
||||
c => DmCache.hook(c, "*"),
|
||||
() => DmCache.snapshot()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ export default function useImgProxy() {
|
|||
return s.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
async function signUrl(u: string) {
|
||||
const result = await hmacSha256(
|
||||
function signUrl(u: string) {
|
||||
const result = hmacSha256(
|
||||
secp.utils.hexToBytes(unwrap(settings).key),
|
||||
secp.utils.hexToBytes(unwrap(settings).salt),
|
||||
te.encode(u)
|
||||
|
@ -27,13 +27,13 @@ export default function useImgProxy() {
|
|||
}
|
||||
|
||||
return {
|
||||
proxy: async (url: string, resize?: number) => {
|
||||
proxy: (url: string, resize?: number) => {
|
||||
if (!settings) return url;
|
||||
const opt = resize ? `rs:fit:${resize}:${resize}/dpr:${window.devicePixelRatio}` : "";
|
||||
const urlBytes = te.encode(url);
|
||||
const urlEncoded = urlSafe(base64.encode(urlBytes, 0, urlBytes.byteLength));
|
||||
const path = `/${opt}/${urlEncoded}`;
|
||||
const sig = await signUrl(path);
|
||||
const sig = signUrl(path);
|
||||
return `${new URL(settings.url).toString()}${sig}${path}`;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { useSyncExternalStore } from "react";
|
||||
import { HexKey, u256 } from "@snort/nostr";
|
||||
|
||||
import { InteractionCache } from "Cache/EventInteractionCache";
|
||||
import { EventInteraction } from "Db";
|
||||
import { sha256, unwrap } from "Util";
|
||||
|
||||
export function useInteractionCache(pubkey?: HexKey, event?: u256) {
|
||||
const id = event && pubkey ? sha256(event + pubkey) : undefined;
|
||||
const EmptyInteraction = {
|
||||
id,
|
||||
event,
|
||||
by: pubkey,
|
||||
} as EventInteraction;
|
||||
const data =
|
||||
useSyncExternalStore(
|
||||
c => InteractionCache.hook(c, id),
|
||||
() => InteractionCache.snapshot().find(a => a.id === id)
|
||||
) || EmptyInteraction;
|
||||
return {
|
||||
data: data,
|
||||
react: () =>
|
||||
InteractionCache.set({
|
||||
...data,
|
||||
event: unwrap(event),
|
||||
by: unwrap(pubkey),
|
||||
reacted: true,
|
||||
}),
|
||||
zap: () =>
|
||||
InteractionCache.set({
|
||||
...data,
|
||||
event: unwrap(event),
|
||||
by: unwrap(pubkey),
|
||||
zapped: true,
|
||||
}),
|
||||
repost: () =>
|
||||
InteractionCache.set({
|
||||
...data,
|
||||
event: unwrap(event),
|
||||
by: unwrap(pubkey),
|
||||
reposted: true,
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { useIntl } from "react-intl";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
|
||||
import { EmailRegex, MnemonicRegex } from "Const";
|
||||
import { LoginStore } from "Login";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { getNip05PubKey } from "Pages/Login";
|
||||
import { bech32ToHex } from "Util";
|
||||
|
||||
export default function useLoginHandler() {
|
||||
const { formatMessage } = useIntl();
|
||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||
|
||||
async function doLogin(key: string) {
|
||||
const insecureMsg = formatMessage({
|
||||
defaultMessage:
|
||||
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
|
||||
});
|
||||
if (key.startsWith("nsec")) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const hexKey = bech32ToHex(key);
|
||||
if (secp.utils.isValidPrivateKey(hexKey)) {
|
||||
LoginStore.loginWithPrivateKey(hexKey);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
} else if (key.startsWith("npub")) {
|
||||
const hexKey = bech32ToHex(key);
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(EmailRegex)) {
|
||||
const hexKey = await getNip05PubKey(key);
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(MnemonicRegex)?.length === 24) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const ent = generateBip39Entropy(key);
|
||||
const keyHex = entropyToPrivateKey(ent);
|
||||
LoginStore.loginWithPrivateKey(keyHex);
|
||||
} else if (secp.utils.isValidPrivateKey(key)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
LoginStore.loginWithPrivateKey(key);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
doLogin,
|
||||
};
|
||||
}
|
|
@ -30,9 +30,9 @@ const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TSto
|
|||
};
|
||||
const getState = (): StoreSnapshot<TSnapshot> => {
|
||||
if (rb?.id) {
|
||||
const feed = System.GetFeed(rb.id);
|
||||
if (feed) {
|
||||
return unwrap(feed).snapshot as StoreSnapshot<TSnapshot>;
|
||||
const q = System.GetQuery(rb.id);
|
||||
if (q) {
|
||||
return unwrap(q).feed?.snapshot as StoreSnapshot<TSnapshot>;
|
||||
}
|
||||
}
|
||||
return EmptySnapshot as StoreSnapshot<TSnapshot>;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import IconProps from "./IconProps";
|
||||
|
||||
export default function NostrIcon(props: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.446 84.924" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
clip-rule="evenodd"
|
||||
d="m35.805 39.467c1.512-1.608 5.559-0.682 6.96-2.4-0.595-1.9-4.07-4.608-4.319-6.96-0.112-1.057 0.563-1.379 0.96-2.64 0.243-0.775 0.004-1.643 0.239-2.16 0.681-1.492 2.526-2.548 2.88-4.08-1.356-6.734 4.686-8.103 8.641-10.32 4.301 0.146 9.927-1.066 13.68 0.96 0.113 0.754-0.646 0.634-0.72 1.2 0.339 0.541 1.563 0.197 1.439 1.2-1.327 1.862-4.511-0.112-5.52 1.68 0.646 0.634 1.735 0.824 2.4 1.44v2.64c-0.708 0.172-1.486 0.274-1.921 0.72 1.552 3.67-5.669 2.291-3.359 6 1.339-0.021 4.954-0.144 6.72-1.2 2.784-1.665 2.711-6.367 5.521-8.159 0.691-0.029 1.57 0.131 1.92-0.24 1.151-2.775 3.98-5.438 8.88-5.76 2.746-0.182 8.349-1.87 10.8 0.239 1.465 1.262 0.81 3.268 2.16 4.561 0.988 0.451 2.105 0.774 2.16 2.16 0.267 1.202-1.834 1.31-0.48 2.159-0.962 1.039-1.811 2.19-3.12 2.881-0.113 1.153 1.554 0.526 1.44 1.68-0.802 1.122-1.209 3.907-2.641 3.6-0.806 0.247-0.373-0.746-0.479-1.199-0.89 0.295-1.405 0.67-2.16 0-0.26 0.78-0.709 1.371-1.2 1.92 1.643 1.478 4.003 2.237 5.521 3.84 3.235-1.359 7.077-5.149 10.8-1.92 0.188 0.988-0.368 1.231-0.24 2.16 0.896 0.774 0.978-0.801 1.92-0.721 1.06 0.062 1.265 0.976 2.16 1.2 0.185 0.904-0.293 1.147-0.24 1.92 0.473 0.889 2.352 0.368 2.881 1.2 0.555 2.155-1.012 2.188-0.961 3.84 1.031 0.388 1.998-1.142 3.601-0.96 0.884 1.517 0.381 4.419 2.16 5.04 0.628 3.104-2.561 3.75-4.32 2.4-0.444 0.436-0.312 1.448-0.72 1.92-1.188 0.147-1.536-0.545-2.4-0.721-0.799 1.563 1.617 1.889 0.72 3.601-1.775-0.463-2.337 1.205-3.359 2.16-1.136-0.064-1.352-1.049-2.16-1.44-0.217 0.423-0.884 0.396-0.96 0.96-0.752 0.804 1.801 1.3 0.72 2.4-1.513 2.06-3.329-1.013-5.76 0-0.55-0.57-1.208-1.032-1.44-1.92-2.051 0.131-3.084-0.756-4.319-1.44-3.303-0.538-4.311 1.677-7.44 0.96 0.216 2.23 3.326 2.419 5.28 2.16 2.783 2.896 3.368 7.992 6.72 10.32 0.458-3.125 4.479 6.161 9.12 10.319 3.707-0.149 6.219 0.33 8.16 1.44 0.042 1.242-2.057 0.343-2.64 0.96 1.246 2.751 4.993-0.816 6.96-0.24-0.479 6.364-12.435 7.859-14.881 2.16-6.689-3.79-9.293-11.666-15.119-16.32-2.059-0.502-3.208-1.912-4.801-2.88-5.372 0.134-10.436 0.287-13.92-1.92-2.16 1.263-3.17 4.747-6 5.521-2.923 0.798-5.911-0.139-8.16 1.92-7.446 1.033-14.465 2.494-19.68 5.76-1.237 0.412-2.52-0.162-3.12 0.479 0.48 2.32 1.668 3.934 1.92 6.48-0.519 0.761-0.962 1.598-1.92 1.92 0.095 1.746 2.833 0.848 3.12 2.4-4.069 1.981-6.507-1.59-7.92-3.841 0.508-4.2-0.333-9.392 2.16-11.52 1.205-1.029 2.837-0.545 4.32-1.68 4.366 0.4 8.705-2.869 12.96-3.84 4.858-1.109 9.547-1.108 11.279-5.28-1.414-1.656-3.291-0.841-5.52-1.44-1.111-0.299-1.463-1.133-2.4-1.68-0.562-0.328-1.474-0.334-2.16-0.72-2.196-1.234-3.287-3.257-6.239-3.841-1.489-0.294-2.832-0.085-4.08-0.479-7.656-2.422-10.618-10.302-13.2-18.24-0.314-3.445-0.995-6.524-1.92-9.359-0.827-8.533-7.048-11.673-13.68-14.4-2.024-0.184-3.309 0.372-5.28 0.24-0.977-0.784-2.486-1.034-2.16-3.12 1.78-0.307 3.603-1.558 5.52-0.96 1.04-0.164 1.452-1.567 2.636-2.16 1.045-0.523 3.934-0.583 5.52-1.92 0.24-0.202 4.291-0.067 4.561 0 2.813 0.7 2.876 4.102 5.04 5.76-1.263 4.763 2.796 8.095 3.6 12.24 0.192 0.99-0.095 1.896 0 2.88 0.472 4.913 2.428 11.467 4.8 14.88 0.998 1.438 2.397 2.623 4.078 3.6z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -15,6 +15,8 @@ import deMessages from "translations/de_DE.json";
|
|||
import ruMessages from "translations/ru_RU.json";
|
||||
import svMessages from "translations/sv_SE.json";
|
||||
import hrMessages from "translations/hr_HR.json";
|
||||
import taINMessages from "translations/ta_IN.json";
|
||||
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
const DefaultLocale = "en-US";
|
||||
|
@ -63,6 +65,9 @@ const getMessages = (locale: string) => {
|
|||
case "hr-HR":
|
||||
case "hr":
|
||||
return hrMessages;
|
||||
case "ta-IN":
|
||||
case "ta":
|
||||
return taINMessages;
|
||||
case DefaultLocale:
|
||||
case "en":
|
||||
return enMessages;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { DefaultRelays, SnortPubKey } from "Const";
|
|||
import { LoginStore, UserPreferences, LoginSession } from "Login";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "Util";
|
||||
import { getCurrentSubscription, SubscriptionEvent } from "Subscription";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
import { EventPublisher } from "System/EventPublisher";
|
||||
|
||||
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||
|
@ -150,7 +150,7 @@ export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts
|
|||
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
|
||||
const newSubs = dedupeById([...(state.subscriptions || []), ...subs]);
|
||||
if (newSubs.length !== state.subscriptions.length) {
|
||||
state.currentSubscription = getCurrentSubscription(state.subscriptions);
|
||||
state.subscriptions = newSubs;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,9 +80,4 @@ export interface LoginSession {
|
|||
* Snort subscriptions licences
|
||||
*/
|
||||
subscriptions: Array<SubscriptionEvent>;
|
||||
|
||||
/**
|
||||
* Current active subscription
|
||||
*/
|
||||
currentSubscription?: SubscriptionEvent;
|
||||
}
|
||||
|
|
|
@ -73,11 +73,22 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||
return [...this.#accounts.keys()];
|
||||
}
|
||||
|
||||
allSubscriptions() {
|
||||
return [...this.#accounts.values()].map(a => a.subscriptions).flat();
|
||||
}
|
||||
|
||||
switchAccount(pk: string) {
|
||||
if (this.#accounts.has(pk)) {
|
||||
this.#activeAccount = pk;
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
loginWithPubkey(key: HexKey, relays?: Record<string, RelaySettings>) {
|
||||
if (this.#accounts.has(key)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
}
|
||||
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
||||
const initRelays = this.decideInitRelays(relays);
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
publicKey: key,
|
||||
|
@ -94,6 +105,13 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||
return newSession;
|
||||
}
|
||||
|
||||
decideInitRelays(relays: Record<string, RelaySettings> | undefined): Record<string, RelaySettings> {
|
||||
if (relays && Object.keys(relays).length > 0) {
|
||||
return relays;
|
||||
}
|
||||
return Object.fromEntries(DefaultRelays.entries());
|
||||
}
|
||||
|
||||
loginWithPrivateKey(key: HexKey, entropy?: string, relays?: Record<string, RelaySettings>) {
|
||||
const pubKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(key));
|
||||
if (this.#accounts.has(pubKey)) {
|
||||
|
@ -127,6 +145,9 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||
|
||||
removeSession(k: string) {
|
||||
if (this.#accounts.delete(k)) {
|
||||
if (this.#activeAccount === k) {
|
||||
this.#activeAccount = undefined;
|
||||
}
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { RawEvent } from "@snort/nostr";
|
||||
|
||||
export interface TrendingUser {
|
||||
pubkey: string;
|
||||
}
|
||||
|
||||
export interface TrendingUserResponse {
|
||||
profiles: Array<TrendingUser>;
|
||||
}
|
||||
|
||||
export interface TrendingNote {
|
||||
event: RawEvent;
|
||||
author: RawEvent; // kind0 event
|
||||
}
|
||||
|
||||
export interface TrendingNoteResponse {
|
||||
notes: Array<TrendingNote>;
|
||||
}
|
||||
|
||||
export class NostrBandError extends Error {
|
||||
body: string;
|
||||
statusCode: number;
|
||||
|
||||
constructor(message: string, body: string, status: number) {
|
||||
super(message);
|
||||
this.body = body;
|
||||
this.statusCode = status;
|
||||
}
|
||||
}
|
||||
|
||||
export default class NostrBandApi {
|
||||
#url = "https://api.nostr.band";
|
||||
|
||||
async trendingProfiles() {
|
||||
return await this.#json<TrendingUserResponse>("GET", "/v0/trending/profiles");
|
||||
}
|
||||
|
||||
async trendingNotes() {
|
||||
return await this.#json<TrendingNoteResponse>("GET", "/v0/trending/notes");
|
||||
}
|
||||
|
||||
async #json<T>(method: string, path: string) {
|
||||
const res = await fetch(`${this.#url}${path}`, {
|
||||
method: method ?? "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
return (await res.json()) as T;
|
||||
} else {
|
||||
throw new NostrBandError("Failed to load content from nostr.band", await res.text(), res.status);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,13 +2,13 @@ import "./ChatPage.css";
|
|||
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { bech32ToHex } from "Util";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import DM from "Element/DM";
|
||||
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||
import { dmsForLogin, dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||
import NoteToSelf from "Element/NoteToSelf";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
@ -24,15 +24,17 @@ export default function ChatPage() {
|
|||
const pubKey = useLogin().publicKey;
|
||||
const [content, setContent] = useState<string>();
|
||||
const dmListRef = useRef<HTMLDivElement>(null);
|
||||
const dms = filterDms(useDmCache());
|
||||
|
||||
function filterDms(dms: readonly RawEvent[]) {
|
||||
return dmsInChat(id === pubKey ? dms.filter(d => isToSelf(d, pubKey)) : dms, id);
|
||||
}
|
||||
const dms = useDmCache();
|
||||
|
||||
const sortedDms = useMemo(() => {
|
||||
return [...dms].sort((a, b) => a.created_at - b.created_at);
|
||||
}, [dms]);
|
||||
if (pubKey) {
|
||||
const myDms = dmsForLogin(dms, pubKey);
|
||||
// filter dms in this chat, or dms to self
|
||||
const thisDms = id === pubKey ? myDms.filter(d => isToSelf(d, pubKey)) : myDms;
|
||||
return [...dmsInChat(thisDms, id)].sort((a, b) => a.created_at - b.created_at);
|
||||
}
|
||||
return [];
|
||||
}, [dms, pubKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dmListRef.current) {
|
||||
|
|
|
@ -21,6 +21,7 @@ const Contributors = [
|
|||
bech32ToHex("npub1vp8fdcyejd4pqjyrjk9sgz68vuhq7pyvnzk8j0ehlljvwgp8n6eqsrnpsw"), // samsamskies
|
||||
bech32ToHex("npub179rec9sw2a5ngkr2wsjpjhwp2ksygjxn6uw5py9daj2ezhw3aw5swv3s6q"), // h3y6e - JA + other stuff
|
||||
bech32ToHex("npub17q5n2z8naw0xl6vu9lvt560lg33pdpe29k0k09umlfxm3vc4tqrq466f2y"), // w3irdrobot
|
||||
bech32ToHex("npub1ltx67888tz7lqnxlrg06x234vjnq349tcfyp52r0lstclp548mcqnuz40t"), // Vivek
|
||||
];
|
||||
|
||||
const Translators = [
|
||||
|
@ -88,8 +89,8 @@ const DonatePage = () => {
|
|||
defaultMessage={"Check out the code here: {link}"}
|
||||
values={{
|
||||
link: (
|
||||
<a className="highlight" href="https://github.com/v0l/snort" rel="noreferrer" target="_blank">
|
||||
https://github.com/v0l/snort
|
||||
<a className="highlight" href="https://git.v0l.io/Kieran/snort" rel="noreferrer" target="_blank">
|
||||
https://git.v0l.io/Kieran/snort
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
|
|
|
@ -15,7 +15,6 @@ header {
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.header-actions .avatar {
|
||||
|
|
|
@ -24,6 +24,7 @@ import useLogin from "Hooks/useLogin";
|
|||
import Avatar from "Element/Avatar";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { profileLink } from "Util";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
|
@ -32,7 +33,8 @@ export default function Layout() {
|
|||
const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { publicKey, relays, preferences, currentSubscription } = useLogin();
|
||||
const { publicKey, relays, preferences, subscriptions } = useLogin();
|
||||
const currentSubscription = getCurrentSubscription(subscriptions);
|
||||
const [pageClass, setPageClass] = useState("page");
|
||||
const pub = useEventPublisher();
|
||||
useLoginFeed();
|
||||
|
@ -64,7 +66,7 @@ export default function Layout() {
|
|||
|
||||
useEffect(() => {
|
||||
if (pub) {
|
||||
System.HandleAuth = pub.nip42Auth;
|
||||
System.HandleAuth = pub.nip42Auth.bind(pub);
|
||||
}
|
||||
}, [pub]);
|
||||
|
||||
|
@ -134,7 +136,7 @@ export default function Layout() {
|
|||
return (
|
||||
<div className={pageClass}>
|
||||
{!shouldHideHeader && (
|
||||
<header>
|
||||
<header className="main-content mt5">
|
||||
<div className="logo" onClick={() => navigate("/")}>
|
||||
<h1>Snort</h1>
|
||||
{currentSubscription && (
|
||||
|
|
|
@ -93,12 +93,7 @@
|
|||
|
||||
.login .login-actions > button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.login .login-actions > button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login .login-or {
|
||||
|
|
|
@ -2,21 +2,17 @@ import "./Login.css";
|
|||
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
|
||||
import { EmailRegex, MnemonicRegex } from "Const";
|
||||
import { bech32ToHex, unwrap } from "Util";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import ZapButton from "Element/ZapButton";
|
||||
import useImgProxy from "Hooks/useImgProxy";
|
||||
import Icon from "Icons/Icon";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { generateNewLogin, LoginStore } from "Login";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLoginHandler from "Hooks/useLoginHandler";
|
||||
|
||||
interface ArtworkEntry {
|
||||
name: string;
|
||||
|
@ -74,6 +70,7 @@ export default function LoginPage() {
|
|||
const [isMasking, setMasking] = useState(true);
|
||||
const { formatMessage } = useIntl();
|
||||
const { proxy } = useImgProxy();
|
||||
const loginHandler = useLoginHandler();
|
||||
const hasNip7 = "nostr" in window;
|
||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||
|
||||
|
@ -85,46 +82,13 @@ export default function LoginPage() {
|
|||
|
||||
useEffect(() => {
|
||||
const ret = unwrap(Artwork.at(Artwork.length * Math.random()));
|
||||
proxy(ret.link).then(a => setArt({ ...ret, link: a }));
|
||||
const url = proxy(ret.link);
|
||||
setArt({ ...ret, link: url });
|
||||
}, []);
|
||||
|
||||
async function doLogin() {
|
||||
const insecureMsg = formatMessage({
|
||||
defaultMessage:
|
||||
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
|
||||
});
|
||||
try {
|
||||
if (key.startsWith("nsec")) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const hexKey = bech32ToHex(key);
|
||||
if (secp.utils.isValidPrivateKey(hexKey)) {
|
||||
LoginStore.loginWithPrivateKey(hexKey);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
} else if (key.startsWith("npub")) {
|
||||
const hexKey = bech32ToHex(key);
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(EmailRegex)) {
|
||||
const hexKey = await getNip05PubKey(key);
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(MnemonicRegex)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const ent = generateBip39Entropy(key);
|
||||
const keyHex = entropyToPrivateKey(ent);
|
||||
LoginStore.loginWithPrivateKey(keyHex);
|
||||
} else if (secp.utils.isValidPrivateKey(key)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
LoginStore.loginWithPrivateKey(key);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
await loginHandler.doLogin(key);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
|
@ -145,8 +109,9 @@ export default function LoginPage() {
|
|||
}
|
||||
|
||||
async function doNip07Login() {
|
||||
const relays = "getRelays" in window.nostr ? await window.nostr.getRelays() : undefined;
|
||||
const pubKey = await window.nostr.getPublicKey();
|
||||
const relays =
|
||||
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;
|
||||
const pubKey = await unwrap(window.nostr).getPublicKey();
|
||||
LoginStore.loginWithPubkey(pubKey, relays);
|
||||
}
|
||||
|
||||
|
@ -165,33 +130,6 @@ export default function LoginPage() {
|
|||
);
|
||||
}
|
||||
|
||||
function generateKey() {
|
||||
if (!hasSubtleCrypto) return;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex login-or">
|
||||
<FormattedMessage defaultMessage="OR" description="Seperator text for Login / Generate Key" />
|
||||
<div className="divider w-max"></div>
|
||||
</div>
|
||||
<h1 dir="auto">
|
||||
<FormattedMessage defaultMessage="Create an Account" description="Heading for generate key flow" />
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Generate a public / private key pair. Do not share your private key with anyone, this acts as your password. Once lost, it cannot be “reset” or recovered. Keep safe!"
|
||||
description="Note about key security before generating a new key"
|
||||
/>
|
||||
</p>
|
||||
<div className="login-actions">
|
||||
<AsyncButton onClick={() => makeRandomKey()}>
|
||||
<FormattedMessage defaultMessage="Generate Key" description="Button: Generate a new key" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function installExtension() {
|
||||
if (hasSubtleCrypto) return;
|
||||
|
||||
|
@ -255,9 +193,9 @@ export default function LoginPage() {
|
|||
<div className="login">
|
||||
<div>
|
||||
<div className="login-container">
|
||||
<div className="logo" onClick={() => navigate("/")}>
|
||||
<h1 className="logo" onClick={() => navigate("/")}>
|
||||
Snort
|
||||
</div>
|
||||
</h1>
|
||||
<h1 dir="auto">
|
||||
<FormattedMessage defaultMessage="Login" description="Login header" />
|
||||
</h1>
|
||||
|
@ -268,7 +206,9 @@ export default function LoginPage() {
|
|||
<input
|
||||
dir="auto"
|
||||
type={isMasking ? "password" : "text"}
|
||||
placeholder={formatMessage(messages.KeyPlaceholder)}
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
|
||||
})}
|
||||
className="f-grow"
|
||||
onChange={e => setKey(e.target.value)}
|
||||
/>
|
||||
|
@ -280,7 +220,7 @@ export default function LoginPage() {
|
|||
/>
|
||||
</div>
|
||||
{error.length > 0 ? <b className="error">{error}</b> : null}
|
||||
<p className="login-note">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Only the secret key can be used to publish (sign events), everything else logs you in read-only mode."
|
||||
description="Explanation for public key only login is read-only"
|
||||
|
@ -290,9 +230,11 @@ export default function LoginPage() {
|
|||
<button type="button" onClick={doLogin}>
|
||||
<FormattedMessage defaultMessage="Login" description="Login button" />
|
||||
</button>
|
||||
<AsyncButton onClick={() => makeRandomKey()}>
|
||||
<FormattedMessage defaultMessage="Create Account" />
|
||||
</AsyncButton>
|
||||
{altLogins()}
|
||||
</div>
|
||||
{generateKey()}
|
||||
{installExtension()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { HexKey, RawEvent } from "@snort/nostr";
|
|||
|
||||
import UnreadCount from "Element/UnreadCount";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { hexToBech32 } from "Util";
|
||||
import { dedupe, hexToBech32, unwrap } from "Util";
|
||||
import NoteToSelf from "Element/NoteToSelf";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
|
@ -31,7 +31,7 @@ export default function MessagesPage() {
|
|||
);
|
||||
}
|
||||
return [];
|
||||
}, [dms, login]);
|
||||
}, [dms, login.publicKey]);
|
||||
|
||||
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unreadMessages, 0), [chats]);
|
||||
|
||||
|
@ -105,7 +105,7 @@ export function setLastReadDm(pk: HexKey) {
|
|||
|
||||
export function dmTo(e: RawEvent) {
|
||||
const firstP = e.tags.find(b => b[0] === "p");
|
||||
return firstP ? firstP[1] : "";
|
||||
return unwrap(firstP?.[1]);
|
||||
}
|
||||
|
||||
export function isToSelf(e: Readonly<RawEvent>, pk: HexKey) {
|
||||
|
@ -137,14 +137,19 @@ function newestMessage(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
|||
return dmsInChat(dms, pk).reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0);
|
||||
}
|
||||
|
||||
export function dmsForLogin(dms: readonly RawEvent[], myPubKey: HexKey) {
|
||||
return dms.filter(a => a.pubkey === myPubKey || (a.pubkey !== myPubKey && dmTo(a) === myPubKey));
|
||||
}
|
||||
|
||||
export function extractChats(dms: RawEvent[], myPubKey: HexKey) {
|
||||
const keys = dms.map(a => [a.pubkey, dmTo(a)]).flat();
|
||||
const filteredKeys = Array.from(new Set<string>(keys));
|
||||
const myDms = dmsForLogin(dms, myPubKey);
|
||||
const keys = myDms.map(a => [a.pubkey, dmTo(a)]).flat();
|
||||
const filteredKeys = dedupe(keys);
|
||||
return filteredKeys.map(a => {
|
||||
return {
|
||||
pubkey: a,
|
||||
unreadMessages: unreadDms(dms, myPubKey, a),
|
||||
newestMessage: newestMessage(dms, myPubKey, a),
|
||||
unreadMessages: unreadDms(myDms, myPubKey, a),
|
||||
newestMessage: newestMessage(myDms, myPubKey, a),
|
||||
} as DmChat;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import Timeline from "Element/Timeline";
|
||||
import { TaskList } from "Tasks/TaskList";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { markNotificationsRead } from "Login";
|
||||
import { unixNow } from "Util";
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const login = useLogin();
|
||||
const [now] = useState(unixNow());
|
||||
|
||||
useEffect(() => {
|
||||
markNotificationsRead(login);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="main-content">
|
||||
<TaskList />
|
||||
</div>
|
||||
<div className="main-content">
|
||||
<TaskList />
|
||||
{login.publicKey && (
|
||||
<Timeline
|
||||
subject={{
|
||||
|
@ -24,10 +24,12 @@ export default function NotificationsPage() {
|
|||
items: [login.publicKey],
|
||||
discriminator: login.publicKey.slice(0, 12),
|
||||
}}
|
||||
now={now}
|
||||
window={60 * 60 * 12}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -194,15 +194,8 @@
|
|||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.qr-modal .modal-body {
|
||||
width: unset;
|
||||
margin-top: -120px;
|
||||
}
|
||||
|
||||
.qr-modal .pfp {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-modal .pfp .avatar {
|
||||
|
@ -220,20 +213,12 @@
|
|||
|
||||
.qr-modal .pfp .username {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.qr-modal canvas {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.qr-modal .pfp .display-name {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile .zap-amount {
|
||||
font-weight: normal;
|
||||
margin-left: 4px;
|
||||
|
|
|
@ -230,20 +230,18 @@ export default function ProfilePage() {
|
|||
case NOTES:
|
||||
return (
|
||||
<>
|
||||
<div className="main-content">
|
||||
{pinned
|
||||
.filter(a => a.kind === EventKind.TextNote)
|
||||
.map(n => {
|
||||
return (
|
||||
<Note
|
||||
key={`pinned-${n.id}`}
|
||||
data={n}
|
||||
related={getReactions(pinned, n.id)}
|
||||
options={{ showTime: false, showPinned: true, canUnpin: id === loginPubKey }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{pinned
|
||||
.filter(a => a.kind === EventKind.TextNote)
|
||||
.map(n => {
|
||||
return (
|
||||
<Note
|
||||
key={`pinned-${n.id}`}
|
||||
data={n}
|
||||
related={getReactions(pinned, n.id)}
|
||||
options={{ showTime: false, showPinned: true, canUnpin: id === loginPubKey }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Timeline
|
||||
key={id}
|
||||
subject={{
|
||||
|
@ -252,7 +250,8 @@ export default function ProfilePage() {
|
|||
discriminator: id.slice(0, 12),
|
||||
}}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
method={"LIMIT_UNTIL"}
|
||||
loadMore={false}
|
||||
ignoreModeration={true}
|
||||
window={60 * 60 * 6}
|
||||
/>
|
||||
|
@ -263,14 +262,7 @@ export default function ProfilePage() {
|
|||
}
|
||||
case FOLLOWS: {
|
||||
if (isMe) {
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => navigate("/new/import")} className="mb10">
|
||||
<FormattedMessage defaultMessage="Find Twitter follows" />
|
||||
</button>
|
||||
<FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
|
||||
</>
|
||||
);
|
||||
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
|
||||
} else {
|
||||
return <FollowsTab id={id} />;
|
||||
}
|
||||
|
@ -378,7 +370,7 @@ export default function ProfilePage() {
|
|||
{isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
|
||||
</div>
|
||||
</div>
|
||||
{tabContent()}
|
||||
<div className="main-content">{tabContent()}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -83,7 +83,9 @@ export default function RootPage() {
|
|||
<div className="main-content">
|
||||
{pubKey && <Tabs tabs={tabs} tab={tab} setTab={t => navigate(unwrap(t.data))} />}
|
||||
</div>
|
||||
<Outlet />
|
||||
<div className="main-content">
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -185,8 +187,12 @@ const GlobalTab = () => {
|
|||
};
|
||||
|
||||
const PostsTab = () => {
|
||||
const { follows } = useLogin();
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
|
||||
const { follows, publicKey } = useLogin();
|
||||
const subject: TimelineSubject = {
|
||||
type: "pubkey",
|
||||
items: follows.item,
|
||||
discriminator: `follows:${publicKey?.slice(0, 12)}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -197,8 +203,12 @@ const PostsTab = () => {
|
|||
};
|
||||
|
||||
const ConversationsTab = () => {
|
||||
const { follows } = useLogin();
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
|
||||
const { follows, publicKey } = useLogin();
|
||||
const subject: TimelineSubject = {
|
||||
type: "pubkey",
|
||||
items: follows.item,
|
||||
discriminator: `follows:${publicKey?.slice(0, 12)}`,
|
||||
};
|
||||
|
||||
return <Timeline subject={subject} postsOnly={false} method={"TIME_RANGE"} window={undefined} relay={undefined} />;
|
||||
};
|
||||
|
|
|
@ -1,33 +1,38 @@
|
|||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useParams } from "react-router-dom";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import Timeline from "Element/Timeline";
|
||||
import { Tab, TabElement } from "Element/Tabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { debounce } from "Util";
|
||||
import { router } from "index";
|
||||
import { SearchRelays } from "Const";
|
||||
import { System } from "System";
|
||||
import { MetadataCache } from "Cache";
|
||||
import { UserCache } from "Cache/UserCache";
|
||||
import TrendingUsers from "Element/TrendingUsers";
|
||||
|
||||
import messages from "./messages";
|
||||
import TrendingNotes from "Element/TrendingPosts";
|
||||
|
||||
const NOTES = 0;
|
||||
const PROFILES = 1;
|
||||
|
||||
const SearchPage = () => {
|
||||
const params = useParams();
|
||||
const { formatMessage } = useIntl();
|
||||
const [search, setSearch] = useState<string | undefined>(params.keyword);
|
||||
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
||||
const [allUsers, setAllUsers] = useState<MetadataCache[]>();
|
||||
const [sortPopular, setSortPopular] = useState<boolean>(true);
|
||||
// tabs
|
||||
const SearchTab = {
|
||||
Posts: { text: formatMessage({ defaultMessage: "Notes" }), value: NOTES },
|
||||
Profiles: { text: formatMessage({ defaultMessage: "People" }), value: PROFILES },
|
||||
};
|
||||
const [tab, setTab] = useState<Tab>(SearchTab.Posts);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyword) {
|
||||
// "navigate" changing only url
|
||||
router.navigate(`/search/${encodeURIComponent(keyword)}`);
|
||||
UserCache.search(keyword).then(v => setAllUsers(v));
|
||||
} else {
|
||||
router.navigate(`/search`);
|
||||
setAllUsers([]);
|
||||
}
|
||||
}, [keyword]);
|
||||
|
||||
|
@ -50,35 +55,76 @@ const SearchPage = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
function tabContent() {
|
||||
if (!keyword) {
|
||||
switch (tab.value) {
|
||||
case PROFILES:
|
||||
return <TrendingUsers />;
|
||||
case NOTES:
|
||||
return <TrendingNotes />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const pf = tab.value == PROFILES;
|
||||
return (
|
||||
<>
|
||||
{sortOptions()}
|
||||
<Timeline
|
||||
key={keyword + (pf ? "_p" : "")}
|
||||
subject={{
|
||||
type: pf ? "profile_keyword" : "post_keyword",
|
||||
items: [keyword + (sortPopular ? " sort:popular" : "")],
|
||||
discriminator: keyword,
|
||||
}}
|
||||
postsOnly={false}
|
||||
noSort={pf && sortPopular}
|
||||
method={"LIMIT_UNTIL"}
|
||||
loadMore={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function sortOptions() {
|
||||
if (tab.value != PROFILES) return null;
|
||||
return (
|
||||
<div className="flex mb10 f-end">
|
||||
<FormattedMessage defaultMessage="Sort" description="Label for sorting options for people search" />
|
||||
|
||||
<select onChange={e => setSortPopular(e.target.value == "true")} value={sortPopular ? "true" : "false"}>
|
||||
<option value={"true"}>
|
||||
<FormattedMessage defaultMessage="Popular" description="Sort order name" />
|
||||
</option>
|
||||
<option value={"false"}>
|
||||
<FormattedMessage defaultMessage="Recent" description="Sort order name" />
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTab(v: Tab) {
|
||||
return <TabElement key={v.value} t={v} tab={tab} setTab={setTab} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2>
|
||||
<FormattedMessage {...messages.Search} />
|
||||
<FormattedMessage defaultMessage="Search" />
|
||||
</h2>
|
||||
<div className="flex mb10">
|
||||
<input
|
||||
type="text"
|
||||
className="f-grow mr10"
|
||||
placeholder={formatMessage(messages.SearchPlaceholder)}
|
||||
placeholder={formatMessage({ defaultMessage: "Search..." })}
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
{!keyword && <TrendingUsers />}
|
||||
{keyword && allUsers?.slice(0, 3).map(u => <ProfilePreview actions={<></>} className="card" pubkey={u.pubkey} />)}
|
||||
{keyword && (
|
||||
<Timeline
|
||||
key={keyword}
|
||||
subject={{
|
||||
type: "keyword",
|
||||
items: [keyword],
|
||||
discriminator: keyword,
|
||||
}}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
/>
|
||||
)}
|
||||
<div className="tabs">{[SearchTab.Posts, SearchTab.Profiles].map(renderTab)}</div>
|
||||
{tabContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,8 +5,10 @@ import Profile from "Pages/settings/Profile";
|
|||
import Relay from "Pages/settings/Relays";
|
||||
import Preferences from "Pages/settings/Preferences";
|
||||
import RelayInfo from "Pages/settings/RelayInfo";
|
||||
import AccountsPage from "Pages/settings/Accounts";
|
||||
import { WalletSettingsRoutes } from "Pages/settings/WalletSettings";
|
||||
import { ManageHandleRoutes } from "Pages/settings/handle";
|
||||
import ExportKeys from "Pages/settings/Keys";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
|
@ -44,6 +46,14 @@ export const SettingsRoutes: RouteObject[] = [
|
|||
path: "preferences",
|
||||
element: <Preferences />,
|
||||
},
|
||||
{
|
||||
path: "accounts",
|
||||
element: <AccountsPage />,
|
||||
},
|
||||
{
|
||||
path: "keys",
|
||||
element: <ExportKeys />,
|
||||
},
|
||||
...ManageHandleRoutes,
|
||||
...WalletSettingsRoutes,
|
||||
];
|
||||
|
|
|
@ -22,8 +22,6 @@ export default defineMessages({
|
|||
Sats: { defaultMessage: "{n} {n, plural, =1 {sat} other {sats}}" },
|
||||
Following: { defaultMessage: "Following {n}" },
|
||||
Settings: { defaultMessage: "Settings" },
|
||||
Search: { defaultMessage: "Search" },
|
||||
SearchPlaceholder: { defaultMessage: "Search..." },
|
||||
Messages: { defaultMessage: "Messages" },
|
||||
MarkAllRead: { defaultMessage: "Mark All Read" },
|
||||
GetVerified: { defaultMessage: "Get Verified" },
|
||||
|
@ -46,5 +44,5 @@ export default defineMessages({
|
|||
},
|
||||
Bookmarks: { defaultMessage: "Bookmarks" },
|
||||
BookmarksCount: { defaultMessage: "{n} Bookmarks" },
|
||||
KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex, mnemonic" },
|
||||
KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex" },
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import Logo from "Element/Logo";
|
|||
import FollowListBase from "Element/FollowListBase";
|
||||
import { clearEntropy } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import TrendingUsers from "Element/TrendingUsers";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
|
@ -43,7 +44,8 @@ export default function DiscoverFollows() {
|
|||
<h3>
|
||||
<FormattedMessage {...messages.PopularAccounts} />
|
||||
</h3>
|
||||
<div dir="ltr">{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} />}</div>
|
||||
{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} showAbout={true} />}
|
||||
<TrendingUsers />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,11 @@ export default function GetVerified() {
|
|||
<h1>
|
||||
<FormattedMessage {...messages.Identifier} />
|
||||
</h1>
|
||||
<div className="next-actions continue-actions">
|
||||
<button className="secondary" type="button" onClick={onNext}>
|
||||
<FormattedMessage {...messages.Skip} />
|
||||
</button>
|
||||
</div>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.PreviewOnSnort} />
|
||||
</h4>
|
||||
|
|
|
@ -68,9 +68,6 @@ export default function ImportFollows() {
|
|||
</p>
|
||||
|
||||
<div className="next-actions continue-actions">
|
||||
<button className="secondary" type="button" onClick={() => navigate("/new/discover")}>
|
||||
<FormattedMessage {...messages.Skip} />
|
||||
</button>
|
||||
<button type="button" onClick={() => navigate("/new/discover")}>
|
||||
<FormattedMessage {...messages.Next} />
|
||||
</button>
|
||||
|
|
|
@ -9,10 +9,16 @@ import { hexToMnemonic } from "nip6";
|
|||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import { PROFILE } from ".";
|
||||
|
||||
const WhatIsSnort = () => {
|
||||
return (
|
||||
<CollapsedSection title={<FormattedMessage {...messages.WhatIsSnort} />}>
|
||||
<CollapsedSection
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage {...messages.WhatIsSnort} />
|
||||
</h3>
|
||||
}>
|
||||
<p>
|
||||
<FormattedMessage {...messages.WhatIsSnortIntro} />
|
||||
</p>
|
||||
|
@ -28,7 +34,12 @@ const WhatIsSnort = () => {
|
|||
|
||||
const HowDoKeysWork = () => {
|
||||
return (
|
||||
<CollapsedSection title={<FormattedMessage {...messages.HowKeysWork} />}>
|
||||
<CollapsedSection
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage {...messages.HowKeysWork} />
|
||||
</h3>
|
||||
}>
|
||||
<p>
|
||||
<FormattedMessage {...messages.DigitalSignatures} />
|
||||
</p>
|
||||
|
@ -44,7 +55,12 @@ const HowDoKeysWork = () => {
|
|||
|
||||
const Extensions = () => {
|
||||
return (
|
||||
<CollapsedSection title={<FormattedMessage {...messages.ImproveSecurity} />}>
|
||||
<CollapsedSection
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage {...messages.ImproveSecurity} />
|
||||
</h3>
|
||||
}>
|
||||
<p>
|
||||
<FormattedMessage {...messages.Extensions} />
|
||||
</p>
|
||||
|
@ -92,7 +108,7 @@ export default function NewUserFlow() {
|
|||
</h2>
|
||||
<Copy text={hexToMnemonic(generatedEntropy ?? "")} />
|
||||
<div className="next-actions">
|
||||
<button type="button" onClick={() => navigate("/new/username")}>
|
||||
<button type="button" onClick={() => navigate(PROFILE)}>
|
||||
<FormattedMessage {...messages.KeysSaved} />{" "}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,24 +1,47 @@
|
|||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Logo from "Element/Logo";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { mapEventToProfile, UserCache } from "Cache";
|
||||
import AvatarEditor from "Element/AvatarEditor";
|
||||
|
||||
import messages from "./messages";
|
||||
import { DISCOVER } from ".";
|
||||
|
||||
export default function NewUserName() {
|
||||
export default function ProfileSetup() {
|
||||
const login = useLogin();
|
||||
const myProfile = useUserProfile(login.publicKey);
|
||||
const [username, setUsername] = useState("");
|
||||
const [picture, setPicture] = useState("");
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onNext = async () => {
|
||||
if (username.length > 0 && publisher) {
|
||||
const ev = await publisher.metadata({ name: username });
|
||||
publisher.broadcast(ev);
|
||||
useEffect(() => {
|
||||
if (myProfile) {
|
||||
setUsername(myProfile.name ?? "");
|
||||
setPicture(myProfile.picture ?? "");
|
||||
}
|
||||
navigate("/new/verify");
|
||||
}, [myProfile]);
|
||||
|
||||
const onNext = async () => {
|
||||
if ((username.length > 0 || picture.length > 0) && publisher) {
|
||||
const ev = await publisher.metadata({
|
||||
...myProfile,
|
||||
name: username,
|
||||
picture,
|
||||
});
|
||||
publisher.broadcast(ev);
|
||||
const profile = mapEventToProfile(ev);
|
||||
if (profile) {
|
||||
UserCache.set(profile);
|
||||
}
|
||||
}
|
||||
navigate(DISCOVER);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -28,13 +51,14 @@ export default function NewUserName() {
|
|||
<div className="progress progress-second"></div>
|
||||
</div>
|
||||
<h1>
|
||||
<FormattedMessage {...messages.PickUsername} />
|
||||
<FormattedMessage defaultMessage="Setup profile" />
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage {...messages.UsernameHelp} />
|
||||
</p>
|
||||
<h2>
|
||||
<FormattedMessage {...messages.Username} />
|
||||
<FormattedMessage defaultMessage="Profile picture" />
|
||||
</h2>
|
||||
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} />
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Username" />
|
||||
</h2>
|
||||
<input
|
||||
className="username"
|
||||
|
@ -47,7 +71,7 @@ export default function NewUserName() {
|
|||
<FormattedMessage defaultMessage="You can change your username at any point." />
|
||||
</div>
|
||||
<div className="next-actions">
|
||||
<button type="button" className="transparent" onClick={() => navigate("/new/verify")}>
|
||||
<button type="button" className="transparent" onClick={() => navigate(DISCOVER)}>
|
||||
<FormattedMessage {...messages.Skip} />
|
||||
</button>
|
||||
<button type="button" onClick={onNext}>
|
|
@ -2,15 +2,15 @@ import "./index.css";
|
|||
import { RouteObject } from "react-router-dom";
|
||||
|
||||
import GetVerified from "Pages/new/GetVerified";
|
||||
import NewUserName from "Pages/new/NewUsername";
|
||||
import ProfileSetup from "Pages/new/ProfileSetup";
|
||||
import NewUserFlow from "Pages/new/NewUserFlow";
|
||||
import ImportFollows from "Pages/new/ImportFollows";
|
||||
import DiscoverFollows from "Pages/new/DiscoverFollows";
|
||||
|
||||
const USERNAME = "/new/username";
|
||||
const IMPORT = "/new/import";
|
||||
const DISCOVER = "/new/discover";
|
||||
const VERIFY = "/new/verify";
|
||||
export const PROFILE = "/new/profile";
|
||||
export const IMPORT = "/new/import";
|
||||
export const DISCOVER = "/new/discover";
|
||||
export const VERIFY = "/new/verify";
|
||||
|
||||
export const NewUserRoutes: RouteObject[] = [
|
||||
{
|
||||
|
@ -18,8 +18,8 @@ export const NewUserRoutes: RouteObject[] = [
|
|||
element: <NewUserFlow />,
|
||||
},
|
||||
{
|
||||
path: USERNAME,
|
||||
element: <NewUserName />,
|
||||
path: PROFILE,
|
||||
element: <ProfileSetup />,
|
||||
},
|
||||
{
|
||||
path: IMPORT,
|
||||
|
|
|
@ -37,16 +37,12 @@ export default defineMessages({
|
|||
ExtensionsNostr: { defaultMessage: `You can also use these extensions to login to most Nostr sites.` },
|
||||
ImproveSecurity: { defaultMessage: "Improve login security with browser extensions" },
|
||||
PickUsername: { defaultMessage: "Pick a username" },
|
||||
UsernameHelp: {
|
||||
defaultMessage:
|
||||
"On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
|
||||
},
|
||||
Username: { defaultMessage: "Username" },
|
||||
UsernamePlaceholder: { defaultMessage: "e.g. Jack" },
|
||||
PopularAccounts: { defaultMessage: "Follow some popular accounts" },
|
||||
Skip: { defaultMessage: "Skip" },
|
||||
Done: { defaultMessage: "Done!" },
|
||||
ImportTwitter: { defaultMessage: "Import Twitter Follows (optional)" },
|
||||
ImportTwitter: { defaultMessage: "Import Twitter Follows" },
|
||||
TwitterPlaceholder: { defaultMessage: "Twitter username..." },
|
||||
FindYourFollows: { defaultMessage: "Find your twitter follows on nostr (Data provided by {provider})" },
|
||||
TwitterUsername: { defaultMessage: "Twitter username" },
|
||||
|
@ -56,7 +52,7 @@ export default defineMessages({
|
|||
Check: { defaultMessage: "Check" },
|
||||
Next: { defaultMessage: "Next" },
|
||||
SetupProfile: { defaultMessage: "Setup your Profile" },
|
||||
Identifier: { defaultMessage: "Get an identifier (optional)" },
|
||||
Identifier: { defaultMessage: "Get an identifier" },
|
||||
IdentifierHelp: {
|
||||
defaultMessage:
|
||||
"Getting an identifier helps confirm the real you to people who know you. Many people can have a username @jack, but there is only one jack@cash.app.",
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import { LoginStore } from "Login";
|
||||
import useLoginHandler from "Hooks/useLoginHandler";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import { getActiveSubscriptions } from "Subscription";
|
||||
|
||||
export default function AccountsPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const loginHandler = useLoginHandler();
|
||||
const logins = LoginStore.getSessions();
|
||||
const sub = getActiveSubscriptions(LoginStore.allSubscriptions());
|
||||
|
||||
async function doLogin() {
|
||||
try {
|
||||
setError("");
|
||||
await loginHandler.doLogin(key);
|
||||
setKey("");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError(
|
||||
formatMessage({
|
||||
defaultMessage: "Unknown login error",
|
||||
})
|
||||
);
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Logins" />
|
||||
</h3>
|
||||
{logins.map(a => (
|
||||
<div className="card flex" key={a}>
|
||||
<ProfilePreview
|
||||
pubkey={a}
|
||||
options={{
|
||||
about: false,
|
||||
}}
|
||||
actions={
|
||||
<div className="f-1">
|
||||
<button className="mb10" onClick={() => LoginStore.switchAccount(a)}>
|
||||
<FormattedMessage defaultMessage="Switch" />
|
||||
</button>
|
||||
<button onClick={() => LoginStore.removeSession(a)}>
|
||||
<FormattedMessage defaultMessage="Logout" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{sub && (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Add Account" />
|
||||
</h3>
|
||||
<div className="flex">
|
||||
<input
|
||||
dir="auto"
|
||||
type="text"
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
|
||||
})}
|
||||
className="f-grow mr10"
|
||||
onChange={e => setKey(e.target.value)}
|
||||
/>
|
||||
<AsyncButton onClick={() => doLogin()}>
|
||||
<FormattedMessage defaultMessage="Login" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{error && <b className="error">{error}</b>}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -13,16 +13,30 @@
|
|||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
gap: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.settings-row:hover {
|
||||
.settings-row.inner {
|
||||
padding: 0.8em 0;
|
||||
background-color: unset;
|
||||
border-radius: unset;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-group-header {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
padding: 0.8em 1em;
|
||||
background-color: var(--note-bg);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.settings-row:hover,
|
||||
.settings-group-header:hover {
|
||||
color: var(--highlight);
|
||||
}
|
||||
|
||||
.settings-row + .settings-row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.align-end {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
@ -31,3 +45,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.settings-group-header .collapse-icon > svg {
|
||||
width: 8px;
|
||||
}
|
||||
|
|
|
@ -2,15 +2,18 @@ import "./Index.css";
|
|||
import { FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Icon from "Icons/Icon";
|
||||
import { logout } from "Login";
|
||||
import { LoginStore, logout } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { unwrap } from "Util";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
import { CollapsedSection } from "Element/Collapsed";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
const SettingsIndex = () => {
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
||||
|
||||
function handleLogout() {
|
||||
logout(unwrap(login.publicKey));
|
||||
|
@ -20,21 +23,56 @@ const SettingsIndex = () => {
|
|||
return (
|
||||
<>
|
||||
<div className="settings-nav">
|
||||
<div className="settings-row" onClick={() => navigate("profile")}>
|
||||
<Icon name="profile" />
|
||||
<FormattedMessage {...messages.Profile} />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("relays")}>
|
||||
<Icon name="relay" />
|
||||
<FormattedMessage {...messages.Relays} />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<CollapsedSection
|
||||
title={
|
||||
<div className="flex">
|
||||
<Icon name="user" className="mr10" />
|
||||
<FormattedMessage defaultMessage="Account" />
|
||||
</div>
|
||||
}
|
||||
className="settings-group-header">
|
||||
<div className="card">
|
||||
<div className="settings-row inner" onClick={() => navigate("profile")}>
|
||||
<Icon name="profile" />
|
||||
<FormattedMessage {...messages.Profile} />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row inner" onClick={() => navigate("relays")}>
|
||||
<Icon name="relay" />
|
||||
<FormattedMessage {...messages.Relays} />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row inner" onClick={() => navigate("keys")}>
|
||||
<Icon name="key" />
|
||||
<FormattedMessage defaultMessage="Export Keys" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row inner" onClick={() => navigate("handle")}>
|
||||
<Icon name="badge" />
|
||||
<FormattedMessage defaultMessage="Nostr Adddress" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row inner" onClick={() => navigate("/subscribe/manage")}>
|
||||
<Icon name="diamond" />
|
||||
<FormattedMessage defaultMessage="Subscription" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
{sub && (
|
||||
<div className="settings-row inner" onClick={() => navigate("accounts")}>
|
||||
<Icon name="code-circle" />
|
||||
<FormattedMessage defaultMessage="Account Switcher" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CollapsedSection>
|
||||
|
||||
<div className="settings-row" onClick={() => navigate("preferences")}>
|
||||
<Icon name="gear" />
|
||||
<FormattedMessage {...messages.Preferences} />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
|
||||
<div className="settings-row" onClick={() => navigate("wallet")}>
|
||||
<Icon name="wallet" />
|
||||
<FormattedMessage defaultMessage="Wallet" />
|
||||
|
@ -45,16 +83,7 @@ const SettingsIndex = () => {
|
|||
<FormattedMessage {...messages.Donate} />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("handle")}>
|
||||
<Icon name="badge" />
|
||||
<FormattedMessage defaultMessage="Snort Nostr Adddress" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("/subscribe/manage")}>
|
||||
<Icon name="diamond" />
|
||||
<FormattedMessage defaultMessage="Snort Subscription" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
|
||||
<div className="settings-row" onClick={handleLogout}>
|
||||
<Icon name="logout" />
|
||||
<FormattedMessage {...messages.LogOut} />
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.export-keys > .copy {
|
||||
padding: 12px 16px;
|
||||
border: 2px dashed #222222;
|
||||
border-radius: 16px;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import "./Keys.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { encodeTLV, NostrPrefix } from "@snort/nostr";
|
||||
|
||||
import Copy from "Element/Copy";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { hexToMnemonic } from "nip6";
|
||||
import { hexToBech32 } from "Util";
|
||||
|
||||
export default function ExportKeys() {
|
||||
const { publicKey, privateKey, generatedEntropy } = useLogin();
|
||||
return (
|
||||
<div className="export-keys">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Public Key" />
|
||||
</h3>
|
||||
<Copy text={hexToBech32("npub", publicKey ?? "")} maxSize={48} className="mb10" />
|
||||
<Copy text={encodeTLV(publicKey ?? "", NostrPrefix.Profile)} maxSize={48} />
|
||||
{privateKey && (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Private Key" />
|
||||
</h3>
|
||||
<Copy text={hexToBech32("nsec", privateKey)} maxSize={48} />
|
||||
</>
|
||||
)}
|
||||
{generatedEntropy && (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Mnemonic" />
|
||||
</h3>
|
||||
<Copy text={hexToMnemonic(generatedEntropy ?? "")} maxSize={48} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -38,7 +38,7 @@ const PreferencesPage = () => {
|
|||
})
|
||||
}
|
||||
style={{ textTransform: "capitalize" }}>
|
||||
{["en", "ja", "es", "hu", "zh-CN", "zh-TW", "fr", "ar", "it", "id", "de", "ru", "sv", "hr"]
|
||||
{["en", "ja", "es", "hu", "zh-CN", "zh-TW", "fr", "ar", "it", "id", "de", "ru", "sv", "hr", "ta-IN"]
|
||||
.sort()
|
||||
.map(a => (
|
||||
<option value={a}>
|
||||
|
|
|
@ -22,29 +22,31 @@
|
|||
|
||||
.settings .image-setting {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.settings .image-setting > div:first-child {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.settings .avatar,
|
||||
.settings .banner {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.settings .avatar .edit,
|
||||
.settings .banner .edit {
|
||||
.avatar .edit,
|
||||
.banner .edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background-color: var(--bg-color);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.settings .avatar .edit:hover {
|
||||
.avatar .edit.new {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.avatar .edit:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,28 +5,26 @@ import { FormattedMessage } from "react-intl";
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faShop } from "@fortawesome/free-solid-svg-icons";
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { hexToBech32, openFile } from "Util";
|
||||
import Copy from "Element/Copy";
|
||||
import { openFile } from "Util";
|
||||
import useFileUpload from "Upload";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import { mapEventToProfile, UserCache } from "Cache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import AvatarEditor from "Element/AvatarEditor";
|
||||
|
||||
export interface ProfileSettingsProps {
|
||||
avatar?: boolean;
|
||||
banner?: boolean;
|
||||
privateKey?: boolean;
|
||||
}
|
||||
|
||||
export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
const navigate = useNavigate();
|
||||
const { publicKey: id, privateKey: privKey } = useLogin();
|
||||
const { publicKey: id } = useLogin();
|
||||
const user = useUserProfile(id ?? "");
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
|
@ -80,7 +78,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||
const ev = await publisher.metadata(userCopy);
|
||||
publisher.broadcast(ev);
|
||||
|
||||
const newProfile = mapEventToProfile(ev as TaggedRawEvent);
|
||||
const newProfile = mapEventToProfile(ev);
|
||||
if (newProfile) {
|
||||
await UserCache.set(newProfile);
|
||||
}
|
||||
|
@ -100,13 +98,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||
}
|
||||
}
|
||||
|
||||
async function setNewAvatar() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setPicture(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
async function setNewBanner() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
|
@ -155,12 +146,10 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||
</div>
|
||||
<div>
|
||||
<input type="text" className="mr10" value={nip05} onChange={e => setNip05(e.target.value)} />
|
||||
{nip05 === "" && (
|
||||
<button type="button" onClick={() => navigate("/verification")}>
|
||||
<FontAwesomeIcon icon={faShop} />
|
||||
<FormattedMessage {...messages.Buy} />
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => navigate("/verification")}>
|
||||
<FontAwesomeIcon icon={faShop} />
|
||||
<FormattedMessage {...messages.Buy} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group card">
|
||||
|
@ -193,11 +182,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||
<div>
|
||||
<FormattedMessage {...messages.Avatar} />:
|
||||
</div>
|
||||
<div style={{ backgroundImage: `url(${avatarPicture})` }} className="avatar">
|
||||
<div className="edit" onClick={() => setNewAvatar()}>
|
||||
<FormattedMessage {...messages.Edit} />
|
||||
</div>
|
||||
</div>
|
||||
<AvatarEditor picture={avatarPicture} onPictureChange={p => setPicture(p)} />
|
||||
</div>
|
||||
)}
|
||||
{(props.banner ?? true) && (
|
||||
|
@ -228,18 +213,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||
<FormattedMessage {...messages.EditProfile} />
|
||||
</h3>
|
||||
{settings()}
|
||||
{privKey && (props.privateKey ?? true) && (
|
||||
<div className="flex f-col bg-grey">
|
||||
<div>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.PrivateKey} />:
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<Copy text={hexToBech32("nsec", privKey)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,12 @@ import { RouteObject, useNavigate } from "react-router-dom";
|
|||
|
||||
import BlueWallet from "Icons/BlueWallet";
|
||||
import ConnectLNC from "Pages/settings/wallet/LNC";
|
||||
import ConnectLNDHub from "./wallet/LNDHub";
|
||||
import ConnectLNDHub from "Pages/settings/wallet/LNDHub";
|
||||
import ConnectNostrWallet from "Pages/settings/wallet/NWC";
|
||||
import ConnectCashu from "Pages/settings/wallet/Cashu";
|
||||
|
||||
import NostrIcon from "Icons/Nostrich";
|
||||
import CashuLogo from "cashu.png";
|
||||
|
||||
const WalletSettings = () => {
|
||||
const navigate = useNavigate();
|
||||
|
@ -19,12 +24,18 @@ const WalletSettings = () => {
|
|||
<img src={LndLogo} width={100} />
|
||||
<h3 className="f-end">LND with LNC</h3>
|
||||
</div>
|
||||
{
|
||||
<div className="card" onClick={() => navigate("/settings/wallet/lndhub")}>
|
||||
<BlueWallet width={100} height={100} />
|
||||
<h3 className="f-end">LNDHub</h3>
|
||||
</div>
|
||||
}
|
||||
<div className="card" onClick={() => navigate("/settings/wallet/lndhub")}>
|
||||
<BlueWallet width={100} height={100} />
|
||||
<h3 className="f-end">LNDHub</h3>
|
||||
</div>
|
||||
<div className="card" onClick={() => navigate("/settings/wallet/nwc")}>
|
||||
<NostrIcon width={100} height={100} />
|
||||
<h3 className="f-end">Nostr Wallet Connect</h3>
|
||||
</div>
|
||||
{/*<div className="card" onClick={() => navigate("/settings/wallet/cashu")}>
|
||||
<img src={CashuLogo} width={100} />
|
||||
<h3 className="f-end">Cashu</h3>
|
||||
</div>*/}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -45,4 +56,12 @@ export const WalletSettingsRoutes = [
|
|||
path: "/settings/wallet/lndhub",
|
||||
element: <ConnectLNDHub />,
|
||||
},
|
||||
{
|
||||
path: "/settings/wallet/nwc",
|
||||
element: <ConnectNostrWallet />,
|
||||
},
|
||||
{
|
||||
path: "/settings/wallet/cashu",
|
||||
element: <ConnectCashu />,
|
||||
},
|
||||
] as Array<RouteObject>;
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import { unwrap } from "Util";
|
||||
import { CashuWallet } from "Wallet/Cashu";
|
||||
import { WalletConfig, WalletKind, Wallets } from "Wallet";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const ConnectCashu = () => {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const [mintUrl, setMintUrl] = useState<string>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
async function tryConnect(config: string) {
|
||||
try {
|
||||
if (!mintUrl) {
|
||||
throw new Error("Mint URL is required");
|
||||
}
|
||||
const connection = new CashuWallet(config);
|
||||
await connection.login();
|
||||
const info = await connection.getInfo();
|
||||
const newWallet = {
|
||||
id: uuid(),
|
||||
kind: WalletKind.Cashu,
|
||||
active: true,
|
||||
info,
|
||||
data: mintUrl,
|
||||
} as WalletConfig;
|
||||
Wallets.add(newWallet);
|
||||
navigate("/wallet");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError((e as Error).message);
|
||||
} else {
|
||||
setError(
|
||||
formatMessage({
|
||||
defaultMessage: "Unknown error",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Enter mint URL" />
|
||||
</h4>
|
||||
<div className="flex">
|
||||
<div className="f-grow mr10">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Mint URL"
|
||||
className="w-max"
|
||||
value={mintUrl}
|
||||
onChange={e => setMintUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<AsyncButton onClick={() => tryConnect(unwrap(mintUrl))} disabled={!mintUrl}>
|
||||
<FormattedMessage defaultMessage="Connect" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{error && <b className="error p10">{error}</b>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectCashu;
|
|
@ -0,0 +1,70 @@
|
|||
import { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import { unwrap } from "Util";
|
||||
import { WalletConfig, WalletKind, Wallets } from "Wallet";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NostrConnectWallet } from "Wallet/NostrWalletConnect";
|
||||
|
||||
const ConnectNostrWallet = () => {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const [config, setConfig] = useState<string>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
async function tryConnect(config: string) {
|
||||
try {
|
||||
const connection = new NostrConnectWallet(config);
|
||||
await connection.login();
|
||||
const info = await connection.getInfo();
|
||||
|
||||
const newWallet = {
|
||||
id: uuid(),
|
||||
kind: WalletKind.NWC,
|
||||
active: true,
|
||||
info,
|
||||
data: config,
|
||||
} as WalletConfig;
|
||||
Wallets.add(newWallet);
|
||||
|
||||
navigate("/wallet");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError((e as Error).message);
|
||||
} else {
|
||||
setError(
|
||||
formatMessage({
|
||||
defaultMessage: "Unknown error",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Enter Nostr Wallet Connect config" />
|
||||
</h4>
|
||||
<div className="flex">
|
||||
<div className="f-grow mr10">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="nostr+walletconnect:<pubkey>?relay=<relay>&secret=<secret>"
|
||||
className="w-max"
|
||||
value={config}
|
||||
onChange={e => setConfig(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<AsyncButton onClick={() => tryConnect(unwrap(config))} disabled={!config}>
|
||||
<FormattedMessage defaultMessage="Connect" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{error && <b className="error p10">{error}</b>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectNostrWallet;
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
|
@ -11,6 +11,7 @@ import SubscriptionCard from "./SubscriptionCard";
|
|||
export default function ManageSubscriptionPage() {
|
||||
const publisher = useEventPublisher();
|
||||
const api = new SnortApi(undefined, publisher);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [subs, setSubs] = useState<Array<Subscription>>();
|
||||
const [error, setError] = useState<SubscriptionError>();
|
||||
|
@ -39,6 +40,11 @@ export default function ManageSubscriptionPage() {
|
|||
{subs.map(a => (
|
||||
<SubscriptionCard sub={a} key={a.id} />
|
||||
))}
|
||||
{subs.length !== 0 && (
|
||||
<button onClick={() => navigate("/subscribe")}>
|
||||
<FormattedMessage defaultMessage="Buy Subscription" />
|
||||
</button>
|
||||
)}
|
||||
{subs.length === 0 && (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.subscribe-page > div.card {
|
||||
margin: 5px;
|
||||
min-height: 350px;
|
||||
min-height: 400px;
|
||||
user-select: none;
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,12 @@ export function mapFeatureName(k: LockedFeatures) {
|
|||
return <FormattedMessage defaultMessage="Unlimited note retention on Snort relay" />;
|
||||
case LockedFeatures.RelayBackup:
|
||||
return <FormattedMessage defaultMessage="Downloadable backups from Snort relay" />;
|
||||
case LockedFeatures.RelayAccess:
|
||||
return <FormattedMessage defaultMessage="Write access to Snort relay, with 1 year of event retention" />;
|
||||
case LockedFeatures.LNProxy:
|
||||
return <FormattedMessage defaultMessage="LN Address Proxy" />;
|
||||
case LockedFeatures.EmailBridge:
|
||||
return <FormattedMessage defaultMessage="Email <> DM bridge for your Snort nostr address" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,9 +94,6 @@ export function SubscribePage() {
|
|||
/>
|
||||
:
|
||||
</p>
|
||||
<b>
|
||||
<FormattedMessage defaultMessage="Not all features are built yet, more features to be added soon!" />
|
||||
</b>
|
||||
<ul>
|
||||
{a.unlocks.map(b => (
|
||||
<li>{mapFeatureName(b)} </li>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue