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.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true, "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 yarnPath: .yarn/releases/yarn-3.6.1.cjs
npmScopes: npmScopes:
void-cat: 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" "test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20230307.0",
"@tauri-apps/cli": "^1.2.3", "@tauri-apps/cli": "^1.2.3",
"@cloudflare/workers-types": "^4.20230307.0" "prettier": "^3.0.0"
}, },
"prettier": { "prettier": {
"printWidth": 120, "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" /> <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>
<symbol id="zapCircle" viewBox="0 0 33 32" fill="none"> <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>
<symbol id="search" viewBox="0 0 20 21" fill="none"> <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" />
@ -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"/> <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> </g>
</symbol> </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> </defs>
</svg> </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"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <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)}> <div className={`flex flex-row copy ${className}`} onClick={() => copy(text)}>
<span className="body">{trimmed}</span> <span className="body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}> <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> </span>
</div> </div>
); );

View File

@ -13,7 +13,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
<h3>{title}</h3> <h3>{title}</h3>
</div> </div>
<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"> <button className="primary" type="button">
<FormattedMessage defaultMessage="Watch Live!" /> <FormattedMessage defaultMessage="Watch Live!" />
</button> </button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,3 @@
.thread-container {
margin: 12px 0 150px 0;
}
.thread-container .hidden-note { .thread-container .hidden-note {
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
@ -11,11 +7,6 @@
box-shadow: none; box-shadow: none;
} }
.thread-root.note > .body {
margin-top: 8px;
padding-left: 8px;
}
.thread-root.note > .body .text { .thread-root.note > .body .text {
font-size: 19px; font-size: 19px;
} }
@ -31,12 +22,13 @@
} }
.thread-note.note { .thread-note.note {
border-radius: 0; border: 0;
margin-bottom: 0;
} }
.light .thread-note.note.card { .thread-note.note .zaps-summary,
box-shadow: none; .thread-note.note .footer,
.thread-note.note .body {
margin-left: 61px;
} }
.thread-container .hidden-note { .thread-container .hidden-note {
@ -58,83 +50,47 @@
position: relative; position: relative;
} }
.line-container {
background: var(--note-bg);
}
.subthread-container.subthread-multi .line-container:before { .subthread-container.subthread-multi .line-container:before {
content: ""; content: "";
position: absolute; position: absolute;
left: 36px; left: calc(48px / 2 + 16px);
top: 48px; top: 48px;
border-left: 1px solid var(--gray-superdark); border-left: 1px solid var(--gray-superdark);
height: 100%; height: 100%;
z-index: -1;
} }
@media (min-width: 720px) { .subthread-container.subthread-mid:not(.subthread-last) .line-container:before {
.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 {
content: ""; content: "";
position: absolute; position: absolute;
border-left: 1px solid var(--gray-superdark); border-left: 1px solid var(--gray-superdark);
left: 36px; left: calc(48px / 2 + 16px);
top: 0; top: 0;
height: 48px; height: 48px;
} z-index: -1;
@media (min-width: 720px) {
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
left: 48px;
}
} }
.subthread-container.subthread-last .line-container:before { .subthread-container.subthread-last .line-container:before {
content: ""; content: "";
position: absolute; position: absolute;
border-left: 1px solid var(--gray-superdark); border-left: 1px solid var(--gray-superdark);
left: 36px; left: calc(48px / 2 + 16px);
top: 0; top: 0;
height: 48px; height: 48px;
} z-index: -1;
@media (min-width: 720px) {
.subthread-container.subthread-last .line-container:before {
left: 48px;
}
} }
.divider-container { .divider-container {
background: var(--note-bg); margin-right: 16px;
} }
.divider { .divider {
height: 1px; height: 1px;
background: var(--gray-superdark); background: var(--gray-superdark);
margin-left: 28px;
margin-right: 22px;
} }
.divider.divider-small { .divider.divider-small {
margin-left: 80px; margin-left: calc(16px + 61px);
} }
.thread-container .collapsed, .thread-container .collapsed,
@ -143,11 +99,6 @@
min-height: 48px; min-height: 48px;
} }
.thread-note.is-last-note {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
.thread-container .collapsed { .thread-container .collapsed {
background-color: var(--note-bg); background-color: var(--note-bg);
} }
@ -155,13 +106,3 @@
.thread-container .hidden-note { .thread-container .hidden-note {
padding-left: 48px; 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", description: "Navigate back button on threads view",
}); });
return ( return (
<div className="main-content mt10"> <>
<BackButton onClick={goBack} text={parent ? parentText : backText} /> <div className="main-content">
<div className="thread-container"> <BackButton onClick={goBack} text={parent ? parentText : backText} />
</div>
<div className="main-content">
{root && renderRoot(root)} {root && renderRoot(root)}
{root && renderChain(root.id)} {root && renderChain(root.id)}
@ -392,7 +394,7 @@ export default function Thread() {
); );
})} })}
</div> </div>
</div> </>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -226,12 +226,12 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "EVENT") { if (json[0] === "EVENT") {
if (typeof json[1] !== "string") { if (typeof json[1] !== "string") {
throw new NostrError( 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") { if (typeof json[2] !== "object") {
throw new NostrError( 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]) const event = parseEventData(json[2])
@ -246,7 +246,7 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "NOTICE") { if (json[0] === "NOTICE") {
if (typeof json[1] !== "string") { if (typeof json[1] !== "string") {
throw new NostrError( 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 { return {
@ -259,17 +259,17 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "OK") { if (json[0] === "OK") {
if (typeof json[1] !== "string") { if (typeof json[1] !== "string") {
throw new NostrError( 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") { if (typeof json[2] !== "boolean") {
throw new NostrError( 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") { if (typeof json[3] !== "string") {
throw new NostrError( 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 { return {
@ -284,7 +284,7 @@ function parseIncomingMessage(data: string): IncomingMessage {
if (json[0] === "EOSE") { if (json[0] === "EOSE") {
if (typeof json[1] !== "string") { if (typeof json[1] !== "string") {
throw new NostrError( 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 { return {
@ -312,7 +312,7 @@ function parseEventData(json: { [key: string]: unknown }): RawEvent {
typeof json["kind"] !== "number" || typeof json["kind"] !== "number" ||
!(json["tags"] instanceof Array) || !(json["tags"] instanceof Array) ||
!json["tags"].every( !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["content"] !== "string" ||
typeof json["sig"] !== "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: "newListener", listener: NewListener): this
override addListener( override addListener(
eventName: "removeListener", eventName: "removeListener",
listener: RemoveListener listener: RemoveListener,
): this ): this
override addListener(eventName: "open", listener: OpenListener): this override addListener(eventName: "open", listener: OpenListener): this
override addListener(eventName: "close", listener: CloseListener): this override addListener(eventName: "close", listener: CloseListener): this
@ -36,7 +36,7 @@ export class EventEmitter extends Base {
override emit( override emit(
eventName: "eose", eventName: "eose",
subscriptionId: SubscriptionId, subscriptionId: SubscriptionId,
nostr: Nostr nostr: Nostr,
): boolean ): boolean
override emit(eventName: "error", err: unknown, nostr: Nostr): boolean override emit(eventName: "error", err: unknown, nostr: Nostr): boolean
override emit(eventName: EventName, ...args: unknown[]): boolean { override emit(eventName: EventName, ...args: unknown[]): boolean {
@ -101,11 +101,11 @@ export class EventEmitter extends Base {
override prependListener( override prependListener(
eventName: "newListener", eventName: "newListener",
listener: NewListener listener: NewListener,
): this ): this
override prependListener( override prependListener(
eventName: "removeListener", eventName: "removeListener",
listener: RemoveListener listener: RemoveListener,
): this ): this
override prependListener(eventName: "open", listener: OpenListener): this override prependListener(eventName: "open", listener: OpenListener): this
override prependListener(eventName: "close", listener: CloseListener): this override prependListener(eventName: "close", listener: CloseListener): this
@ -120,30 +120,30 @@ export class EventEmitter extends Base {
override prependOnceListener( override prependOnceListener(
eventName: "newListener", eventName: "newListener",
listener: NewListener listener: NewListener,
): this ): this
override prependOnceListener( override prependOnceListener(
eventName: "removeListener", eventName: "removeListener",
listener: RemoveListener listener: RemoveListener,
): this ): this
override prependOnceListener(eventName: "open", listener: OpenListener): this override prependOnceListener(eventName: "open", listener: OpenListener): this
override prependOnceListener( override prependOnceListener(
eventName: "close", eventName: "close",
listener: CloseListener listener: CloseListener,
): this ): this
override prependOnceListener( override prependOnceListener(
eventName: "event", eventName: "event",
listener: EventListener listener: EventListener,
): this ): this
override prependOnceListener( override prependOnceListener(
eventName: "notice", eventName: "notice",
listener: NoticeListener listener: NoticeListener,
): this ): this
override prependOnceListener(eventName: "ok", listener: OkListener): this override prependOnceListener(eventName: "ok", listener: OkListener): this
override prependOnceListener(eventName: "eose", listener: EoseListener): this override prependOnceListener(eventName: "eose", listener: EoseListener): this
override prependOnceListener( override prependOnceListener(
eventName: "error", eventName: "error",
listener: ErrorListener listener: ErrorListener,
): this ): this
override prependOnceListener(eventName: EventName, listener: Listener): this { override prependOnceListener(eventName: EventName, listener: Listener): this {
return super.prependOnceListener(eventName, listener) return super.prependOnceListener(eventName, listener)
@ -156,7 +156,7 @@ export class EventEmitter extends Base {
override removeListener(eventName: "newListener", listener: NewListener): this override removeListener(eventName: "newListener", listener: NewListener): this
override removeListener( override removeListener(
eventName: "removeListener", eventName: "removeListener",
listener: RemoveListener listener: RemoveListener,
): this ): this
override removeListener(eventName: "open", listener: OpenListener): this override removeListener(eventName: "open", listener: OpenListener): this
override removeListener(eventName: "close", listener: CloseListener): this override removeListener(eventName: "close", listener: CloseListener): this

View File

@ -43,18 +43,18 @@ export class Nostr extends EventEmitter {
*/ */
open( open(
url: URL | string, url: URL | string,
opts?: { read?: boolean; write?: boolean; fetchInfo?: boolean } opts?: { read?: boolean; write?: boolean; fetchInfo?: boolean },
): void { ): void {
const relayUrl = new URL(url) const relayUrl = new URL(url)
// If the connection already exists, update the options. // If the connection already exists, update the options.
const existingConn = this.#conns.find( const existingConn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString() (c) => c.relay.url.toString() === relayUrl.toString(),
) )
if (existingConn !== undefined) { if (existingConn !== undefined) {
if (opts === undefined) { if (opts === undefined) {
throw new NostrError( 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) { if (opts.read !== undefined) {
@ -88,7 +88,7 @@ export class Nostr extends EventEmitter {
event: parseEvent(msg.event), event: parseEvent(msg.event),
subscriptionId: msg.subscriptionId, subscriptionId: msg.subscriptionId,
}, },
this this,
) )
} else if (msg.kind === "notice") { } else if (msg.kind === "notice") {
this.emit("notice", msg.notice, this) this.emit("notice", msg.notice, this)
@ -101,7 +101,7 @@ export class Nostr extends EventEmitter {
ok: msg.ok, ok: msg.ok,
message: msg.message, message: msg.message,
}, },
this this,
) )
} else if (msg.kind === "eose") { } else if (msg.kind === "eose") {
this.emit("eose", msg.subscriptionId, this) this.emit("eose", msg.subscriptionId, this)
@ -116,13 +116,13 @@ export class Nostr extends EventEmitter {
onOpen: async () => { onOpen: async () => {
// Update the connection readyState. // Update the connection readyState.
const conn = this.#conns.find( const conn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString() (c) => c.relay.url.toString() === relayUrl.toString(),
) )
if (conn === undefined) { if (conn === undefined) {
this.#error( this.#error(
new NostrError( 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 { } else {
if (conn.relay.readyState !== ReadyState.CONNECTING) { if (conn.relay.readyState !== ReadyState.CONNECTING) {
@ -130,8 +130,8 @@ export class Nostr extends EventEmitter {
new NostrError( new NostrError(
`bug: expected connection to ${relayUrl.toString()} to have readyState CONNECTING, got ${ `bug: expected connection to ${relayUrl.toString()} to have readyState CONNECTING, got ${
conn.relay.readyState conn.relay.readyState
}` }`,
) ),
) )
} }
conn.relay = { conn.relay = {
@ -148,13 +148,13 @@ export class Nostr extends EventEmitter {
onClose: () => { onClose: () => {
// Update the connection readyState. // Update the connection readyState.
const conn = this.#conns.find( const conn = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString() (c) => c.relay.url.toString() === relayUrl.toString(),
) )
if (conn === undefined) { if (conn === undefined) {
this.#error( this.#error(
new NostrError( 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 { } else {
conn.relay.readyState = ReadyState.CLOSED conn.relay.readyState = ReadyState.CLOSED
@ -207,7 +207,7 @@ export class Nostr extends EventEmitter {
} }
const relayUrl = new URL(url) const relayUrl = new URL(url)
const c = this.#conns.find( const c = this.#conns.find(
(c) => c.relay.url.toString() === relayUrl.toString() (c) => c.relay.url.toString() === relayUrl.toString(),
) )
if (c === undefined) { if (c === undefined) {
throw new NostrError(`connection to ${url} doesn't exist`) throw new NostrError(`connection to ${url} doesn't exist`)
@ -231,7 +231,7 @@ export class Nostr extends EventEmitter {
*/ */
subscribe( subscribe(
filters: Filters[], filters: Filters[],
subscriptionId: SubscriptionId = randomSubscriptionId() subscriptionId: SubscriptionId = randomSubscriptionId(),
): SubscriptionId { ): SubscriptionId {
this.#subscriptions.set(subscriptionId, filters) this.#subscriptions.set(subscriptionId, filters)
for (const { conn, read } of this.#conns.values()) { 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 info.name = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "name" to be a string: ${JSON.stringify( `invalid relay info, expected "name" to be a string: ${JSON.stringify(
info info,
)}` )}`,
) )
} }
if (info.description !== undefined && typeof info.description !== "string") { if (info.description !== undefined && typeof info.description !== "string") {
info.description = undefined info.description = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "description" to be a string: ${JSON.stringify( `invalid relay info, expected "description" to be a string: ${JSON.stringify(
info info,
)}` )}`,
) )
} }
if (info.pubkey !== undefined && typeof info.pubkey !== "string") { if (info.pubkey !== undefined && typeof info.pubkey !== "string") {
info.pubkey = undefined info.pubkey = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "pubkey" to be a string: ${JSON.stringify( `invalid relay info, expected "pubkey" to be a string: ${JSON.stringify(
info info,
)}` )}`,
) )
} }
if (info.contact !== undefined && typeof info.contact !== "string") { if (info.contact !== undefined && typeof info.contact !== "string") {
info.contact = undefined info.contact = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "contact" to be a string: ${JSON.stringify( `invalid relay info, expected "contact" to be a string: ${JSON.stringify(
info info,
)}` )}`,
) )
} }
if (info.supported_nips !== undefined) { if (info.supported_nips !== undefined) {
@ -109,16 +109,16 @@ export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
info.supported_nips = undefined info.supported_nips = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "supported_nips" elements to be numbers: ${JSON.stringify( `invalid relay info, expected "supported_nips" elements to be numbers: ${JSON.stringify(
info info,
)}` )}`,
) )
} }
} else { } else {
info.supported_nips = undefined info.supported_nips = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "supported_nips" to be an array: ${JSON.stringify( `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 info.software = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "software" to be a string: ${JSON.stringify( `invalid relay info, expected "software" to be a string: ${JSON.stringify(
info info,
)}` )}`,
) )
} }
if (info.version !== undefined && typeof info.version !== "string") { if (info.version !== undefined && typeof info.version !== "string") {
info.version = undefined info.version = undefined
throw new NostrError( throw new NostrError(
`invalid relay info, expected "version" to be a string: ${JSON.stringify( `invalid relay info, expected "version" to be a string: ${JSON.stringify(
info info,
)}` )}`,
) )
} }
return info return info

View File

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

View File

@ -30,7 +30,7 @@ export interface Contact {
*/ */
export function createContactList( export function createContactList(
contacts: Contact[], contacts: Contact[],
priv?: HexOrBechPrivateKey priv?: HexOrBechPrivateKey,
): Promise<ContactList> { ): Promise<ContactList> {
return signEvent( return signEvent(
{ {
@ -44,7 +44,7 @@ export function createContactList(
content: "", content: "",
getContacts, getContacts,
}, },
priv priv,
) )
} }
@ -57,8 +57,8 @@ export function getContacts(this: ContactList): Contact[] {
if (pubkey === undefined) { if (pubkey === undefined) {
throw new NostrError( throw new NostrError(
`missing contact pubkey for contact list event: ${JSON.stringify( `missing contact pubkey for contact list event: ${JSON.stringify(
this this,
)}` )}`,
) )
} }
@ -70,7 +70,7 @@ export function getContacts(this: ContactList): Contact[] {
} }
} catch (e) { } catch (e) {
throw new NostrError( 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( export function createDeletion(
{ events, content }: { events: EventId[]; content?: string }, { events, content }: { events: EventId[]; content?: string },
priv?: HexOrBechPrivateKey priv?: HexOrBechPrivateKey,
): Promise<Deletion> { ): Promise<Deletion> {
return signEvent( return signEvent(
{ {
@ -30,7 +30,7 @@ export function createDeletion(
content: content ?? "", content: content ?? "",
getEvents, getEvents,
}, },
priv priv,
) )
} }
@ -40,7 +40,7 @@ export function getEvents(this: Deletion): EventId[] {
.map((tag) => { .map((tag) => {
if (tag[1] === undefined) { if (tag[1] === undefined) {
throw new NostrError( throw new NostrError(
`invalid deletion event tag: ${JSON.stringify(tag)}` `invalid deletion event tag: ${JSON.stringify(tag)}`,
) )
} }
return tag[1] return tag[1]

View File

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

View File

@ -122,7 +122,7 @@ type UnsignedWithPubkey<T extends Event | RawEvent> = {
*/ */
export async function signEvent<T extends RawEvent>( export async function signEvent<T extends RawEvent>(
event: Unsigned<T>, event: Unsigned<T>,
priv?: HexOrBechPrivateKey priv?: HexOrBechPrivateKey,
): Promise<T> { ): Promise<T> {
event.created_at ??= unixTimestamp() event.created_at ??= unixTimestamp()
if (priv !== undefined) { if (priv !== undefined) {
@ -130,7 +130,7 @@ export async function signEvent<T extends RawEvent>(
event.pubkey = getPublicKey(priv) event.pubkey = getPublicKey(priv)
const id = serializeEventId( const id = serializeEventId(
// This conversion is safe because the pubkey field is set above. // 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.id = id
event.sig = schnorrSign(id, priv) event.sig = schnorrSign(id, priv)
@ -162,8 +162,8 @@ export function parseEvent(event: RawEvent): Event {
if (event.id !== serializeEventId(event)) { if (event.id !== serializeEventId(event)) {
throw new NostrError( throw new NostrError(
`invalid id ${event.id} for event ${JSON.stringify( `invalid id ${event.id} for event ${JSON.stringify(
event event,
)}, expected ${serializeEventId(event)}` )}, expected ${serializeEventId(event)}`,
) )
} }
if (!schnorrVerify(event.sig, event.id, event.pubkey)) { 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. * @return The internet identifier. `undefined` if there is no internet identifier.
*/ */
verifyInternetIdentifier( verifyInternetIdentifier(
opts?: VerificationOptions opts?: VerificationOptions,
): Promise<InternetIdentifier | undefined> ): Promise<InternetIdentifier | undefined>
} }
@ -38,7 +38,7 @@ export interface UserMetadata {
*/ */
export function createSetMetadata( export function createSetMetadata(
content: UserMetadata, content: UserMetadata,
priv?: HexOrBechPrivateKey priv?: HexOrBechPrivateKey,
): Promise<SetMetadata> { ): Promise<SetMetadata> {
return signEvent( return signEvent(
{ {
@ -48,7 +48,7 @@ export function createSetMetadata(
getUserMetadata, getUserMetadata,
verifyInternetIdentifier, verifyInternetIdentifier,
}, },
priv priv,
) )
} }
@ -60,7 +60,7 @@ export function getUserMetadata(this: SetMetadata): UserMetadata {
typeof userMetadata.picture !== "string" typeof userMetadata.picture !== "string"
) { ) {
throw new NostrError( throw new NostrError(
`invalid user metadata ${userMetadata} in ${JSON.stringify(this)}` `invalid user metadata ${userMetadata} in ${JSON.stringify(this)}`,
) )
} }
return userMetadata return userMetadata
@ -68,7 +68,7 @@ export function getUserMetadata(this: SetMetadata): UserMetadata {
export async function verifyInternetIdentifier( export async function verifyInternetIdentifier(
this: SetMetadata, this: SetMetadata,
opts?: VerificationOptions opts?: VerificationOptions,
): Promise<InternetIdentifier | undefined> { ): Promise<InternetIdentifier | undefined> {
const metadata = this.getUserMetadata() const metadata = this.getUserMetadata()
if (metadata.nip05 === undefined) { if (metadata.nip05 === undefined) {
@ -81,14 +81,14 @@ export async function verifyInternetIdentifier(
!/^[a-zA-Z0-9-_]+$/.test(name) !/^[a-zA-Z0-9-_]+$/.test(name)
) { ) {
throw new NostrError( throw new NostrError(
`invalid NIP-05 internet identifier: ${metadata.nip05}` `invalid NIP-05 internet identifier: ${metadata.nip05}`,
) )
} }
const res = await fetch( const res = await fetch(
`${ `${
opts?.https === false ? "http" : "https" opts?.https === false ? "http" : "https"
}://${domain}/.well-known/nostr.json?name=${name}`, }://${domain}/.well-known/nostr.json?name=${name}`,
{ redirect: "error" } { redirect: "error" },
) )
const wellKnown = await res.json() const wellKnown = await res.json()
const pubkey = wellKnown.names?.[name] const pubkey = wellKnown.names?.[name]
@ -96,7 +96,7 @@ export async function verifyInternetIdentifier(
throw new NostrError( throw new NostrError(
`invalid NIP-05 internet identifier: ${ `invalid NIP-05 internet identifier: ${
metadata.nip05 metadata.nip05
} pubkey does not match, ${JSON.stringify(wellKnown)}` } pubkey does not match, ${JSON.stringify(wellKnown)}`,
) )
} }
const relays = wellKnown.relays?.[pubkey] const relays = wellKnown.relays?.[pubkey]

View File

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

View File

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

View File

@ -18,7 +18,7 @@ app.use("/", (req: express.Request, res: express.Response) => {
.readdirSync(path.join(__dirname, "..", "..", "dist", "test")) .readdirSync(path.join(__dirname, "..", "..", "dist", "test"))
.filter( .filter(
(f) => (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>`) .map((src) => `<script src="${src}"></script>`)
.join("\n") .join("\n")

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ describe("relay info", () => {
assert.ok((relay.info.supported_nips?.length ?? 0) > 0) assert.ok((relay.info.supported_nips?.length ?? 0) > 0)
assert.strictEqual( assert.strictEqual(
relay.info.software, 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") assert.strictEqual(relay.info.version, "0.8.8")
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,6 @@ import { ExternalStore } from "@snort/shared";
export function useSystemState(system: ExternalStore<SystemSnapshot>) { export function useSystemState(system: ExternalStore<SystemSnapshot>) {
return useSyncExternalStore<SystemSnapshot>( return useSyncExternalStore<SystemSnapshot>(
cb => system.hook(cb), 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); setTimeout(() => this.#processZapperQueue(), 1_000);
@ -116,7 +116,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
}); });
} }
}, },
5 5,
); );
setTimeout(() => this.#processNip5Queue(), 1_000); setTimeout(() => this.#processNip5Queue(), 1_000);
@ -135,7 +135,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
console.warn("Failed to process item", i); console.warn("Failed to process item", i);
} }
batch.pop(); // pop any batch.pop(); // pop any
})() })(),
); );
if (batch.length === batchSize) { if (batch.length === batchSize) {
await Promise.all(batch); await Promise.all(batch);

View File

@ -153,7 +153,7 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
this.#log( this.#log(
`[${this.Address}] Closed (code=${e.code}), trying again in ${(this.ConnectTimeout / 1000) `[${this.Address}] Closed (code=${e.code}), trying again in ${(this.ConnectTimeout / 1000)
.toFixed(0) .toFixed(0)
.toLocaleString()} sec` .toLocaleString()} sec`,
); );
this.ReconnectTimer = setTimeout(() => { this.ReconnectTimer = setTimeout(() => {
this.Connect(); this.Connect();
@ -425,7 +425,7 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
"%s Inactive connection has %d active requests! %O", "%s Inactive connection has %d active requests! %O",
this.Address, this.Address,
this.ActiveRequests.size, this.ActiveRequests.size,
this.ActiveRequests this.ActiveRequests,
); );
} else { } else {
this.Close(); 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 * 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() { processContent() {
if (this.#content) { if (this.#content) {
this.#content = this.#content.replace(MentionNostrEntityRegex, m => this.#content = this.#content.replace(MentionNostrEntityRegex, m => this.#replaceMention(m));
this.#replaceMention(m)
);
const hashTags = [...this.#content.matchAll(HashtagRegex)]; const hashTags = [...this.#content.matchAll(HashtagRegex)];
hashTags.map(hashTag => { hashTags.map(hashTag => {

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ export class Nip7Signer implements EventSigner {
throw new Error("Cannot use NIP-07 signer, not found!"); throw new Error("Cannot use NIP-07 signer, not found!");
} }
return await barrierQueue(Nip7Queue, () => 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!"); throw new Error("Cannot use NIP-07 signer, not found!");
} }
return await barrierQueue(Nip7Queue, () => 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, pubkey: a,
loaded: unixNowMs() - ProfileCacheExpire + 30_000, // expire in 30s loaded: unixNowMs() - ProfileCacheExpire + 30_000, // expire in 30s
created: 69, created: 69,
} as MetadataCache) } as MetadataCache),
); );
await Promise.all(empty); await Promise.all(empty);
} }

View File

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

View File

@ -5,195 +5,198 @@ import { validateNostrLink } from "./nostr-link";
import { splitByUrl } from "./utils"; import { splitByUrl } from "./utils";
export interface ParsedFragment { export interface ParsedFragment {
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji" type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji";
content: string content: string;
mimeType?: string mimeType?: string;
} }
export type Fragment = string | ParsedFragment; export type Fragment = string | ParsedFragment;
function extractLinks(fragments: Fragment[]) { function extractLinks(fragments: Fragment[]) {
return fragments return fragments
.map(f => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return splitByUrl(f).map(a => { return splitByUrl(f).map(a => {
const validateLink = () => { const validateLink = () => {
const normalizedStr = a.toLowerCase(); const normalizedStr = a.toLowerCase();
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) { if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
return validateNostrLink(normalizedStr); 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;
});
} }
return f;
}) return (
.flat(); 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[]) { function extractMentions(fragments: Fragment[]) {
return fragments return fragments
.map(f => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(MentionNostrEntityRegex).map(i => { return f.split(MentionNostrEntityRegex).map(i => {
if (MentionNostrEntityRegex.test(i)) { if (MentionNostrEntityRegex.test(i)) {
return { return {
type: "mention", type: "mention",
content: i content: i,
} as ParsedFragment; } as ParsedFragment;
} else { } else {
return i; return i;
} }
}); });
} }
return f; return f;
}) })
.flat(); .flat();
} }
function extractCashuTokens(fragments: Fragment[]) { function extractCashuTokens(fragments: Fragment[]) {
return fragments return fragments
.map(f => { .map(f => {
if (typeof f === "string" && f.includes("cashuA")) { if (typeof f === "string" && f.includes("cashuA")) {
return f.split(CashuRegex).map(a => { return f.split(CashuRegex).map(a => {
return { return {
type: "cashu", type: "cashu",
content: a content: a,
} as ParsedFragment } as ParsedFragment;
}); });
} }
return f; return f;
}) })
.flat(); .flat();
} }
function extractInvoices(fragments: Fragment[]) { function extractInvoices(fragments: Fragment[]) {
return fragments return fragments
.map(f => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(InvoiceRegex).map(i => { return f.split(InvoiceRegex).map(i => {
if (i.toLowerCase().startsWith("lnbc")) { if (i.toLowerCase().startsWith("lnbc")) {
return { return {
type: "invoice", type: "invoice",
content: i content: i,
} as ParsedFragment } as ParsedFragment;
} else { } else {
return i; return i;
} }
}); });
} }
return f; return f;
}) })
.flat(); .flat();
} }
function extractHashtags(fragments: Fragment[]) { function extractHashtags(fragments: Fragment[]) {
return fragments return fragments
.map(f => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(HashtagRegex).map(i => { return f.split(HashtagRegex).map(i => {
if (i.toLowerCase().startsWith("#")) { if (i.toLowerCase().startsWith("#")) {
return { return {
type: "hashtag", type: "hashtag",
content: i.substring(1) content: i.substring(1),
} as ParsedFragment; } as ParsedFragment;
} else { } else {
return i; return i;
} }
}); });
} }
return f; return f;
}) })
.flat(); .flat();
} }
function extractCustomEmoji(fragments: Fragment[], tags: Array<Array<string>>) { function extractCustomEmoji(fragments: Fragment[], tags: Array<Array<string>>) {
return fragments return fragments
.map(f => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(/:(\w+):/g).map(i => { return f.split(/:(\w+):/g).map(i => {
const t = tags.find(a => a[0] === "emoji" && a[1] === i); const t = tags.find(a => a[0] === "emoji" && a[1] === i);
if (t) { if (t) {
return { return {
type: "custom_emoji", type: "custom_emoji",
content: t[2] content: t[2],
} as ParsedFragment } as ParsedFragment;
} else { } else {
return i; return i;
} }
}); });
} }
return f; return f;
}) })
.flat(); .flat();
} }
export function transformText(body: string, tags: Array<Array<string>>) { export function transformText(body: string, tags: Array<Array<string>>) {
let fragments = extractLinks([body]); let fragments = extractLinks([body]);
fragments = extractMentions(fragments); fragments = extractMentions(fragments);
fragments = extractHashtags(fragments); fragments = extractHashtags(fragments);
fragments = extractInvoices(fragments); fragments = extractInvoices(fragments);
fragments = extractCashuTokens(fragments); fragments = extractCashuTokens(fragments);
fragments = extractCustomEmoji(fragments, tags); fragments = extractCustomEmoji(fragments, tags);
fragments = fragments.map(a => { fragments = fragments
if (typeof a === "string") { .map(a => {
if (a.length > 0) { if (typeof a === "string") {
return { type: "text", content: a } as ParsedFragment; if (a.length > 0) {
} return { type: "text", content: a } as ParsedFragment;
} else {
return a;
} }
}).filter(a => a).map(a => unwrap(a)); } else {
return fragments as Array<ParsedFragment>; 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 { export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean {
return a.keys === b.keys return (
&& a.since === b.since a.keys === b.keys &&
&& a.until === b.until a.since === b.since &&
&& a.limit === b.limit a.until === b.until &&
&& a.search === b.search a.limit === b.limit &&
&& a.ids === b.ids a.search === b.search &&
&& a.kinds === b.kinds a.ids === b.ids &&
&& a.authors === b.authors a.kinds === b.kinds &&
&& a["#e"] === b["#e"] a.authors === b.authors &&
&& a["#p"] === b["#p"] a["#e"] === b["#e"] &&
&& a["#t"] === b["#t"] a["#p"] === b["#p"] &&
&& a["#d"] === b["#d"] a["#t"] === b["#t"] &&
&& a["#r"] === b["#r"]; a["#d"] === b["#d"] &&
a["#r"] === b["#r"]
);
} }
export function splitByUrl(str: string) { export function splitByUrl(str: string) {
const urlRegex = const urlRegex =
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i; /((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
return str.split(urlRegex); return str.split(urlRegex);
} }

View File

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

View File

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

View File

@ -12617,6 +12617,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "pretty-bytes@npm:^5.3.0, pretty-bytes@npm:^5.4.1":
version: 5.6.0 version: 5.6.0
resolution: "pretty-bytes@npm:5.6.0" resolution: "pretty-bytes@npm:5.6.0"
@ -13411,6 +13420,7 @@ __metadata:
"@cloudflare/workers-types": ^4.20230307.0 "@cloudflare/workers-types": ^4.20230307.0
"@tauri-apps/cli": ^1.2.3 "@tauri-apps/cli": ^1.2.3
eslint: ^8.44.0 eslint: ^8.44.0
prettier: ^3.0.0
typescript: ^5.1.6 typescript: ^5.1.6
languageName: unknown languageName: unknown
linkType: soft linkType: soft