Profile / Thread styles

This commit is contained in:
Kieran 2023-07-24 15:30:21 +01:00
parent d292e658fc
commit 076d5d8cd5
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
67 changed files with 684 additions and 602 deletions

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
.yarn/
build/
.vscode/
.github/
transifex.yml
dist/

View File

@ -14,5 +14,6 @@
},
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"eslint.nodePath": ".yarn/sdks"
"eslint.nodePath": ".yarn/sdks",
"prettier.prettierPath": ".yarn/sdks/prettier/index.js"
}

View File

@ -1,4 +1,4 @@
yarnPath: .yarn/releases/yarn-3.6.1.cjs
npmScopes:
void-cat:
npmRegistryServer: https://git.v0l.io/api/packages/Kieran/npm/
npmRegistryServer: https://git.v0l.io/api/packages/Kieran/npm/

View File

@ -9,8 +9,9 @@
"test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230307.0",
"@tauri-apps/cli": "^1.2.3",
"@cloudflare/workers-types": "^4.20230307.0"
"prettier": "^3.0.0"
},
"prettier": {
"printWidth": 120,

View File

@ -1,3 +0,0 @@
build/
.github/
transifex.yml

View File

@ -40,7 +40,7 @@
<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" />
</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" />
@ -231,6 +231,57 @@
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4999 0.833313C5.43731 0.833313 1.33325 4.93737 1.33325 9.99998C1.33325 15.0626 5.43731 19.1666 10.4999 19.1666C15.5625 19.1666 19.6666 15.0626 19.6666 9.99998C19.6666 4.93737 15.5625 0.833313 10.4999 0.833313ZM3.4212 7.51555C3.14831 8.2931 2.99992 9.12921 2.99992 9.99998C2.99992 14.1421 6.35778 17.5 10.4999 17.5C14.6421 17.5 17.9999 14.1421 17.9999 9.99998C17.9999 6.80623 16.0037 4.07873 13.1909 2.99718C13.1217 4.03512 13.4842 5.67357 12.6141 6.4745C11.7016 7.31445 10.0684 7.67798 8.95316 8.17356L8.21274 9.16081C8.15449 9.23853 8.08464 9.33174 8.01635 9.40895C7.93696 9.49869 7.81503 9.62 7.63588 9.70944C7.40325 9.82557 7.1402 9.86628 6.88336 9.82589C6.68555 9.79478 6.53265 9.716 6.42986 9.65445C6.34141 9.60149 6.24668 9.53377 6.16766 9.47729L3.4212 7.51555Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="pencil" viewBox="0 0 16 17" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4714 2.02846C13.4747 1.03173 11.8587 1.03173 10.862 2.02846L3.31758 9.57282C3.1619 9.72843 3.04412 9.84615 2.94092 9.97913C2.84971 10.0967 2.76849 10.2217 2.69812 10.3528C2.6185 10.5011 2.55877 10.6565 2.4798 10.862L1.04446 14.5939C1.01453 14.6717 0.99999 14.7528 1 14.8332C1.00001 15.0068 1.06793 15.1773 1.19528 15.3046C1.38158 15.4909 1.66011 15.5501 1.90601 15.4555L5.4925 14.0761C5.51286 14.0682 5.53364 14.0602 5.55452 14.0522L5.63792 14.0201C5.84336 13.9411 5.99883 13.8814 6.14713 13.8018C6.27823 13.7314 6.40321 13.6502 6.52075 13.559C6.65374 13.4558 6.77144 13.338 6.92704 13.1823L14.4714 5.63794C15.4682 4.64121 15.4682 3.02519 14.4714 2.02846ZM3.58733 11.6967L2.82738 13.6725L4.80321 12.9126L3.58733 11.6967Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="stars" viewBox="0 0 16 17" fill="none">
<g>
<path d="M3.66663 1.83332C3.66663 1.46513 3.36815 1.16666 2.99996 1.16666C2.63177 1.16666 2.33329 1.46513 2.33329 1.83332V2.83332H1.33329C0.965103 2.83332 0.666626 3.1318 0.666626 3.49999C0.666626 3.86818 0.965103 4.16666 1.33329 4.16666H2.33329V5.16666C2.33329 5.53485 2.63177 5.83332 2.99996 5.83332C3.36815 5.83332 3.66663 5.53485 3.66663 5.16666V4.16666H4.66663C5.03482 4.16666 5.33329 3.86818 5.33329 3.49999C5.33329 3.1318 5.03482 2.83332 4.66663 2.83332H3.66663V1.83332Z" fill="currentColor"/>
<path d="M3.66663 11.8333C3.66663 11.4651 3.36815 11.1667 2.99996 11.1667C2.63177 11.1667 2.33329 11.4651 2.33329 11.8333V12.8333H1.33329C0.965103 12.8333 0.666626 13.1318 0.666626 13.5C0.666626 13.8682 0.965103 14.1667 1.33329 14.1667H2.33329V15.1667C2.33329 15.5348 2.63177 15.8333 2.99996 15.8333C3.36815 15.8333 3.66663 15.5348 3.66663 15.1667V14.1667H4.66663C5.03482 14.1667 5.33329 13.8682 5.33329 13.5C5.33329 13.1318 5.03482 12.8333 4.66663 12.8333H3.66663V11.8333Z" fill="currentColor"/>
<path d="M9.28886 2.26067C9.18983 2.00321 8.94247 1.83332 8.66663 1.83332C8.39078 1.83332 8.14342 2.00321 8.04439 2.26067L6.88828 5.26658C6.68801 5.78728 6.62508 5.93732 6.539 6.05838C6.45262 6.17986 6.34649 6.28599 6.22502 6.37236C6.10396 6.45844 5.95391 6.52137 5.43321 6.72164L2.42731 7.87776C2.16985 7.97678 1.99996 8.22414 1.99996 8.49999C1.99996 8.77584 2.16985 9.0232 2.42731 9.12222L5.43322 10.2783C5.95391 10.4786 6.10396 10.5415 6.22502 10.6276C6.34649 10.714 6.45262 10.8201 6.539 10.9416C6.62508 11.0627 6.68801 11.2127 6.88828 11.7334L8.0444 14.7393C8.14342 14.9968 8.39078 15.1667 8.66663 15.1667C8.94247 15.1667 9.18983 14.9968 9.28886 14.7393L10.445 11.7334C10.6452 11.2127 10.7082 11.0627 10.7943 10.9416C10.8806 10.8201 10.9868 10.714 11.1082 10.6276C11.2293 10.5415 11.3793 10.4786 11.9 10.2783L14.9059 9.12222C15.1634 9.0232 15.3333 8.77584 15.3333 8.49999C15.3333 8.22414 15.1634 7.97678 14.9059 7.87776L11.9 6.72164C11.3793 6.52137 11.2293 6.45844 11.1082 6.37236C10.9868 6.28599 10.8806 6.17986 10.7943 6.05838C10.7082 5.93732 10.6452 5.78728 10.445 5.26658L9.28886 2.26067Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="link-02" viewBox="0 0 17 16" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47432 8C4.47432 7.63181 4.7728 7.33333 5.14099 7.33333L11.8077 7.33333C12.1758 7.33333 12.4743 7.63181 12.4743 8C12.4743 8.36819 12.1758 8.66667 11.8077 8.66667L5.14099 8.66667C4.7728 8.66667 4.47432 8.36819 4.47432 8Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.14099 5.33333C3.66823 5.33333 2.47432 6.52724 2.47432 8C2.47432 9.47276 3.66823 10.6667 5.14099 10.6667H6.47432C6.84251 10.6667 7.14099 10.9651 7.14099 11.3333C7.14099 11.7015 6.84251 12 6.47432 12H5.14099C2.93185 12 1.14099 10.2091 1.14099 8C1.14099 5.79086 2.93185 4 5.14099 4H6.47432C6.84251 4 7.14099 4.29848 7.14099 4.66667C7.14099 5.03486 6.84251 5.33333 6.47432 5.33333H5.14099Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.80766 4.66667C9.80766 4.29848 10.1061 4 10.4743 4H11.8077C14.0168 4 15.8077 5.79086 15.8077 8C15.8077 10.2091 14.0168 12 11.8077 12H10.4743C10.1061 12 9.80766 11.7015 9.80766 11.3333C9.80766 10.9651 10.1061 10.6667 10.4743 10.6667H11.8077C13.2804 10.6667 14.4743 9.47276 14.4743 8C14.4743 6.52724 13.2804 5.33333 11.8077 5.33333H10.4743C10.1061 5.33333 9.80766 5.03486 9.80766 4.66667Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="bookmark-solid" viewBox="0 0 16 17" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.50582 1.83334H9.49422C10.0309 1.83334 10.4738 1.83333 10.8346 1.86281C11.2093 1.89342 11.5538 1.95913 11.8773 2.12399C12.3791 2.37966 12.787 2.7876 13.0427 3.28937C13.2076 3.61293 13.2733 3.9574 13.3039 4.33213C13.3334 4.69293 13.3334 5.13584 13.3334 5.67249V14.5C13.3334 14.7377 13.2068 14.9573 13.0013 15.0766C12.7958 15.1959 12.5423 15.1967 12.3359 15.0788L8.00002 12.6012L3.66411 15.0788C3.45778 15.1967 3.20428 15.1959 2.99874 15.0766C2.79319 14.9573 2.66669 14.7377 2.66669 14.5L2.66669 5.67248C2.66668 5.13583 2.66667 4.69292 2.69615 4.33213C2.72677 3.9574 2.79248 3.61293 2.95734 3.28937C3.213 2.7876 3.62095 2.37966 4.12271 2.12399C4.44627 1.95913 4.79074 1.89342 5.16547 1.86281C5.52627 1.83333 5.96917 1.83334 6.50582 1.83334Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="zap-solid" viewBox="0 0 16 17" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.93212 1.2218C9.2036 1.33964 9.36488 1.62235 9.32818 1.91602L8.75518 6.5L12.8852 6.49999C13.0458 6.49996 13.2086 6.49993 13.3415 6.51197C13.4673 6.52336 13.708 6.55347 13.9168 6.72221C14.1559 6.91539 14.2928 7.20776 14.2882 7.51507C14.2841 7.78351 14.1531 7.98776 14.0814 8.09164C14.0055 8.20149 13.9013 8.32648 13.7984 8.44989L7.84547 15.5935C7.65601 15.8208 7.33934 15.896 7.06786 15.7782C6.79638 15.6604 6.6351 15.3776 6.6718 15.084L7.2448 10.5L3.11481 10.5C2.95415 10.5 2.79142 10.5001 2.65847 10.488C2.53273 10.4766 2.29195 10.4465 2.08314 10.2778C1.84409 10.0846 1.70715 9.79223 1.71178 9.48492C1.71583 9.21648 1.84684 9.01223 1.91859 8.90835C1.99445 8.79851 2.09866 8.67351 2.20153 8.55011C2.20663 8.54399 2.21172 8.53788 2.21681 8.53178L8.15451 1.40654C8.34397 1.17918 8.66064 1.10395 8.93212 1.2218Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="repeat" viewBox="0 0 18 19" fill="none">
<g>
<path d="M12.2197 1.65717C12.5126 1.36428 12.9874 1.36428 13.2803 1.65717L16.2803 4.65717C16.5732 4.95006 16.5732 5.42494 16.2803 5.71783L13.2803 8.71783C12.9874 9.01072 12.5126 9.01072 12.2197 8.71783C11.9268 8.42494 11.9268 7.95006 12.2197 7.65717L13.9393 5.9375H5.85C5.20757 5.9375 4.77085 5.93808 4.43328 5.96566C4.10447 5.99253 3.93632 6.04122 3.81902 6.10099C3.53677 6.2448 3.3073 6.47427 3.16349 6.75652C3.10372 6.87381 3.05503 7.04197 3.02816 7.37078C3.00058 7.70835 3 8.14507 3 8.7875V8.9375C3 9.35171 2.66421 9.6875 2.25 9.6875C1.83579 9.6875 1.5 9.35171 1.5 8.9375V8.75653C1.49999 8.15281 1.49998 7.65452 1.53315 7.24863C1.56759 6.82706 1.64151 6.43953 1.82698 6.07553C2.1146 5.51104 2.57354 5.0521 3.13803 4.76448C3.50203 4.57901 3.88956 4.50509 4.31113 4.47065C4.71703 4.43748 5.2153 4.43749 5.81903 4.4375L13.9393 4.4375L12.2197 2.71783C11.9268 2.42494 11.9268 1.95006 12.2197 1.65717Z" fill="currentColor"/>
<path d="M15.75 9.6875C15.3358 9.6875 15 10.0233 15 10.4375V10.5875C15 11.2299 14.9994 11.6667 14.9718 12.0042C14.945 12.333 14.8963 12.5012 14.8365 12.6185C14.6927 12.9007 14.4632 13.1302 14.181 13.274C14.0637 13.3338 13.8955 13.3825 13.5667 13.4093C13.2292 13.4369 12.7924 13.4375 12.15 13.4375H4.06066L5.78033 11.7178C6.07322 11.4249 6.07322 10.9501 5.78033 10.6572C5.48744 10.3643 5.01256 10.3643 4.71967 10.6572L1.71967 13.6572C1.42678 13.9501 1.42678 14.4249 1.71967 14.7178L4.71967 17.7178C5.01256 18.0107 5.48744 18.0107 5.78033 17.7178C6.07322 17.4249 6.07322 16.9501 5.78033 16.6572L4.06066 14.9375H12.181C12.7847 14.9375 13.283 14.9375 13.6889 14.9044C14.1104 14.8699 14.498 14.796 14.862 14.6105C15.4265 14.3229 15.8854 13.864 16.173 13.2995C16.3585 12.9355 16.4324 12.5479 16.4669 12.1264C16.5 11.7205 16.5 11.2222 16.5 10.6185V10.4375C16.5 10.0233 16.1642 9.6875 15.75 9.6875Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="check-verified" viewBox="0 0 16 17" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.35368 8.68746C1.35348 8.59824 1.33596 8.50912 1.30112 8.42515L0.820156 7.26063C0.718546 7.01538 0.666058 6.75212 0.666016 6.48665C0.665973 6.22104 0.718266 5.95803 0.819908 5.71263C0.92155 5.46724 1.07055 5.24428 1.25839 5.05649C1.44618 4.86875 1.66912 4.71985 1.91446 4.61828L3.07708 4.13669C3.24546 4.06707 3.37975 3.93337 3.44985 3.76519L3.93155 2.60222C4.13674 2.10683 4.53031 1.71323 5.0257 1.50803C5.52109 1.30283 6.0777 1.30282 6.57309 1.50803L7.73517 1.9894C7.9038 2.05901 8.09336 2.05892 8.26187 1.989L8.26319 1.98845L9.42629 1.50874C9.92151 1.30381 10.4782 1.30375 10.9734 1.50885C11.4687 1.71402 11.8622 2.1075 12.0674 2.60276L12.5376 3.73781C12.5417 3.74677 12.5457 3.75585 12.5496 3.76507C12.6192 3.93362 12.753 4.06762 12.9213 4.13766L14.0846 4.61953C14.58 4.82473 14.9736 5.21833 15.1788 5.71372C15.384 6.20911 15.384 6.76572 15.1788 7.26111L14.6972 8.42378C14.6622 8.50822 14.6448 8.59818 14.6447 8.68766C14.6448 8.77714 14.6622 8.8667 14.6972 8.95114L15.1788 10.1138C15.384 10.6092 15.384 11.1658 15.1788 11.6612C14.9736 12.1566 14.58 12.5502 14.0846 12.7554L12.9213 13.2373C12.753 13.3073 12.6192 13.4413 12.5496 13.6099C12.5457 13.6191 12.5417 13.6282 12.5376 13.6371L12.0674 14.7722C11.8622 15.2674 11.4687 15.6609 10.9734 15.8661C10.4782 16.0712 9.92151 16.0711 9.42629 15.8662L8.26319 15.3865L8.26187 15.3859C8.09336 15.316 7.9038 15.3159 7.73517 15.3855L6.57309 15.8669C6.0777 16.0721 5.52109 16.0721 5.0257 15.8669C4.53031 15.6617 4.13674 15.2681 3.93155 14.7727L3.44985 13.6097C3.37975 13.4415 3.24546 13.3079 3.07708 13.2382L1.91446 12.7566C1.66912 12.6551 1.44618 12.5062 1.25839 12.3184C1.07055 12.1306 0.92155 11.9077 0.819908 11.6623C0.718266 11.4169 0.665973 11.1539 0.666016 10.8883C0.666058 10.6228 0.718546 10.3595 0.820156 10.1143L1.30112 8.94977C1.33596 8.8658 1.35348 8.77668 1.35368 8.68746ZM10.8041 7.4922C11.0644 7.23185 11.0644 6.80974 10.8041 6.54939C10.5437 6.28904 10.1216 6.28904 9.86128 6.54939L7.33268 9.07798L6.47075 8.21605C6.2104 7.95571 5.78829 7.95571 5.52794 8.21605C5.2676 8.4764 5.2676 8.89851 5.52794 9.15886L6.86128 10.4922C7.12163 10.7525 7.54374 10.7525 7.80409 10.4922L10.8041 7.4922Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="copy-solid" viewBox="0 0 17 16" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9049 2.70076C11.4937 2.66717 10.9655 2.66665 10.2077 2.66665H5.47441C5.10622 2.66665 4.80774 2.36817 4.80774 1.99998C4.80774 1.63179 5.10622 1.33331 5.47441 1.33331L10.2363 1.33331C10.9588 1.33331 11.5416 1.3333 12.0135 1.37186C12.4994 1.41156 12.9262 1.49543 13.321 1.69662C13.9482 2.0162 14.4582 2.52614 14.7778 3.15334C14.979 3.54821 15.0628 3.975 15.1025 4.4609C15.1411 4.93282 15.1411 5.51559 15.1411 6.23813V11C15.1411 11.3682 14.8426 11.6666 14.4744 11.6666C14.1062 11.6666 13.8077 11.3682 13.8077 11V6.26665C13.8077 5.50891 13.8072 4.9807 13.7736 4.56947C13.7407 4.16603 13.6792 3.93424 13.5898 3.75867C13.398 3.38234 13.092 3.07638 12.7157 2.88463C12.5402 2.79518 12.3084 2.73373 11.9049 2.70076Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58204 3.66665H10.0334C10.3849 3.66663 10.688 3.66662 10.9378 3.68703C11.2013 3.70856 11.4635 3.7561 11.7157 3.88463C12.092 4.07638 12.398 4.38234 12.5898 4.75867C12.7183 5.01092 12.7658 5.27306 12.7874 5.53663C12.8078 5.78635 12.8078 6.08953 12.8077 6.44096V11.8923C12.8078 12.2438 12.8078 12.5469 12.7874 12.7967C12.7658 13.0602 12.7183 13.3224 12.5898 13.5746C12.398 13.951 12.092 14.2569 11.7157 14.4487C11.4635 14.5772 11.2013 14.6247 10.9378 14.6463C10.688 14.6667 10.3849 14.6667 10.0334 14.6666H4.58206C4.23062 14.6667 3.92744 14.6667 3.67772 14.6463C3.41416 14.6247 3.15201 14.5772 2.89976 14.4487C2.52344 14.2569 2.21747 13.951 2.02573 13.5746C1.8972 13.3224 1.84965 13.0602 1.82812 12.7967C1.80772 12.547 1.80773 12.2438 1.80774 11.8923V6.44095C1.80773 6.08952 1.80772 5.78634 1.82812 5.53663C1.84965 5.27306 1.8972 5.01092 2.02573 4.75867C2.21747 4.38234 2.52344 4.07638 2.89976 3.88463C3.15201 3.7561 3.41416 3.70856 3.67772 3.68703C3.92744 3.66662 4.23061 3.66663 4.58204 3.66665Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="heart-solid" viewBox="0 0 19 18" fill="none">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.49466 2.78774C7.73973 1.25408 5.14439 0.940234 3.12891 2.6623C0.948817 4.52502 0.63207 7.66213 2.35603 9.88052C3.01043 10.7226 4.28767 11.9877 5.51513 13.1462C6.75696 14.3184 7.99593 15.426 8.60692 15.9671C8.61074 15.9705 8.61463 15.9739 8.61859 15.9774C8.67603 16.0283 8.74753 16.0917 8.81608 16.1433C8.89816 16.2052 9.01599 16.2819 9.17334 16.3288C9.38253 16.3912 9.60738 16.3912 9.81656 16.3288C9.97391 16.2819 10.0917 16.2052 10.1738 16.1433C10.2424 16.0917 10.3139 16.0283 10.3713 15.9774C10.3753 15.9739 10.3792 15.9705 10.383 15.9671C10.994 15.426 12.2329 14.3184 13.4748 13.1462C14.7022 11.9877 15.9795 10.7226 16.6339 9.88052C18.3512 7.67065 18.0834 4.50935 15.8532 2.65572C13.8153 0.961905 11.2476 1.25349 9.49466 2.78774Z" fill="currentColor"/>
</g>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@ -16,7 +16,7 @@ export default function Copy({ text, maxSize = 32, className }: CopyProps) {
<div className={`flex flex-row copy ${className}`} onClick={() => copy(text)}>
<span className="body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
{copied ? <Icon name="check" size={14} /> : <Icon name="copy" size={14} />}
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}
</span>
</div>
);

View File

@ -13,7 +13,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
<h3>{title}</h3>
</div>
<div>
<Link to={`/live/${encodeTLV(NostrPrefix.Address, d, undefined, ev.kind, ev.pubkey)}`}>
<Link to={`https://zap.stream/${encodeTLV(NostrPrefix.Address, d, undefined, ev.kind, ev.pubkey)}`}>
<button className="primary" type="button">
<FormattedMessage defaultMessage="Watch Live!" />
</button>

View File

@ -29,7 +29,7 @@ const Nip05 = ({ nip05, pubkey, verifyNip = true }: Nip05Params) => {
<span className="domain" data-domain={domain?.toLowerCase()}>
{domain}
</span>
<Icon name="badge" className="badge" size={16} />
<Icon name="check-verified" className="badge" size={16} />
</>
)}
</div>

View File

@ -2,7 +2,7 @@
min-height: 110px;
display: flex;
flex-direction: column;
gap: 12px;
gap: 8px;
}
.note:hover {
@ -185,7 +185,6 @@
.note.active {
border-left: 1px solid var(--highlight);
border-bottom-left-radius: 0;
margin-left: -1px;
}

View File

@ -191,7 +191,7 @@ export default function NoteFooter(props: NoteFooterProps) {
function repostIcon() {
return (
<div className={`reaction-pill ${hasReposted() ? "reacted" : ""}`} onClick={() => repost()}>
<Icon name="repost" size={17} />
<Icon name="repeat" size={18} />
{reposts.length > 0 && <div className="reaction-pill-number">{formatShort(reposts.length)}</div>}
</div>
);
@ -201,12 +201,11 @@ export default function NoteFooter(props: NoteFooterProps) {
if (!prefs.enableReactions) {
return null;
}
const reacted = hasReacted("+");
return (
<>
<div
className={`reaction-pill ${hasReacted("+") ? "reacted" : ""} `}
onClick={() => react(prefs.reactionEmoji)}>
<Icon name="heart" />
<div className={`reaction-pill ${reacted ? "reacted" : ""} `} onClick={() => react(prefs.reactionEmoji)}>
<Icon name={reacted ? "heart-solid" : "heart"} size={18} />
<div className="reaction-pill-number">{formatShort(positive.length)}</div>
</div>
</>

View File

@ -1,23 +1,14 @@
.reaction {
}
.reaction > .note {
margin: 10px 0;
}
.reaction > .header {
display: flex;
flex-direction: row;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.reaction > .header .reply {
font-size: var(--font-size-small);
.reaction > div:nth-child(1) {
font-size: 16px;
font-weight: 600;
}
.reaction > .header > .info {
font-size: var(--font-size);
white-space: nowrap;
color: var(--font-secondary-color);
margin-right: 24px;
.reaction > div:nth-child(1) svg {
opacity: 0.5;
}

View File

@ -1,13 +1,16 @@
import "./NoteReaction.css";
import { Link } from "react-router-dom";
import { useMemo } from "react";
import { EventKind, NostrEvent, TaggedNostrEvent, NostrPrefix, EventExt } from "@snort/system";
import { EventKind, NostrEvent, TaggedNostrEvent, NostrPrefix } from "@snort/system";
import Note from "Element/Note";
import ProfileImage from "Element/ProfileImage";
import { getDisplayName } from "Element/ProfileImage";
import { eventLink, hexToBech32 } from "SnortUtils";
import NoteTime from "Element/NoteTime";
import useModeration from "Hooks/useModeration";
import { FormattedMessage } from "react-intl";
import Icon from "Icons/Icon";
import { useUserProfile } from "@snort/system-react";
import { System } from "index";
export interface NoteReactionProps {
data: TaggedNostrEvent;
@ -16,6 +19,7 @@ export interface NoteReactionProps {
export default function NoteReaction(props: NoteReactionProps) {
const { data: ev } = props;
const { isMuted } = useModeration();
const profile = useUserProfile(System, ev.pubkey);
const refEvent = useMemo(() => {
if (ev) {
@ -60,12 +64,15 @@ export default function NoteReaction(props: NoteReactionProps) {
};
return shouldNotBeRendered ? null : (
<div className="reaction">
<div className="header flex">
<ProfileImage pubkey={EventExt.getRootPubKey(ev)} />
<div className="info">
<NoteTime from={ev.created_at * 1000} />
</div>
<div className="card reaction">
<div className="flex g4">
<Icon name="repeat" size={18} />
<FormattedMessage
defaultMessage="{name} reposted"
values={{
name: getDisplayName(profile, ev.pubkey),
}}
/>
</div>
{root ? <Note data={root} options={opt} related={[]} /> : null}
{!root && refEvent ? (

View File

@ -50,8 +50,7 @@ export default function ProfileImage({
<Link
className={`pfp${className ? ` ${className}` : ""}`}
to={link === undefined ? profileLink(pubkey) : link}
onClick={handleClick}
replace={true}>
onClick={handleClick}>
<div className="avatar-wrapper">
<Avatar user={user} />
</div>

View File

@ -5,8 +5,9 @@
overflow-x: scroll;
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* Firefox */
margin-bottom: 18px;
white-space: nowrap;
gap: 8px;
padding: 16px 12px;
}
.tabs::-webkit-scrollbar {
@ -14,23 +15,21 @@
}
.tab {
background: var(--gray-ultradark);
color: var(--font-tertiary-color);
border: 1px solid var(--border-color);
border-radius: 16px;
border-radius: 100px;
font-weight: 600;
font-size: 14px;
padding: 6px 12px;
text-align: center;
font-feature-settings: "tnum";
}
.tab:not(:last-of-type) {
margin-right: 8px;
font-size: 16px;
padding: 10px 16px;
display: flex;
align-items: center;
justify-items: center;
gap: 6px;
}
.tab.active {
border-color: var(--font-color);
color: var(--font-color);
color: black;
background: white;
}
.tabs > div {

View File

@ -1,8 +1,9 @@
import { ReactNode } from "react";
import "./Tabs.css";
import useHorizontalScroll from "Hooks/useHorizontalScroll";
export interface Tab {
text: string;
text: ReactNode;
value: number;
disabled?: boolean;
}

View File

@ -1,7 +1,3 @@
.thread-container {
margin: 12px 0 150px 0;
}
.thread-container .hidden-note {
margin: 0;
border-radius: 0;
@ -11,11 +7,6 @@
box-shadow: none;
}
.thread-root.note > .body {
margin-top: 8px;
padding-left: 8px;
}
.thread-root.note > .body .text {
font-size: 19px;
}
@ -31,12 +22,13 @@
}
.thread-note.note {
border-radius: 0;
margin-bottom: 0;
border: 0;
}
.light .thread-note.note.card {
box-shadow: none;
.thread-note.note .zaps-summary,
.thread-note.note .footer,
.thread-note.note .body {
margin-left: 61px;
}
.thread-container .hidden-note {
@ -58,83 +50,47 @@
position: relative;
}
.line-container {
background: var(--note-bg);
}
.subthread-container.subthread-multi .line-container:before {
content: "";
position: absolute;
left: 36px;
left: calc(48px / 2 + 16px);
top: 48px;
border-left: 1px solid var(--gray-superdark);
height: 100%;
z-index: -1;
}
@media (min-width: 720px) {
.subthread-container.subthread-multi .line-container:before {
left: 48px;
}
}
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
content: "";
position: absolute;
left: 36px;
top: 48px;
border-left: 1px solid var(--gray-superdark);
height: 100%;
}
@media (min-width: 720px) {
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
left: 48px;
}
}
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
.subthread-container.subthread-mid:not(.subthread-last) .line-container:before {
content: "";
position: absolute;
border-left: 1px solid var(--gray-superdark);
left: 36px;
left: calc(48px / 2 + 16px);
top: 0;
height: 48px;
}
@media (min-width: 720px) {
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
left: 48px;
}
z-index: -1;
}
.subthread-container.subthread-last .line-container:before {
content: "";
position: absolute;
border-left: 1px solid var(--gray-superdark);
left: 36px;
left: calc(48px / 2 + 16px);
top: 0;
height: 48px;
}
@media (min-width: 720px) {
.subthread-container.subthread-last .line-container:before {
left: 48px;
}
z-index: -1;
}
.divider-container {
background: var(--note-bg);
margin-right: 16px;
}
.divider {
height: 1px;
background: var(--gray-superdark);
margin-left: 28px;
margin-right: 22px;
}
.divider.divider-small {
margin-left: 80px;
margin-left: calc(16px + 61px);
}
.thread-container .collapsed,
@ -143,11 +99,6 @@
min-height: 48px;
}
.thread-note.is-last-note {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
.thread-container .collapsed {
background-color: var(--note-bg);
}
@ -155,13 +106,3 @@
.thread-container .hidden-note {
padding-left: 48px;
}
.thread-root.thread-root-single.note {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
.thread-root.ghost-root {
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}

View File

@ -374,9 +374,11 @@ export default function Thread() {
description: "Navigate back button on threads view",
});
return (
<div className="main-content mt10">
<BackButton onClick={goBack} text={parent ? parentText : backText} />
<div className="thread-container">
<>
<div className="main-content">
<BackButton onClick={goBack} text={parent ? parentText : backText} />
</div>
<div className="main-content">
{root && renderRoot(root)}
{root && renderChain(root.id)}
@ -392,7 +394,7 @@ export default function Thread() {
);
})}
</div>
</div>
</>
);
}

View File

@ -40,7 +40,7 @@ export default function Layout() {
};
const shouldHideNoteCreator = useMemo(() => {
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/p/", "/e", "/subscribe", "/live"];
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/p/", "/e", "/subscribe"];
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
}, [location, isReplyNoteCreatorShowing]);
@ -50,8 +50,8 @@ export default function Layout() {
}, [location]);
useEffect(() => {
const widePage = ["/login", "/messages", "/live"];
const noScroll = ["/messages", "/live"];
const widePage = ["/login", "/messages"];
const noScroll = ["/messages"];
if (widePage.some(a => location.pathname.startsWith(a))) {
setPageClass(noScroll.some(a => location.pathname.startsWith(a)) ? "scroll-lock" : "");
} else {

View File

@ -2,28 +2,21 @@
display: flex;
flex-direction: column;
align-items: flex-start;
border: 1px solid var(--gray-superdark);
}
.profile .banner {
width: 100%;
height: 160px;
object-fit: cover;
margin-bottom: -60px;
margin-bottom: -37px;
z-index: 0;
}
@media (min-width: 720px) {
.profile .banner {
border-radius: 12px;
}
}
.profile .profile-actions {
position: absolute;
top: 72px;
right: 0;
display: flex;
flex-direction: row;
align-items: center;
align-self: flex-end;
}
.profile .icon-actions {
@ -52,13 +45,13 @@
}
.profile .profile-wrapper {
margin: 0 16px;
width: calc(100% - 32px);
margin: 0 16px 12px 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative;
overflow: hidden;
gap: 16px;
}
.profile p {
@ -66,21 +59,26 @@
}
.details-wrapper > .name > h2 {
margin: 12px 0 0 0;
margin: 0 0 4px 0;
font-weight: 600;
font-size: 19px;
line-height: 23px;
font-size: 21px;
}
.details-wrapper > .name > .nip05 {
font-size: 15px;
}
.profile-wrapper > .avatar-wrapper {
z-index: 1;
display: flex;
justify-content: space-between;
}
.profile-wrapper > .avatar-wrapper .avatar {
width: 120px;
height: 120px;
width: 100px;
height: 100px;
background-image: var(--img-url);
border: 3px solid var(--bg-color);
border: 3px solid #fff;
}
.profile .name {
@ -88,28 +86,30 @@
flex-direction: column;
}
.profile .details {
width: 100%;
.profile .about {
color: var(--font-secondary-color);
margin-bottom: 12px;
font-weight: 400;
font-size: 14px;
line-height: 22px;
font-size: 16px;
line-height: 26px;
}
.profile .details p {
.profile .about p {
word-break: break-word;
}
.profile .details a {
.profile .about a {
color: var(--highlight);
text-decoration: none;
}
.profile .details a:hover {
.profile .about a:hover {
text-decoration: underline;
}
.profile .about .text {
font-size: inherit;
line-height: inherit;
}
.profile .btn-icon {
color: var(--font-color);
padding: 6px;
@ -118,54 +118,36 @@
.profile .details-wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
width: calc(100% - 32px);
gap: 16px;
}
.profile .details .text {
font-size: 14px;
}
.profile .links {
font-size: 14px;
margin-top: 4px;
margin-left: 2px;
margin-bottom: 12px;
}
.profile .website {
margin: 4px 0;
.profile .link-section {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 15px;
line-height: 24px;
}
.profile .link {
display: flex;
flex-direction: row;
align-items: center;
}
@media (max-width: 720px) {
.profile .lnurl {
display: none;
}
}
.profile .website a {
color: var(--font-color);
gap: 8px;
}
.profile .website a {
text-decoration: none;
}
.profile .website a:hover {
text-decoration: underline;
}
.profile .lnurl {
cursor: pointer;
.profile .link svg {
color: var(--highlight);
}
.profile .ln-address {
display: flex;
flex-direction: row;
align-items: center;
.profile .lnurl {
cursor: pointer;
}
.profile .lnurl:hover {
@ -177,21 +159,9 @@
text-overflow: ellipsis;
}
.profile .links svg {
color: var(--highlight);
margin-right: 8px;
width: 12px;
height: 12px;
}
.profile .npub {
display: flex;
flex-direction: row;
align-items: center;
}
.profile .copy {
margin-top: 12px;
.profile .copy .body {
font-size: inherit;
line-height: inherit;
}
.qr-modal .pfp {

View File

@ -1,6 +1,6 @@
import "./ProfilePage.css";
import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { FormattedMessage } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import {
encodeTLV,
@ -109,7 +109,6 @@ function BookMarksTab({ id }: { id: HexKey }) {
}
export default function ProfilePage() {
const { formatMessage } = useIntl();
const params = useParams();
const navigate = useNavigate();
const [id, setId] = useState<string>();
@ -144,16 +143,88 @@ export default function ProfilePage() {
const follows = useFollowsFeed(id);
// tabs
const ProfileTab = {
Notes: { text: formatMessage({ defaultMessage: "Notes" }), value: NOTES },
Reactions: { text: formatMessage(messages.Reactions), value: REACTIONS },
Followers: { text: formatMessage(messages.Followers), value: FOLLOWERS },
Follows: { text: formatMessage(messages.Follows), value: FOLLOWS },
Zaps: { text: formatMessage(messages.Zaps), value: ZAPS },
Muted: { text: formatMessage(messages.Muted), value: MUTED },
Blocked: { text: formatMessage(messages.BlockedCount, { n: blocked.length }), value: BLOCKED },
Relays: { text: formatMessage(messages.Relays), value: RELAYS },
Bookmarks: { text: formatMessage(messages.Bookmarks), value: BOOKMARKS },
};
Notes: {
text: (
<>
<Icon name="pencil" size={16} />
<FormattedMessage defaultMessage="Notes" />
</>
),
value: NOTES,
},
Reactions: {
text: (
<>
<Icon name="reaction" size={16} />
<FormattedMessage defaultMessage="Reactions" />
</>
),
value: REACTIONS,
},
Followers: {
text: (
<>
<Icon name="user-v2" size={16} />
<FormattedMessage defaultMessage="Followers" />
</>
),
value: FOLLOWERS,
},
Follows: {
text: (
<>
<Icon name="stars" size={16} />
<FormattedMessage defaultMessage="Follows" />
</>
),
value: FOLLOWS,
},
Zaps: {
text: (
<>
<Icon name="zap-solid" size={16} />
<FormattedMessage defaultMessage="Zaps" />
</>
),
value: ZAPS,
},
Muted: {
text: (
<>
<Icon name="mute" size={16} />
<FormattedMessage defaultMessage="Muted" />
</>
),
value: MUTED,
},
Blocked: {
text: (
<>
<Icon name="block" size={16} />
<FormattedMessage defaultMessage="Blocked" />
</>
),
value: BLOCKED,
},
Relays: {
text: (
<>
<Icon name="wifi" size={16} />
<FormattedMessage defaultMessage="Relays" />
</>
),
value: RELAYS,
},
Bookmarks: {
text: (
<>
<Icon name="bookmark-solid" size={16} />
<FormattedMessage defaultMessage="Bookmarks" />
</>
),
value: BOOKMARKS,
},
} as { [key: string]: Tab };
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
const optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a =>
unwrap(a)
@ -179,34 +250,48 @@ export default function ProfilePage() {
function username() {
return (
<div className="name">
<h2>
{user?.display_name || user?.name || "Nostrich"}
<FollowsYou followsMe={follows.includes(loginPubKey ?? "")} />
</h2>
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
<>
<div className="name">
<h2>
{user?.display_name || user?.name || "Nostrich"}
<FollowsYou followsMe={follows.includes(loginPubKey ?? "")} />
</h2>
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
</div>
<BadgeList badges={badges} />
<Copy text={npub} />
{links()}
</div>
<div className="link-section">
<Copy text={npub} />
{links()}
</div>
</>
);
}
function tryFormatWebsite(url: string) {
try {
const u = new URL(url);
return `${u.hostname}${u.pathname !== "/" ? u.pathname : ""}`;
} catch {
// ignore
}
return url;
}
function links() {
return (
<div className="links">
<>
{user?.website && (
<div className="website f-ellipsis">
<Icon name="link" />
<div className="link website f-ellipsis">
<Icon name="link-02" size={16} />
<a href={website_url} target="_blank" rel="noreferrer">
{user.website}
{tryFormatWebsite(user.website)}
</a>
</div>
)}
{lnurl && (
<div className="lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
<Icon name="zap" />
<div className="link lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
<Icon name="zapCircle" size={16} />
{lnurl.name}
</div>
)}
@ -218,14 +303,14 @@ export default function ProfilePage() {
author={id}
target={user?.display_name || user?.name}
/>
</div>
</>
);
}
function bio() {
return (
aboutText.length > 0 && (
<div dir="auto" className="details">
<div dir="auto" className="about">
{about}
</div>
)
@ -296,8 +381,12 @@ export default function ProfilePage() {
function avatar() {
return (
<div className="avatar-wrapper">
<div className="avatar-wrapper w-max">
<Avatar user={user} />
<div className="profile-actions">
{renderIcons()}
{!isMe && id && <FollowButton pubkey={id} />}
</div>
</div>
);
}
@ -356,12 +445,8 @@ export default function ProfilePage() {
function userDetails() {
if (!id) return;
return (
<div className="details-wrapper">
<div className="details-wrapper w-max">
{username()}
<div className="profile-actions">
{renderIcons()}
{!isMe && <FollowButton pubkey={id} />}
</div>
{bio()}
</div>
);
@ -374,9 +459,9 @@ export default function ProfilePage() {
const w = window.document.querySelector(".page")?.clientWidth;
return (
<>
<div className="profile flex">
<div className="profile">
{user?.banner && <ProxyImg alt="banner" className="banner" src={user.banner} size={w} />}
<div className="profile-wrapper flex">
<div className="profile-wrapper w-max">
{avatar()}
{userDetails()}
</div>

View File

@ -31,15 +31,6 @@ export default function RootPage() {
const { publicKey: pubKey, tags, preferences } = useLogin();
const [rootType, setRootType] = useState<RootPage>("following");
useEffect(() => {
if (location.pathname === "/") {
const t = pubKey ? preferences.defaultRootTab ?? "/notes" : "/global";
navigate(t, {
replace: true,
});
}
}, [location]);
const menuItems = [
{
tab: "following",
@ -107,6 +98,18 @@ export default function RootPage() {
element: ReactNode;
}>;
useEffect(() => {
if (location.pathname === "/") {
const t = pubKey ? preferences.defaultRootTab ?? "/notes" : "/global";
navigate(t);
} else {
const currentTab = menuItems.find(a => a.path === location.pathname)?.tab;
if (currentTab) {
setRootType(currentTab);
}
}
}, [location]);
function currentMenuItem() {
if (location.pathname.startsWith("/t/")) {
return (
@ -139,8 +142,7 @@ export default function RootPage() {
{menuItems.map(a => (
<MenuItem
onClick={() => {
setRootType(a.tab);
navigate(a.path, { replace: true });
navigate(a.path);
}}>
{a.element}
</MenuItem>
@ -148,8 +150,7 @@ export default function RootPage() {
{tags.item.map(v => (
<MenuItem
onClick={() => {
setRootType("tags");
navigate(`/t/${v}`, { replace: true });
navigate(`/t/${v}`);
}}>
<Icon name="hash" />
{v}

View File

@ -9,7 +9,7 @@
--font-size-tiny: 11px;
--modal-bg-color: rgba(0, 0, 0, 0.8);
--note-bg: #0c0c0c;
--highlight: #8b5cf6;
--highlight: #ac88ff;
--error: #ff6053;
--success: #2ad544;
--warning: #ff8800;
@ -23,6 +23,7 @@
--gray-tertiary: #444;
--gray-dark: #2b2b2b;
--gray-superdark: #1a1a1a;
--gray-ultradark: #111;
--gray-gradient: linear-gradient(to bottom right, var(--gray-superlight), var(--gray), var(--gray-light));
--snort-gradient: linear-gradient(90deg, #a178ff 0%, #ff6baf 100%);
--dm-gradient: linear-gradient(90deg, #5722d2 0%, #db1771 100%);
@ -123,10 +124,17 @@ body #root > div:not(.page) header {
}
.card {
padding: 16px;
padding: 12px 16px;
border-bottom: 1px solid var(--gray-superdark);
}
/* Card inside card */
.card .card {
border: 1px solid var(--gray-superdark);
border-radius: 16px;
min-height: 0;
}
.card .header {
display: flex;
flex-direction: row;
@ -407,6 +415,11 @@ input:disabled {
a {
color: inherit;
line-height: 1.3em;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a.ext {

View File

@ -17,6 +17,9 @@
"+vVZ/G": {
"defaultMessage": "Connect"
},
"+xliwN": {
"defaultMessage": "{name} reposted"
},
"/4tOwT": {
"defaultMessage": "Skip"
},
@ -36,6 +39,9 @@
"/n5KSF": {
"defaultMessage": "{n} ms"
},
"00LcfG": {
"defaultMessage": "Load more"
},
"08zn6O": {
"defaultMessage": "Export Keys"
},
@ -438,6 +444,9 @@
"IEwZvs": {
"defaultMessage": "Are you sure you want to unpin this note?"
},
"IKKHqV": {
"defaultMessage": "Follows"
},
"INSqIz": {
"defaultMessage": "Twitter username..."
},

View File

@ -5,12 +5,14 @@
"+vA//S": "Logins",
"+vIQlC": "Please make sure to save the following password in order to manage your handle in the future",
"+vVZ/G": "Connect",
"+xliwN": "{name} reposted",
"/4tOwT": "Skip",
"/JE/X+": "Account Support",
"/PCavi": "Public",
"/RD0e2": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.",
"/d6vEc": "Make your profile easier to find and share",
"/n5KSF": "{n} ms",
"00LcfG": "Load more",
"08zn6O": "Export Keys",
"0Azlrb": "Manage",
"0BUTMv": "Search...",
@ -87,7 +89,6 @@
"B6+XJy": "zapped",
"B6H7eJ": "nsec, npub, nip-05, hex",
"BGCM48": "Write access to Snort relay, with 1 year of event retention",
"BGxpTN": "Stream Chat",
"BOUMjw": "No nostr users found for {twitterUsername}",
"BOr9z/": "Snort is an open source project built by passionate people in their free time",
"BWpuKl": "Update",
@ -144,6 +145,7 @@
"HbefNb": "Open Wallet",
"IDjHJ6": "Thanks for using Snort, please consider donating if you can.",
"IEwZvs": "Are you sure you want to unpin this note?",
"IKKHqV": "Follows",
"INSqIz": "Twitter username...",
"IUZC+0": "This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.",
"Ig9/a1": "Sent {n} sats to {name}",
@ -268,7 +270,6 @@
"c+oiJe": "Install Extension",
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
"c3g2hL": "Broadcast Again",
"cE4Hfw": "Discover",
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
"cPIKU2": "Following",
"cQfLWb": "URL..",
@ -285,7 +286,6 @@
"eHAneD": "Reaction emoji",
"eJj8HD": "Get Verified",
"eSzf2G": "A single zap of {nIn} sats will allocate {nOut} sats to the zap pool.",
"fBI91o": "Zap",
"fOksnD": "Can't vote because LNURL service does not support zaps",
"fWZYP5": "Pinned",
"filwqD": "Read",
@ -366,7 +366,6 @@
"pzTOmv": "Followers",
"qD9EUF": "Email <> DM bridge for your Snort nostr address",
"qDwvZ4": "Unknown error",
"qInqHy": "Message...",
"qMx1sA": "Default Zap amount",
"qUJTsT": "Blocked",
"qdGuQo": "Your Private Key Is (do not share this with anyone)",
@ -426,4 +425,4 @@
"zjJZBd": "You're ready!",
"zonsdq": "Failed to load LNURL service",
"zvCDao": "Automatically show latest notes"
}
}

View File

@ -226,12 +226,12 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "EVENT") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "EVENT" should be a string, but wasn't: ${data}`
`second element of "EVENT" should be a string, but wasn't: ${data}`,
)
}
if (typeof json[2] !== "object") {
throw new NostrError(
`second element of "EVENT" should be an object, but wasn't: ${data}`
`second element of "EVENT" should be an object, but wasn't: ${data}`,
)
}
const event = parseEventData(json[2])
@ -246,7 +246,7 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "NOTICE") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "NOTICE" should be a string, but wasn't: ${data}`
`second element of "NOTICE" should be a string, but wasn't: ${data}`,
)
}
return {
@ -259,17 +259,17 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "OK") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "OK" should be a string, but wasn't: ${data}`
`second element of "OK" should be a string, but wasn't: ${data}`,
)
}
if (typeof json[2] !== "boolean") {
throw new NostrError(
`third element of "OK" should be a boolean, but wasn't: ${data}`
`third element of "OK" should be a boolean, but wasn't: ${data}`,
)
}
if (typeof json[3] !== "string") {
throw new NostrError(
`fourth element of "OK" should be a string, but wasn't: ${data}`
`fourth element of "OK" should be a string, but wasn't: ${data}`,
)
}
return {
@ -284,7 +284,7 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "EOSE") {
if (typeof json[1] !== "string") {
throw new NostrError(
`second element of "EOSE" should be a string, but wasn't: ${data}`
`second element of "EOSE" should be a string, but wasn't: ${data}`,
)
}
return {
@ -312,7 +312,7 @@ function parseEventData(json: { [key: string]: unknown }): RawEvent {
typeof json["kind"] !== "number" ||
!(json["tags"] instanceof Array) ||
!json["tags"].every(
(x) => x instanceof Array && x.every((y) => typeof y === "string")
(x) => x instanceof Array && x.every((y) => typeof y === "string"),
) ||
typeof json["content"] !== "string" ||
typeof json["sig"] !== "string"

View File

@ -13,7 +13,7 @@ export class EventEmitter extends Base {
override addListener(eventName: "newListener", listener: NewListener): this
override addListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override addListener(eventName: "open", listener: OpenListener): this
override addListener(eventName: "close", listener: CloseListener): this
@ -36,7 +36,7 @@ export class EventEmitter extends Base {
override emit(
eventName: "eose",
subscriptionId: SubscriptionId,
nostr: Nostr
nostr: Nostr,
): boolean
override emit(eventName: "error", err: unknown, nostr: Nostr): boolean
override emit(eventName: EventName, ...args: unknown[]): boolean {
@ -101,11 +101,11 @@ export class EventEmitter extends Base {
override prependListener(
eventName: "newListener",
listener: NewListener
listener: NewListener,
): this
override prependListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override prependListener(eventName: "open", listener: OpenListener): this
override prependListener(eventName: "close", listener: CloseListener): this
@ -120,30 +120,30 @@ export class EventEmitter extends Base {
override prependOnceListener(
eventName: "newListener",
listener: NewListener
listener: NewListener,
): this
override prependOnceListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override prependOnceListener(eventName: "open", listener: OpenListener): this
override prependOnceListener(
eventName: "close",
listener: CloseListener
listener: CloseListener,
): this
override prependOnceListener(
eventName: "event",
listener: EventListener
listener: EventListener,
): this
override prependOnceListener(
eventName: "notice",
listener: NoticeListener
listener: NoticeListener,
): this
override prependOnceListener(eventName: "ok", listener: OkListener): this
override prependOnceListener(eventName: "eose", listener: EoseListener): this
override prependOnceListener(
eventName: "error",
listener: ErrorListener
listener: ErrorListener,
): this
override prependOnceListener(eventName: EventName, listener: Listener): this {
return super.prependOnceListener(eventName, listener)
@ -156,7 +156,7 @@ export class EventEmitter extends Base {
override removeListener(eventName: "newListener", listener: NewListener): this
override removeListener(
eventName: "removeListener",
listener: RemoveListener
listener: RemoveListener,
): this
override removeListener(eventName: "open", listener: OpenListener): this
override removeListener(eventName: "close", listener: CloseListener): this

View File

@ -43,18 +43,18 @@ export class Nostr extends EventEmitter {
*/
open(
url: URL | string,
opts?: { read?: boolean; write?: boolean; fetchInfo?: boolean }
opts?: { read?: boolean; write?: boolean; fetchInfo?: boolean },
): void {
const relayUrl = new URL(url)
// If the connection already exists, update the options.
const existingConn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (existingConn !== undefined) {
if (opts === undefined) {
throw new NostrError(
`called connect with existing connection ${url}, but options were not specified`
`called connect with existing connection ${url}, but options were not specified`,
)
}
if (opts.read !== undefined) {
@ -88,7 +88,7 @@ export class Nostr extends EventEmitter {
event: parseEvent(msg.event),
subscriptionId: msg.subscriptionId,
},
this
this,
)
} else if (msg.kind === "notice") {
this.emit("notice", msg.notice, this)
@ -101,7 +101,7 @@ export class Nostr extends EventEmitter {
ok: msg.ok,
message: msg.message,
},
this
this,
)
} else if (msg.kind === "eose") {
this.emit("eose", msg.subscriptionId, this)
@ -116,13 +116,13 @@ export class Nostr extends EventEmitter {
onOpen: async () => {
// Update the connection readyState.
const conn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (conn === undefined) {
this.#error(
new NostrError(
`bug: expected connection to ${relayUrl.toString()} to be in the map`
)
`bug: expected connection to ${relayUrl.toString()} to be in the map`,
),
)
} else {
if (conn.relay.readyState !== ReadyState.CONNECTING) {
@ -130,8 +130,8 @@ export class Nostr extends EventEmitter {
new NostrError(
`bug: expected connection to ${relayUrl.toString()} to have readyState CONNECTING, got ${
conn.relay.readyState
}`
)
}`,
),
)
}
conn.relay = {
@ -148,13 +148,13 @@ export class Nostr extends EventEmitter {
onClose: () => {
// Update the connection readyState.
const conn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (conn === undefined) {
this.#error(
new NostrError(
`bug: expected connection to ${relayUrl.toString()} to be in the map`
)
`bug: expected connection to ${relayUrl.toString()} to be in the map`,
),
)
} else {
conn.relay.readyState = ReadyState.CLOSED
@ -207,7 +207,7 @@ export class Nostr extends EventEmitter {
}
const relayUrl = new URL(url)
const c = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString()
(c) => c.relay.url.toString() === relayUrl.toString(),
)
if (c === undefined) {
throw new NostrError(`connection to ${url} doesn't exist`)
@ -231,7 +231,7 @@ export class Nostr extends EventEmitter {
*/
subscribe(
filters: Filters[],
subscriptionId: SubscriptionId = randomSubscriptionId()
subscriptionId: SubscriptionId = randomSubscriptionId(),
): SubscriptionId {
this.#subscriptions.set(subscriptionId, filters)
for (const { conn, read } of this.#conns.values()) {

View File

@ -75,32 +75,32 @@ export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
info.name = undefined
throw new NostrError(
`invalid relay info, expected "name" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.description !== undefined && typeof info.description !== "string") {
info.description = undefined
throw new NostrError(
`invalid relay info, expected "description" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.pubkey !== undefined && typeof info.pubkey !== "string") {
info.pubkey = undefined
throw new NostrError(
`invalid relay info, expected "pubkey" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.contact !== undefined && typeof info.contact !== "string") {
info.contact = undefined
throw new NostrError(
`invalid relay info, expected "contact" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.supported_nips !== undefined) {
@ -109,16 +109,16 @@ export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
info.supported_nips = undefined
throw new NostrError(
`invalid relay info, expected "supported_nips" elements to be numbers: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
} else {
info.supported_nips = undefined
throw new NostrError(
`invalid relay info, expected "supported_nips" to be an array: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
}
@ -126,16 +126,16 @@ export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
info.software = undefined
throw new NostrError(
`invalid relay info, expected "software" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
if (info.version !== undefined && typeof info.version !== "string") {
info.version = undefined
throw new NostrError(
`invalid relay info, expected "version" to be a string: ${JSON.stringify(
info
)}`
info,
)}`,
)
}
return info

View File

@ -99,7 +99,7 @@ export function schnorrVerify(sig: Hex, data: Hex, key: PublicKey): boolean {
export async function aesEncryptBase64(
sender: PrivateKey,
recipient: PublicKey,
plaintext: string
plaintext: string,
): Promise<AesEncryptedBase64> {
const sharedPoint = secp.secp256k1.getSharedSecret(sender, "02" + recipient)
const sharedKey = sharedPoint.slice(1, 33)
@ -109,7 +109,7 @@ export async function aesEncryptBase64(
sharedKey,
{ name: "AES-CBC" },
false,
["encrypt"]
["encrypt"],
)
const iv = window.crypto.getRandomValues(new Uint8Array(16))
const data = new TextEncoder().encode(plaintext)
@ -119,7 +119,7 @@ export async function aesEncryptBase64(
iv,
},
key,
data
data,
)
return {
data: base64.fromByteArray(new Uint8Array(encrypted)),
@ -131,7 +131,7 @@ export async function aesEncryptBase64(
const cipher = crypto.createCipheriv(
"aes-256-cbc",
Buffer.from(sharedKey),
iv
iv,
)
let encrypted = cipher.update(plaintext, "utf8", "base64")
encrypted += cipher.final("base64")
@ -145,7 +145,7 @@ export async function aesEncryptBase64(
export async function aesDecryptBase64(
sender: PublicKey,
recipient: PrivateKey,
{ data, iv }: AesEncryptedBase64
{ data, iv }: AesEncryptedBase64,
): Promise<string> {
const sharedPoint = secp.secp256k1.getSharedSecret(recipient, "02" + sender)
const sharedKey = sharedPoint.slice(1, 33)
@ -157,7 +157,7 @@ export async function aesDecryptBase64(
sharedKey,
{ name: "AES-CBC" },
false,
["decrypt"]
["decrypt"],
)
const plaintext = await window.crypto.subtle.decrypt(
{
@ -165,7 +165,7 @@ export async function aesDecryptBase64(
iv: decodedIv,
},
importedKey,
decodedData
decodedData,
)
return new TextDecoder().decode(plaintext)
} else {
@ -173,7 +173,7 @@ export async function aesDecryptBase64(
const decipher = crypto.createDecipheriv(
"aes-256-cbc",
Buffer.from(sharedKey),
base64.toByteArray(iv)
base64.toByteArray(iv),
)
const plaintext = decipher.update(data, "base64", "utf8")
return plaintext + decipher.final()

View File

@ -30,7 +30,7 @@ export interface Contact {
*/
export function createContactList(
contacts: Contact[],
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<ContactList> {
return signEvent(
{
@ -44,7 +44,7 @@ export function createContactList(
content: "",
getContacts,
},
priv
priv,
)
}
@ -57,8 +57,8 @@ export function getContacts(this: ContactList): Contact[] {
if (pubkey === undefined) {
throw new NostrError(
`missing contact pubkey for contact list event: ${JSON.stringify(
this
)}`
this,
)}`,
)
}
@ -70,7 +70,7 @@ export function getContacts(this: ContactList): Contact[] {
}
} catch (e) {
throw new NostrError(
`invalid relay URL for contact list event: ${JSON.stringify(this)}`
`invalid relay URL for contact list event: ${JSON.stringify(this)}`,
)
}

View File

@ -21,7 +21,7 @@ export interface Deletion extends RawEvent {
*/
export function createDeletion(
{ events, content }: { events: EventId[]; content?: string },
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<Deletion> {
return signEvent(
{
@ -30,7 +30,7 @@ export function createDeletion(
content: content ?? "",
getEvents,
},
priv
priv,
)
}
@ -40,7 +40,7 @@ export function getEvents(this: Deletion): EventId[] {
.map((tag) => {
if (tag[1] === undefined) {
throw new NostrError(
`invalid deletion event tag: ${JSON.stringify(tag)}`
`invalid deletion event tag: ${JSON.stringify(tag)}`,
)
}
return tag[1]

View File

@ -46,7 +46,7 @@ export async function createDirectMessage(
message: string
recipient: PublicKey
},
priv?: PrivateKey
priv?: PrivateKey,
): Promise<DirectMessage> {
recipient = parsePublicKey(recipient)
if (priv === undefined) {
@ -66,7 +66,7 @@ export async function createDirectMessage(
getRecipient,
getPrevious,
},
priv
priv,
)
} else {
priv = parsePrivateKey(priv)
@ -80,14 +80,14 @@ export async function createDirectMessage(
getRecipient,
getPrevious,
},
priv
priv,
)
}
}
export async function getMessage(
this: DirectMessage,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<string | undefined> {
if (priv !== undefined) {
priv = parsePrivateKey(priv)
@ -114,9 +114,9 @@ export function getRecipient(this: DirectMessage): PublicKey {
const recipientTag = this.tags.find((tag) => tag[0] === "p")
if (typeof recipientTag?.[1] !== "string") {
throw new NostrError(
`expected "p" tag to be of type string, but got ${
recipientTag?.[1]
} in ${JSON.stringify(this)}`
`expected "p" tag to be of type string, but got ${recipientTag?.[1]} in ${JSON.stringify(
this,
)}`,
)
}
return recipientTag[1]
@ -129,9 +129,9 @@ export function getPrevious(this: DirectMessage): EventId | undefined {
}
if (typeof previousTag[1] !== "string") {
throw new NostrError(
`expected "e" tag to be of type string, but got ${
previousTag?.[1]
} in ${JSON.stringify(this)}`
`expected "e" tag to be of type string, but got ${previousTag?.[1]} in ${JSON.stringify(
this,
)}`,
)
}
return previousTag[1]

View File

@ -122,7 +122,7 @@ type UnsignedWithPubkey<T extends Event | RawEvent> = {
*/
export async function signEvent<T extends RawEvent>(
event: Unsigned<T>,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<T> {
event.created_at ??= unixTimestamp()
if (priv !== undefined) {
@ -130,7 +130,7 @@ export async function signEvent<T extends RawEvent>(
event.pubkey = getPublicKey(priv)
const id = serializeEventId(
// This conversion is safe because the pubkey field is set above.
event as unknown as UnsignedWithPubkey<T>
event as unknown as UnsignedWithPubkey<T>,
)
event.id = id
event.sig = schnorrSign(id, priv)
@ -162,8 +162,8 @@ export function parseEvent(event: RawEvent): Event {
if (event.id !== serializeEventId(event)) {
throw new NostrError(
`invalid id ${event.id} for event ${JSON.stringify(
event
)}, expected ${serializeEventId(event)}`
event,
)}, expected ${serializeEventId(event)}`,
)
}
if (!schnorrVerify(event.sig, event.id, event.pubkey)) {

View File

@ -22,7 +22,7 @@ export interface SetMetadata extends RawEvent {
* @return The internet identifier. `undefined` if there is no internet identifier.
*/
verifyInternetIdentifier(
opts?: VerificationOptions
opts?: VerificationOptions,
): Promise<InternetIdentifier | undefined>
}
@ -38,7 +38,7 @@ export interface UserMetadata {
*/
export function createSetMetadata(
content: UserMetadata,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<SetMetadata> {
return signEvent(
{
@ -48,7 +48,7 @@ export function createSetMetadata(
getUserMetadata,
verifyInternetIdentifier,
},
priv
priv,
)
}
@ -60,7 +60,7 @@ export function getUserMetadata(this: SetMetadata): UserMetadata {
typeof userMetadata.picture !== "string"
) {
throw new NostrError(
`invalid user metadata ${userMetadata} in ${JSON.stringify(this)}`
`invalid user metadata ${userMetadata} in ${JSON.stringify(this)}`,
)
}
return userMetadata
@ -68,7 +68,7 @@ export function getUserMetadata(this: SetMetadata): UserMetadata {
export async function verifyInternetIdentifier(
this: SetMetadata,
opts?: VerificationOptions
opts?: VerificationOptions,
): Promise<InternetIdentifier | undefined> {
const metadata = this.getUserMetadata()
if (metadata.nip05 === undefined) {
@ -81,14 +81,14 @@ export async function verifyInternetIdentifier(
!/^[a-zA-Z0-9-_]+$/.test(name)
) {
throw new NostrError(
`invalid NIP-05 internet identifier: ${metadata.nip05}`
`invalid NIP-05 internet identifier: ${metadata.nip05}`,
)
}
const res = await fetch(
`${
opts?.https === false ? "http" : "https"
}://${domain}/.well-known/nostr.json?name=${name}`,
{ redirect: "error" }
{ redirect: "error" },
)
const wellKnown = await res.json()
const pubkey = wellKnown.names?.[name]
@ -96,7 +96,7 @@ export async function verifyInternetIdentifier(
throw new NostrError(
`invalid NIP-05 internet identifier: ${
metadata.nip05
} pubkey does not match, ${JSON.stringify(wellKnown)}`
} pubkey does not match, ${JSON.stringify(wellKnown)}`,
)
}
const relays = wellKnown.relays?.[pubkey]

View File

@ -12,7 +12,7 @@ export interface TextNote extends RawEvent {
export function createTextNote(
content: string,
priv?: HexOrBechPrivateKey
priv?: HexOrBechPrivateKey,
): Promise<TextNote> {
return signEvent(
{
@ -20,6 +20,6 @@ export function createTextNote(
tags: [],
content,
},
priv
priv,
)
}

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@ -18,7 +18,7 @@ app.use("/", (req: express.Request, res: express.Response) => {
.readdirSync(path.join(__dirname, "..", "..", "dist", "test"))
.filter(
(f) =>
f.startsWith("test.") && !f.endsWith(".map") && !f.endsWith(".d.ts")
f.startsWith("test.") && !f.endsWith(".map") && !f.endsWith(".d.ts"),
)
.map((src) => `<script src="${src}"></script>`)
.join("\n")

View File

@ -30,7 +30,7 @@ export interface Setup {
export async function setup(
done: (e?: unknown) => void,
test: (setup: Setup) => void | Promise<void>
test: (setup: Setup) => void | Promise<void>,
) {
try {
await restartRelay()
@ -55,7 +55,7 @@ export async function setup(
const { data, iv } = await aesEncryptBase64(
parsePrivateKey(publisherSecret),
pubkey,
plaintext
plaintext,
)
return `${data}?iv=${iv}`
},
@ -67,7 +67,7 @@ export async function setup(
{
data,
iv,
}
},
)
},
},

View File

@ -52,7 +52,7 @@ describe("deletion", () => {
// After the text note has been published, delete it.
const deletion = await createDeletion(
{ events: [textNoteId] },
publisherSecret
publisherSecret,
)
deletionId = deletion.id
publisher.publish({
@ -66,7 +66,7 @@ describe("deletion", () => {
subscriber.subscribe([])
}
})
}
},
)
})
})

View File

@ -34,16 +34,16 @@ describe("direct-message", () => {
if (event.kind === EventKind.DirectMessage) {
assert.strictEqual(
event.getRecipient(),
parsePublicKey(subscriberPubkey)
parsePublicKey(subscriberPubkey),
)
assert.strictEqual(
await event.getMessage(subscriberSecret),
message
message,
)
}
done()
}
},
)
const subscriptionId = subscriber.subscribe([])
@ -54,11 +54,11 @@ describe("direct-message", () => {
message,
recipient: subscriberPubkey,
},
publisherSecret
publisherSecret,
)
publisher.publish(event)
})
}
},
)
})
@ -92,11 +92,11 @@ describe("direct-message", () => {
if (event.kind === EventKind.DirectMessage) {
assert.strictEqual(
event.getRecipient(),
parsePublicKey(recipientPubkey)
parsePublicKey(recipientPubkey),
)
assert.strictEqual(
await event.getMessage(subscriberSecret),
undefined
undefined,
)
}
@ -104,7 +104,7 @@ describe("direct-message", () => {
} catch (e) {
done(e)
}
}
},
)
const subscriptionId = subscriber.subscribe([])
@ -116,11 +116,11 @@ describe("direct-message", () => {
message,
recipient: recipientPubkey,
},
publisherSecret
publisherSecret,
)
publisher.publish(event)
})
}
},
)
})
})

View File

@ -34,7 +34,7 @@ describe("internet-identifier", () => {
picture: "",
nip05: "bob@localhost:12647",
},
publisherSecret
publisherSecret,
)),
})
})
@ -66,7 +66,7 @@ describe("internet-identifier", () => {
name: "",
picture: "",
},
publisherSecret
publisherSecret,
)),
})
})

View File

@ -16,7 +16,7 @@ describe("relay info", () => {
assert.ok((relay.info.supported_nips?.length ?? 0) > 0)
assert.strictEqual(
relay.info.software,
"https://git.sr.ht/~gheartsfield/nostr-rs-relay"
"https://git.sr.ht/~gheartsfield/nostr-rs-relay",
)
assert.strictEqual(relay.info.version, "0.8.8")
}

View File

@ -43,12 +43,12 @@ describe("set metadata", () => {
publisher.publish({
...(await createSetMetadata(
{ name, about, picture },
publisherSecret
publisherSecret,
)),
created_at: timestamp,
})
})
}
},
)
})
})

View File

@ -30,7 +30,7 @@ describe("text note", () => {
assert.strictEqual(event.content, note)
assert.strictEqual(actualSubscriptionId, subscriptionId)
done()
}
},
)
const subscriptionId = subscriber.subscribe([])
@ -45,7 +45,7 @@ describe("text note", () => {
created_at: timestamp,
})
})
}
},
)
})

View File

@ -32,7 +32,7 @@ export abstract class FeedCache<TCached> {
this.cache.size,
this.onTable.size,
this.#hooks.length,
((this.#hits / (this.#hits + this.#miss)) * 100).toFixed(1)
((this.#hits / (this.#hits + this.#miss)) * 100).toFixed(1),
);
}, 30_000);
}
@ -174,7 +174,7 @@ export abstract class FeedCache<TCached> {
`Loaded %d/%d in %d ms`,
fromCacheFiltered.length,
keys.length,
(unixNowMs() - start).toLocaleString()
(unixNowMs() - start).toLocaleString(),
);
return mapped.filter(a => !a.has).map(a => a.key);
}

View File

@ -73,7 +73,7 @@ export function countMembers(a: any) {
export function equalProp(
a: string | number | Array<string | number> | undefined,
b: string | number | Array<string | number> | undefined
b: string | number | Array<string | number> | undefined,
) {
if ((a !== undefined && b === undefined) || (a === undefined && b !== undefined)) {
return false;

View File

@ -8,7 +8,7 @@ import { unwrap } from "@snort/shared";
const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>(
system: SystemInterface,
type: { new (): TStore },
rb: RequestBuilder | null
rb: RequestBuilder | null,
) => {
const subscribe = (onChanged: () => void) => {
if (rb) {
@ -33,7 +33,7 @@ const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TSto
};
return useSyncExternalStore<StoreSnapshot<TSnapshot>>(
v => subscribe(v),
() => getState()
() => getState(),
);
};

View File

@ -5,6 +5,6 @@ import { ExternalStore } from "@snort/shared";
export function useSystemState(system: ExternalStore<SystemSnapshot>) {
return useSyncExternalStore<SystemSnapshot>(
cb => system.hook(cb),
() => system.snapshot()
() => system.snapshot(),
);
}

View File

@ -18,6 +18,6 @@ export function useUserProfile(system: NostrSystem, pubKey?: HexKey): MetadataCa
}
};
},
() => system.ProfileLoader.Cache.getFromCache(pubKey)
() => system.ProfileLoader.Cache.getFromCache(pubKey),
);
}

View File

@ -96,7 +96,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
});
}
},
5
5,
);
setTimeout(() => this.#processZapperQueue(), 1_000);
@ -116,7 +116,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
});
}
},
5
5,
);
setTimeout(() => this.#processNip5Queue(), 1_000);
@ -135,7 +135,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
console.warn("Failed to process item", i);
}
batch.pop(); // pop any
})()
})(),
);
if (batch.length === batchSize) {
await Promise.all(batch);

View File

@ -153,7 +153,7 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
this.#log(
`[${this.Address}] Closed (code=${e.code}), trying again in ${(this.ConnectTimeout / 1000)
.toFixed(0)
.toLocaleString()} sec`
.toLocaleString()} sec`,
);
this.ReconnectTimer = setTimeout(() => {
this.Connect();
@ -425,7 +425,7 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
"%s Inactive connection has %d active requests! %O",
this.Address,
this.ActiveRequests.size,
this.ActiveRequests
this.ActiveRequests,
);
} else {
this.Close();

View File

@ -33,4 +33,4 @@ export const CashuRegex = /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/i;
/**
* Regex to match any npub/nevent/naddr/nprofile/note
*/
export const MentionNostrEntityRegex = /@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g;
export const MentionNostrEntityRegex = /@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g;

View File

@ -43,9 +43,7 @@ export class EventBuilder {
*/
processContent() {
if (this.#content) {
this.#content = this.#content.replace(MentionNostrEntityRegex, m =>
this.#replaceMention(m)
);
this.#content = this.#content.replace(MentionNostrEntityRegex, m => this.#replaceMention(m));
const hashTags = [...this.#content.matchAll(HashtagRegex)];
hashTags.map(hashTag => {

View File

@ -146,7 +146,7 @@ export class EventPublisher {
relays: Array<string>,
note?: HexKey,
msg?: string,
fnExtra?: EventBuilderHook
fnExtra?: EventBuilderHook,
) {
const eb = this.#eb(EventKind.ZapRequest);
eb.content(msg ?? "");

View File

@ -175,6 +175,6 @@ function pickTopRelays(cache: RelayCache, authors: Array<string>, n: number) {
key: a.key,
relays: [],
};
})
}),
);
}

View File

@ -1,6 +1,4 @@
import { MessageEncryptor, MessageEncryptorPayload, MessageEncryptorVersion } from "index";
import { base64 } from "@scure/base";
import { secp256k1 } from "@noble/curves/secp256k1";
export class Nip4WebCryptoEncryptor implements MessageEncryptor {
@ -20,7 +18,7 @@ export class Nip4WebCryptoEncryptor implements MessageEncryptor {
iv: iv,
},
key,
data
data,
);
return {
ciphertext: new Uint8Array(result),

View File

@ -94,7 +94,7 @@ export class Nip46Signer implements EventSigner {
"#p": [this.#localPubkey],
},
],
() => {}
() => {},
);
if (isBunker) {
@ -181,7 +181,7 @@ export class Nip46Signer implements EventSigner {
result: "ack",
error: "",
},
unwrap(this.#remotePubkey)
unwrap(this.#remotePubkey),
);
id = "connect";
}

View File

@ -37,7 +37,7 @@ export class Nip7Signer implements EventSigner {
throw new Error("Cannot use NIP-07 signer, not found!");
}
return await barrierQueue(Nip7Queue, () =>
unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content)
unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content),
);
}
@ -46,7 +46,7 @@ export class Nip7Signer implements EventSigner {
throw new Error("Cannot use NIP-07 signer, not found!");
}
return await barrierQueue(Nip7Queue, () =>
unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content)
unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content),
);
}

View File

@ -128,7 +128,7 @@ export class ProfileLoaderService {
pubkey: a,
loaded: unixNowMs() - ProfileCacheExpire + 30_000, // expire in 30s
created: 69,
} as MetadataCache)
} as MetadataCache),
);
await Promise.all(empty);
}

View File

@ -27,7 +27,7 @@ class QueryTrace {
readonly filters: Array<ReqFilter>,
readonly connId: string,
fnClose: (id: string) => void,
fnProgress: () => void
fnProgress: () => void,
) {
this.id = uuid();
this.start = unixNowMs();
@ -293,7 +293,7 @@ export class Query implements QueryBase {
q.filters,
c.Id,
x => c.CloseReq(x),
() => this.#onProgress()
() => this.#onProgress(),
);
this.#tracing.push(qt);
c.QueueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());

View File

@ -5,195 +5,198 @@ import { validateNostrLink } from "./nostr-link";
import { splitByUrl } from "./utils";
export interface ParsedFragment {
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji"
content: string
mimeType?: string
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji";
content: string;
mimeType?: string;
}
export type Fragment = string | ParsedFragment;
function extractLinks(fragments: Fragment[]) {
return fragments
.map(f => {
if (typeof f === "string") {
return splitByUrl(f).map(a => {
const validateLink = () => {
const normalizedStr = a.toLowerCase();
return fragments
.map(f => {
if (typeof f === "string") {
return splitByUrl(f).map(a => {
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()) {
const url = new URL(a);
const extension = url.pathname.match(FileExtensionRegex);
if (extension && extension.length > 1) {
const mediaType = (() => {
switch (extension[1]) {
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":
case "m3u8":
return "video";
default:
return "unknown";
}
})();
return {
type: "media",
content: a,
mimeType: `${mediaType}/${extension[1]}`
} as ParsedFragment;
} else {
return {
type: "link",
content: a
} as ParsedFragment;
}
}
return a;
});
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
return validateNostrLink(normalizedStr);
}
return f;
})
.flat();
return (
normalizedStr.startsWith("http:") ||
normalizedStr.startsWith("https:") ||
normalizedStr.startsWith("magnet:")
);
};
if (validateLink()) {
const url = new URL(a);
const extension = url.pathname.match(FileExtensionRegex);
if (extension && extension.length > 1) {
const mediaType = (() => {
switch (extension[1]) {
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":
case "m3u8":
return "video";
default:
return "unknown";
}
})();
return {
type: "media",
content: a,
mimeType: `${mediaType}/${extension[1]}`,
} as ParsedFragment;
} else {
return {
type: "link",
content: a,
} as ParsedFragment;
}
}
return a;
});
}
return f;
})
.flat();
}
function extractMentions(fragments: Fragment[]) {
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(MentionNostrEntityRegex).map(i => {
if (MentionNostrEntityRegex.test(i)) {
return {
type: "mention",
content: i
} as ParsedFragment;
} else {
return i;
}
});
}
return f;
})
.flat();
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(MentionNostrEntityRegex).map(i => {
if (MentionNostrEntityRegex.test(i)) {
return {
type: "mention",
content: i,
} as ParsedFragment;
} else {
return i;
}
});
}
return f;
})
.flat();
}
function extractCashuTokens(fragments: Fragment[]) {
return fragments
.map(f => {
if (typeof f === "string" && f.includes("cashuA")) {
return f.split(CashuRegex).map(a => {
return {
type: "cashu",
content: a
} as ParsedFragment
});
}
return f;
})
.flat();
return fragments
.map(f => {
if (typeof f === "string" && f.includes("cashuA")) {
return f.split(CashuRegex).map(a => {
return {
type: "cashu",
content: a,
} as ParsedFragment;
});
}
return f;
})
.flat();
}
function extractInvoices(fragments: Fragment[]) {
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(InvoiceRegex).map(i => {
if (i.toLowerCase().startsWith("lnbc")) {
return {
type: "invoice",
content: i
} as ParsedFragment
} else {
return i;
}
});
}
return f;
})
.flat();
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(InvoiceRegex).map(i => {
if (i.toLowerCase().startsWith("lnbc")) {
return {
type: "invoice",
content: i,
} as ParsedFragment;
} else {
return i;
}
});
}
return f;
})
.flat();
}
function extractHashtags(fragments: Fragment[]) {
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(HashtagRegex).map(i => {
if (i.toLowerCase().startsWith("#")) {
return {
type: "hashtag",
content: i.substring(1)
} as ParsedFragment;
} else {
return i;
}
});
}
return f;
})
.flat();
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(HashtagRegex).map(i => {
if (i.toLowerCase().startsWith("#")) {
return {
type: "hashtag",
content: i.substring(1),
} as ParsedFragment;
} else {
return i;
}
});
}
return f;
})
.flat();
}
function extractCustomEmoji(fragments: Fragment[], tags: Array<Array<string>>) {
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(/:(\w+):/g).map(i => {
const t = tags.find(a => a[0] === "emoji" && a[1] === i);
if (t) {
return {
type: "custom_emoji",
content: t[2]
} as ParsedFragment
} else {
return i;
}
});
}
return f;
})
.flat();
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(/:(\w+):/g).map(i => {
const t = tags.find(a => a[0] === "emoji" && a[1] === i);
if (t) {
return {
type: "custom_emoji",
content: t[2],
} as ParsedFragment;
} else {
return i;
}
});
}
return f;
})
.flat();
}
export function transformText(body: string, tags: Array<Array<string>>) {
let fragments = extractLinks([body]);
fragments = extractMentions(fragments);
fragments = extractHashtags(fragments);
fragments = extractInvoices(fragments);
fragments = extractCashuTokens(fragments);
fragments = extractCustomEmoji(fragments, tags);
fragments = fragments.map(a => {
if (typeof a === "string") {
if (a.length > 0) {
return { type: "text", content: a } as ParsedFragment;
}
} else {
return a;
let fragments = extractLinks([body]);
fragments = extractMentions(fragments);
fragments = extractHashtags(fragments);
fragments = extractInvoices(fragments);
fragments = extractCashuTokens(fragments);
fragments = extractCustomEmoji(fragments, tags);
fragments = fragments
.map(a => {
if (typeof a === "string") {
if (a.length > 0) {
return { type: "text", content: a } as ParsedFragment;
}
}).filter(a => a).map(a => unwrap(a));
return fragments as Array<ParsedFragment>;
}
} else {
return a;
}
})
.filter(a => a)
.map(a => unwrap(a));
return fragments as Array<ParsedFragment>;
}

View File

@ -27,24 +27,26 @@ export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | Req
}
export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean {
return a.keys === b.keys
&& a.since === b.since
&& a.until === b.until
&& a.limit === b.limit
&& a.search === b.search
&& a.ids === b.ids
&& a.kinds === b.kinds
&& a.authors === b.authors
&& a["#e"] === b["#e"]
&& a["#p"] === b["#p"]
&& a["#t"] === b["#t"]
&& a["#d"] === b["#d"]
&& a["#r"] === b["#r"];
return (
a.keys === b.keys &&
a.since === b.since &&
a.until === b.until &&
a.limit === b.limit &&
a.search === b.search &&
a.ids === b.ids &&
a.kinds === b.kinds &&
a.authors === b.authors &&
a["#e"] === b["#e"] &&
a["#p"] === b["#p"] &&
a["#t"] === b["#t"] &&
a["#d"] === b["#d"] &&
a["#r"] === b["#r"]
);
}
export function splitByUrl(str: string) {
const urlRegex =
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
return str.split(urlRegex);
}
const urlRegex =
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
return str.split(urlRegex);
}

View File

@ -193,7 +193,7 @@ describe("build diff, large follow list", () => {
},
],
};
})
}),
);
expect(unixNowMs() - start).toBeLessThan(500);

View File

@ -17,8 +17,8 @@ describe("tryParseNostrLink", () => {
});
expect(
parseNostrLink(
"nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"
)
"nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p",
),
).toMatchObject({
id: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
type: NostrPrefix.Profile,
@ -30,8 +30,8 @@ describe("tryParseNostrLink", () => {
});
expect(
parseNostrLink(
"nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu"
)
"nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu",
),
).toMatchObject({
id: "ipsum",
type: NostrPrefix.Address,

View File

@ -12617,6 +12617,15 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:^3.0.0":
version: 3.0.0
resolution: "prettier@npm:3.0.0"
bin:
prettier: bin/prettier.cjs
checksum: 6a832876a1552dc58330d2467874e5a0b46b9ccbfc5d3531eb69d15684743e7f83dc9fbd202db6270446deba9c82b79d24383d09924c462b457136a759425e33
languageName: node
linkType: hard
"pretty-bytes@npm:^5.3.0, pretty-bytes@npm:^5.4.1":
version: 5.6.0
resolution: "pretty-bytes@npm:5.6.0"
@ -13411,6 +13420,7 @@ __metadata:
"@cloudflare/workers-types": ^4.20230307.0
"@tauri-apps/cli": ^1.2.3
eslint: ^8.44.0
prettier: ^3.0.0
typescript: ^5.1.6
languageName: unknown
linkType: soft