wip
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"@tanstack/react-query": "^4.29.15",
|
"@tanstack/react-query": "^4.29.15",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tauri-apps/api": "^1.4.0",
|
"@tauri-apps/api": "^1.4.0",
|
||||||
|
"cheerio": "1.0.0-rc.12",
|
||||||
"dayjs": "^1.11.8",
|
"dayjs": "^1.11.8",
|
||||||
"destr": "^1.2.2",
|
"destr": "^1.2.2",
|
||||||
"framer-motion": "^10.12.17",
|
"framer-motion": "^10.12.17",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.45.0",
|
"react-hook-form": "^7.45.0",
|
||||||
"react-hotkeys-hook": "^4.4.0",
|
"react-hotkeys-hook": "^4.4.0",
|
||||||
|
"react-player": "^2.12.0",
|
||||||
"react-resizable-panels": "^0.0.48",
|
"react-resizable-panels": "^0.0.48",
|
||||||
"react-router-dom": "^6.14.0",
|
"react-router-dom": "^6.14.0",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
|
|||||||
148
pnpm-lock.yaml
generated
148
pnpm-lock.yaml
generated
@@ -19,6 +19,9 @@ dependencies:
|
|||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
|
cheerio:
|
||||||
|
specifier: 1.0.0-rc.12
|
||||||
|
version: 1.0.0-rc.12
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.8
|
specifier: ^1.11.8
|
||||||
version: 1.11.8
|
version: 1.11.8
|
||||||
@@ -52,6 +55,9 @@ dependencies:
|
|||||||
react-hotkeys-hook:
|
react-hotkeys-hook:
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
version: 4.4.0(react-dom@18.2.0)(react@18.2.0)
|
version: 4.4.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-player:
|
||||||
|
specifier: ^2.12.0
|
||||||
|
version: 2.12.0(react@18.2.0)
|
||||||
react-resizable-panels:
|
react-resizable-panels:
|
||||||
specifier: ^0.0.48
|
specifier: ^0.0.48
|
||||||
version: 0.0.48(react-dom@18.2.0)(react@18.2.0)
|
version: 0.0.48(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -1399,6 +1405,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/boolbase@1.0.0:
|
||||||
|
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/brace-expansion@1.1.11:
|
/brace-expansion@1.1.11:
|
||||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1511,6 +1521,30 @@ packages:
|
|||||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cheerio-select@2.1.0:
|
||||||
|
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
css-select: 5.1.0
|
||||||
|
css-what: 6.1.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/cheerio@1.0.0-rc.12:
|
||||||
|
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
cheerio-select: 2.1.0
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
parse5: 7.1.2
|
||||||
|
parse5-htmlparser2-tree-adapter: 7.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/chokidar@3.5.3:
|
/chokidar@3.5.3:
|
||||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
@@ -1645,6 +1679,21 @@ packages:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
/css-select@5.1.0:
|
||||||
|
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
css-what: 6.1.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
nth-check: 2.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/css-what@6.1.0:
|
||||||
|
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cssesc@3.0.0:
|
/cssesc@3.0.0:
|
||||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -1721,6 +1770,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/define-properties@1.2.0:
|
/define-properties@1.2.0:
|
||||||
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
|
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1781,6 +1835,33 @@ packages:
|
|||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dom-serializer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/domelementtype@2.3.0:
|
||||||
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/domhandler@5.0.3:
|
||||||
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/domutils@3.1.0:
|
||||||
|
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
@@ -1799,6 +1880,11 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
|
|
||||||
|
/entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/env-paths@2.2.1:
|
/env-paths@2.2.1:
|
||||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2560,6 +2646,15 @@ packages:
|
|||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/htmlparser2@8.0.2:
|
||||||
|
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/http-cache-semantics@4.1.1:
|
/http-cache-semantics@4.1.1:
|
||||||
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
|
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -2986,6 +3081,10 @@ packages:
|
|||||||
strip-bom: 3.0.0
|
strip-bom: 3.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/load-script@1.0.0:
|
||||||
|
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/locate-path@5.0.0:
|
/locate-path@5.0.0:
|
||||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -3107,6 +3206,10 @@ packages:
|
|||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/memoize-one@5.2.1:
|
||||||
|
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/memorystream@0.3.1:
|
/memorystream@0.3.1:
|
||||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
@@ -3428,10 +3531,15 @@ packages:
|
|||||||
set-blocking: 2.0.0
|
set-blocking: 2.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/nth-check@2.1.1:
|
||||||
|
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/object-assign@4.1.1:
|
/object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/object-hash@3.0.0:
|
/object-hash@3.0.0:
|
||||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||||
@@ -3560,6 +3668,19 @@ packages:
|
|||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/parse5-htmlparser2-tree-adapter@7.0.0:
|
||||||
|
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
|
||||||
|
dependencies:
|
||||||
|
domhandler: 5.0.3
|
||||||
|
parse5: 7.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/parse5@7.1.2:
|
||||||
|
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||||
|
dependencies:
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/path-exists@4.0.0:
|
/path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -3754,7 +3875,6 @@ packages:
|
|||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/punycode@2.3.0:
|
/punycode@2.3.0:
|
||||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||||
@@ -3790,6 +3910,10 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-fast-compare@3.2.2:
|
||||||
|
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-hook-form@7.45.0(react@18.2.0):
|
/react-hook-form@7.45.0(react@18.2.0):
|
||||||
resolution: {integrity: sha512-AbHeZ4ad+0dEIknSW9dBgIwcvRDfZ1O97sgj75WaMdOX0eg8TBiUf9wxzVkIjZbk76BBIE9lmFOzyD4PN80ZQg==}
|
resolution: {integrity: sha512-AbHeZ4ad+0dEIknSW9dBgIwcvRDfZ1O97sgj75WaMdOX0eg8TBiUf9wxzVkIjZbk76BBIE9lmFOzyD4PN80ZQg==}
|
||||||
engines: {node: '>=12.22.0'}
|
engines: {node: '>=12.22.0'}
|
||||||
@@ -3811,12 +3935,24 @@ packages:
|
|||||||
|
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/react-is@18.2.0:
|
/react-is@18.2.0:
|
||||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-player@2.12.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-rymLRz/2GJJD+Wc01S7S+i9pGMFYnNmQibR2gVE3KmHJCBNN8BhPAlOPTGZtn1uKpJ6p4RPLlzPQ1OLreXd8gw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.6.0'
|
||||||
|
dependencies:
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
load-script: 1.0.0
|
||||||
|
memoize-one: 5.2.1
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-fast-compare: 3.2.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-resizable-panels@0.0.48(react-dom@18.2.0)(react@18.2.0):
|
/react-resizable-panels@0.0.48(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-fJa3itmJ3HLLmVG7y8tka80wFW63N6ai76q7MGwU8nSXeA0qkX36vnmPyXm34lvtsGjn1Cgi5IPhPQnf42SVpA==}
|
resolution: {integrity: sha512-fJa3itmJ3HLLmVG7y8tka80wFW63N6ai76q7MGwU8nSXeA0qkX36vnmPyXm34lvtsGjn1Cgi5IPhPQnf42SVpA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3981,8 +4117,8 @@ packages:
|
|||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/rollup@3.25.1:
|
/rollup@3.25.2:
|
||||||
resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==}
|
resolution: {integrity: sha512-VLnkxZMDr3jpxgtmS8pQZ0UvhslmF4ADq/9w4erkctbgjCqLW9oa89fJuXEs4ZmgyoF7Dm8rMDKSS5b5u2hHUg==}
|
||||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -4753,7 +4889,7 @@ packages:
|
|||||||
'@types/node': 18.16.18
|
'@types/node': 18.16.18
|
||||||
esbuild: 0.17.19
|
esbuild: 0.17.19
|
||||||
postcss: 8.4.24
|
postcss: 8.4.24
|
||||||
rollup: 3.25.1
|
rollup: 3.25.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
@@ -22,11 +22,7 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"all": true,
|
"all": true,
|
||||||
"request": true,
|
"request": true,
|
||||||
"scope": [
|
"scope": ["http://**", "https://**"]
|
||||||
"https://void.cat/*",
|
|
||||||
"https://skrape.dev/*",
|
|
||||||
"https://lume.nu/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"fs": {
|
"fs": {
|
||||||
"all": false,
|
"all": false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createAccount } from "@libs/storage";
|
import { createAccount, createBlock } from "@libs/storage";
|
||||||
import { Button } from "@shared/button";
|
import { Button } from "@shared/button";
|
||||||
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -30,6 +30,11 @@ export function CreateStep1Screen() {
|
|||||||
mutationFn: (data: any) =>
|
mutationFn: (data: any) =>
|
||||||
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
createBlock(
|
||||||
|
0,
|
||||||
|
"Preserve your freedom",
|
||||||
|
"https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv",
|
||||||
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
navigate("/auth/create/step-2", { replace: true });
|
navigate("/auth/create/step-2", { replace: true });
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import { useOnboarding } from "@stores/onboarding";
|
|||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
import { Body, fetch } from "@tauri-apps/api/http";
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function CreateStep3Screen() {
|
export function CreateStep3Screen() {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const profile = useOnboarding((state: any) => state.profile);
|
const profile = useOnboarding((state: any) => state.profile);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -48,6 +51,7 @@ export function CreateStep3Screen() {
|
|||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// redirect to step 4
|
// redirect to step 4
|
||||||
|
navigate("/auth/create/step-4", { replace: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -131,8 +131,6 @@ export function CreateStep4Screen() {
|
|||||||
updateAccount("follows", follows, account.pubkey),
|
updateAccount("follows", follows, account.pubkey),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||||
// redirect to next step
|
|
||||||
navigate("/auth/onboarding", { replace: true });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -156,6 +154,9 @@ export function CreateStep4Screen() {
|
|||||||
|
|
||||||
// update
|
// update
|
||||||
update.mutate(follows);
|
update.mutate(follows);
|
||||||
|
|
||||||
|
// redirect to next step
|
||||||
|
setTimeout(() => navigate("/auth/onboarding", { replace: true }), 1200);
|
||||||
} catch {
|
} catch {
|
||||||
console.log("error");
|
console.log("error");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createAccount } from "@libs/storage";
|
import { createAccount, createBlock } from "@libs/storage";
|
||||||
import { LoaderIcon } from "@shared/icons";
|
import { LoaderIcon } from "@shared/icons";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { getPublicKey, nip19 } from "nostr-tools";
|
import { getPublicKey, nip19 } from "nostr-tools";
|
||||||
@@ -31,6 +31,11 @@ export function ImportStep1Screen() {
|
|||||||
mutationFn: (data: any) =>
|
mutationFn: (data: any) =>
|
||||||
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
createBlock(
|
||||||
|
0,
|
||||||
|
"Preserve your freedom",
|
||||||
|
"https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv",
|
||||||
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||||
// redirect to next step
|
// redirect to next step
|
||||||
navigate("/auth/import/step-2", { replace: true });
|
navigate("/auth/import/step-2", { replace: true });
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
|
import { LoaderIcon } from "@shared/icons";
|
||||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { useContext } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function OnboardingScreen() {
|
export function OnboardingScreen() {
|
||||||
@@ -12,9 +13,12 @@ export function OnboardingScreen() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { status, account } = useAccount();
|
const { status, account } = useAccount();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const publish = async () => {
|
const publish = async () => {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||||
ndk.signer = signer;
|
ndk.signer = signer;
|
||||||
@@ -30,7 +34,7 @@ export function OnboardingScreen() {
|
|||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// redirect to home
|
// redirect to home
|
||||||
navigate("/", { replace: true });
|
setTimeout(() => navigate("/", { replace: true }), 1200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
@@ -80,9 +84,15 @@ export function OnboardingScreen() {
|
|||||||
onClick={() => publish()}
|
onClick={() => publish()}
|
||||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
||||||
>
|
>
|
||||||
<span className="w-5" />
|
{loading ? (
|
||||||
<span>Publish</span>
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="w-5" />
|
||||||
|
<span>Publish</span>
|
||||||
|
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function ChannelCreateModal() {
|
|||||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex h-full w-full flex-col gap-4"
|
className="flex h-full w-full flex-col gap-4 mb-0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={"hidden"}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function ChannelsListItem({ data }: { data: any }) {
|
|||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/app/channel/${data.event_id}`}
|
to={`/app/channel/${data.event_id}`}
|
||||||
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|||||||
import { CancelIcon, HideIcon } from "@shared/icons";
|
import { CancelIcon, HideIcon } from "@shared/icons";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { Tooltip } from "@shared/tooltip";
|
import { Tooltip } from "@shared/tooltip";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { useChannelMessages } from "@stores/channels";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { Fragment, useContext, useState } from "react";
|
import { Fragment, useContext, useState } from "react";
|
||||||
|
|
||||||
export function MessageHideButton({ id }: { id: string }) {
|
export function MessageHideButton({ id }: { id: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
const hide = useChannelMessages((state: any) => state.hideMessage);
|
const hide = useChannelMessages((state: any) => state.hideMessage);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
|||||||
return (
|
return (
|
||||||
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<User pubkey={data.pubkey} time={data.created_at} isChat={true} />
|
||||||
<div className="-mt-[20px] pl-[49px]">
|
<div className="-mt-[20px] pl-[49px]">
|
||||||
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||||
{content.parsed}
|
{content.parsed}
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|||||||
import { CancelIcon, MuteIcon } from "@shared/icons";
|
import { CancelIcon, MuteIcon } from "@shared/icons";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { Tooltip } from "@shared/tooltip";
|
import { Tooltip } from "@shared/tooltip";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useChannelMessages } from "@stores/channels";
|
import { useChannelMessages } from "@stores/channels";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { Fragment, useContext, useState } from "react";
|
import { Fragment, useContext, useState } from "react";
|
||||||
|
|
||||||
export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
const mute = useChannelMessages((state: any) => state.muteUser);
|
const mute = useChannelMessages((state: any) => state.muteUser);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,29 +109,31 @@ export function ChannelScreen() {
|
|||||||
>
|
>
|
||||||
<h3 className="font-semibold text-zinc-100">Public Channel</h3>
|
<h3 className="font-semibold text-zinc-100">Public Channel</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-1 p-3">
|
<div className="w-full h-full flex-1 p-3">
|
||||||
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
|
<div className="h-full flex flex-col justify-between rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
||||||
{!messages ? (
|
<div className="flex-1 w-full h-full">
|
||||||
<p>Loading...</p>
|
{!messages ? (
|
||||||
) : (
|
<p>Loading...</p>
|
||||||
<Virtuoso
|
) : (
|
||||||
ref={virtuosoRef}
|
<Virtuoso
|
||||||
data={messages}
|
ref={virtuosoRef}
|
||||||
itemContent={itemContent}
|
data={messages}
|
||||||
computeItemKey={computeItemKey}
|
itemContent={itemContent}
|
||||||
initialTopMostItemIndex={messages.length - 1}
|
computeItemKey={computeItemKey}
|
||||||
alignToBottom={true}
|
initialTopMostItemIndex={messages.length - 1}
|
||||||
followOutput={true}
|
alignToBottom={true}
|
||||||
overscan={50}
|
followOutput={true}
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
overscan={50}
|
||||||
className="scrollbar-hide overflow-y-auto h-full w-full"
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||||
components={{
|
className="scrollbar-hide overflow-y-auto"
|
||||||
Header: () => Header,
|
components={{
|
||||||
EmptyPlaceholder: () => Empty,
|
Header: () => Header,
|
||||||
}}
|
EmptyPlaceholder: () => Empty,
|
||||||
/>
|
}}
|
||||||
)}
|
/>
|
||||||
<div className="w-full inline-flex shrink-0 px-5 py-3 border-t border-zinc-800">
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 px-5 p-3 rounded-b-xl border-t border-zinc-800 bg-zinc-900 z-50">
|
||||||
<ChannelMessageForm channelID={id} />
|
<ChannelMessageForm channelID={id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function ChatsListItem({ data }: { data: any }) {
|
|||||||
) : (
|
) : (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/app/chat/${data.sender_pubkey}`}
|
to={`/app/chat/${data.sender_pubkey}`}
|
||||||
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ export function ChatMessageItem({
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<User pubkey={data.sender_pubkey} time={data.created_at} />
|
<User
|
||||||
|
pubkey={data.sender_pubkey}
|
||||||
|
time={data.created_at}
|
||||||
|
isChat={true}
|
||||||
|
/>
|
||||||
<div className="-mt-[20px] pl-[49px]">
|
<div className="-mt-[20px] pl-[49px]">
|
||||||
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||||
{content.parsed}
|
{content.parsed}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function NewMessageModal() {
|
|||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[500px] flex flex-col pb-5 overflow-y-auto">
|
<div className="h-[500px] flex flex-col pb-5 overflow-x-hidden overflow-y-auto">
|
||||||
{status === "loading" || isFetching ? (
|
{status === "loading" || isFetching ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function ChatsListSelfItem({ data }: { data: any }) {
|
|||||||
) : (
|
) : (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/app/chat/${data.pubkey}`}
|
to={`/app/chat/${data.pubkey}`}
|
||||||
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
||||||
|
|||||||
@@ -52,39 +52,37 @@ export function ChatScreen() {
|
|||||||
>
|
>
|
||||||
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
|
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-1 p-3">
|
<div className="w-full h-full flex-1 p-3">
|
||||||
{account && (
|
<div className="h-full flex flex-col justify-between rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
||||||
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
|
<div className="flex-1 w-full h-full">
|
||||||
{status === "loading" ? (
|
{status === "loading" ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full">
|
<Virtuoso
|
||||||
<Virtuoso
|
ref={virtuosoRef}
|
||||||
ref={virtuosoRef}
|
data={data}
|
||||||
data={data}
|
itemContent={itemContent}
|
||||||
itemContent={itemContent}
|
computeItemKey={computeItemKey}
|
||||||
computeItemKey={computeItemKey}
|
initialTopMostItemIndex={data.length - 1}
|
||||||
initialTopMostItemIndex={data.length - 1}
|
alignToBottom={true}
|
||||||
alignToBottom={true}
|
followOutput={true}
|
||||||
followOutput={true}
|
overscan={50}
|
||||||
overscan={50}
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
className="relative scrollbar-hide overflow-y-auto"
|
||||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
components={{
|
||||||
components={{
|
EmptyPlaceholder: () => Empty,
|
||||||
EmptyPlaceholder: () => Empty,
|
}}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="shrink-0 px-5 p-3 border-t border-zinc-800">
|
|
||||||
<ChatMessageForm
|
|
||||||
receiverPubkey={pubkey}
|
|
||||||
userPubkey={account.pubkey}
|
|
||||||
userPrivkey={account.privkey}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="shrink-0 px-5 p-3 rounded-b-xl border-t border-zinc-800 bg-zinc-900 z-50">
|
||||||
|
<ChatMessageForm
|
||||||
|
receiverPubkey={pubkey}
|
||||||
|
userPubkey={account.pubkey}
|
||||||
|
userPrivkey={account.privkey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
@@ -92,18 +90,16 @@ export function ChatScreen() {
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
|
||||||
/>
|
/>
|
||||||
{pubkey && <ChatSidebar pubkey={pubkey} />}
|
<ChatSidebar pubkey={pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Empty = (
|
const Empty = (
|
||||||
<div className="flex flex-col gap-1 text-center">
|
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full flex flex-col gap-1 text-center">
|
||||||
<h3 className="text-base font-semibold leading-none text-white">
|
<h3 className="mb-2 text-4xl">🙌</h3>
|
||||||
Nothing to see here yet
|
<p className="leading-none text-zinc-400">
|
||||||
</h3>
|
|
||||||
<p className="text-base leading-none text-zinc-400">
|
|
||||||
You two didn't talk yet, let's send first message
|
You two didn't talk yet, let's send first message
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
getLastLogin,
|
getLastLogin,
|
||||||
} from "@libs/storage";
|
} from "@libs/storage";
|
||||||
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
||||||
import { LumeIcon } from "@shared/icons";
|
import { LoaderIcon, LumeIcon } from "@shared/icons";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
import { dateToUnix, getHourAgo } from "@utils/date";
|
||||||
import { useAccount } from "@utils/hooks/useAccount";
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
@@ -177,27 +177,7 @@ export function Root() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-16 left-1/2 -translate-x-1/2 transform">
|
<div className="absolute bottom-16 left-1/2 -translate-x-1/2 transform">
|
||||||
<svg
|
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||||
className="h-5 w-5 animate-spin text-black dark:text-zinc-100"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<title id="loading">Loading</title>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { createBlock } from "@libs/storage";
|
||||||
import { CancelIcon } from "@shared/icons";
|
import { CancelIcon } from "@shared/icons";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
export function AddFeedBlock({ parentState }: { parentState: any }) {
|
export function AddFeedBlock({ parentState }: { parentState: any }) {
|
||||||
const addBlock = useActiveAccount((state: any) => state.addBlock);
|
const queryClient = useQueryClient();
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
@@ -18,6 +21,13 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
|||||||
parentState(false);
|
parentState(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const block = useMutation({
|
||||||
|
mutationFn: (data: any) => createBlock(data.kind, data.title, data.content),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -35,7 +45,7 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// insert to database
|
// insert to database
|
||||||
addBlock(1, data.title, pubkey);
|
block.mutate({ kind: 1, title: data.title, content: pubkey });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -43,7 +53,7 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
|||||||
reset();
|
reset();
|
||||||
// close modal
|
// close modal
|
||||||
closeModal();
|
closeModal();
|
||||||
}, 1000);
|
}, 1200);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,7 +80,7 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900">
|
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
|
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -102,7 +112,7 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
|||||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex h-full w-full flex-col gap-4"
|
className="flex h-full w-full flex-col gap-4 mb-0"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { createBlock } from "@libs/storage";
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
import { CancelIcon } from "@shared/icons";
|
import { CancelIcon } from "@shared/icons";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { open } from "@tauri-apps/api/dialog";
|
import { open } from "@tauri-apps/api/dialog";
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
import { Body, fetch } from "@tauri-apps/api/http";
|
||||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
export function AddImageBlock({ parentState }: { parentState: any }) {
|
export function AddImageBlock({ parentState }: { parentState: any }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [account, addBlock] = useActiveAccount((state: any) => [
|
|
||||||
state.account,
|
|
||||||
state.addBlock,
|
|
||||||
]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
const [image, setImage] = useState("");
|
const [image, setImage] = useState("");
|
||||||
|
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
const tags = useRef(null);
|
const tags = useRef(null);
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
@@ -88,6 +89,13 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const block = useMutation({
|
||||||
|
mutationFn: (data: any) => createBlock(data.kind, data.title, data.content),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -105,8 +113,8 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
|||||||
// publish event
|
// publish event
|
||||||
event.publish();
|
event.publish();
|
||||||
|
|
||||||
// insert to database
|
// mutate
|
||||||
addBlock(0, data.title, data.content);
|
block.mutate({ kind: 0, title: data.title, content: data.content });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -114,7 +122,7 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
|||||||
reset();
|
reset();
|
||||||
// close modal
|
// close modal
|
||||||
closeModal();
|
closeModal();
|
||||||
}, 1000);
|
}, 1200);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -145,7 +153,7 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900">
|
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
|
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -177,7 +185,7 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
|||||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex h-full w-full flex-col gap-4"
|
className="flex h-full w-full flex-col gap-4 mb-0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type={"hidden"}
|
type={"hidden"}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { getNotesByAuthor } from "@libs/storage";
|
import { getNotesByAuthor, removeBlock } from "@libs/storage";
|
||||||
import { Note } from "@shared/notes/note";
|
import { Note } from "@shared/notes/note";
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import {
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
useInfiniteQuery,
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
@@ -11,12 +14,7 @@ const ITEM_PER_PAGE = 10;
|
|||||||
const TIME = Math.floor(Date.now() / 1000);
|
const TIME = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
export function FeedBlock({ params }: { params: any }) {
|
export function FeedBlock({ params }: { params: any }) {
|
||||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
removeBlock(params.id, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
data,
|
data,
|
||||||
@@ -65,6 +63,13 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
}
|
}
|
||||||
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||||
|
|
||||||
|
const block = useMutation({
|
||||||
|
mutationFn: (id: string) => removeBlock(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const renderItem = (index: string | number) => {
|
const renderItem = (index: string | number) => {
|
||||||
const note = notes[index];
|
const note = notes[index];
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ export function FeedBlock({ params }: { params: any }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
||||||
<TitleBar title={params.title} onClick={() => close()} />
|
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Note } from "@shared/notes/note";
|
|||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
@@ -63,17 +62,16 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let sub: NDKSubscription;
|
const follows = account ? JSON.parse(account.follows) : [];
|
||||||
|
|
||||||
if (account) {
|
const filter: NDKFilter = {
|
||||||
const follows = JSON.parse(account.follows);
|
kinds: [1, 6],
|
||||||
const filter: NDKFilter = {
|
authors: follows,
|
||||||
kinds: [1, 6],
|
since: dateToUnix(),
|
||||||
authors: follows,
|
};
|
||||||
since: dateToUnix(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sub = ndk.subscribe(filter);
|
const sub = account ? ndk.subscribe(filter) : null;
|
||||||
|
if (sub) {
|
||||||
sub.addListener("event", (event: NDKEvent) => {
|
sub.addListener("event", (event: NDKEvent) => {
|
||||||
createNote(
|
createNote(
|
||||||
event.id,
|
event.id,
|
||||||
@@ -87,7 +85,9 @@ export function FollowingBlock({ block }: { block: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sub.stop();
|
if (sub) {
|
||||||
|
sub.stop();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [account]);
|
}, [account]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
|
import { removeBlock } from "@libs/storage";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export function ImageBlock({ params }: { params: any }) {
|
export function ImageBlock({ params }: { params: any }) {
|
||||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const close = () => {
|
const block = useMutation({
|
||||||
removeBlock(params.id, true);
|
mutationFn: (id: string) => removeBlock(id),
|
||||||
};
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
|
<div className="shrink-0 w-[350px] h-full flex flex-col justify-between border-r border-zinc-900">
|
||||||
<TitleBar title={params.title} onClick={() => close()} />
|
<div className="flex-1 w-full h-full overflow-hidden p-3">
|
||||||
<div className="w-full flex-1 p-3">
|
<div className="w-full h-full">
|
||||||
<Image
|
<Image
|
||||||
src={params.content}
|
src={params.content}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt={params.title}
|
alt={params.title}
|
||||||
className="w-full h-full object-cover rounded-md"
|
className="w-full h-full object-cover rounded-xl border-t border-zinc-800/50"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getNoteByID } from "@libs/storage";
|
import { getNoteByID, removeBlock } from "@libs/storage";
|
||||||
import { Kind1 } from "@shared/notes/contents/kind1";
|
import { Kind1 } from "@shared/notes/contents/kind1";
|
||||||
import { Kind1063 } from "@shared/notes/contents/kind1063";
|
import { Kind1063 } from "@shared/notes/contents/kind1063";
|
||||||
import { NoteMetadata } from "@shared/notes/metadata";
|
import { NoteMetadata } from "@shared/notes/metadata";
|
||||||
@@ -7,11 +7,12 @@ import { RepliesList } from "@shared/notes/replies/list";
|
|||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { TitleBar } from "@shared/titleBar";
|
import { TitleBar } from "@shared/titleBar";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { parser } from "@utils/parser";
|
import { parser } from "@utils/parser";
|
||||||
|
|
||||||
export function ThreadBlock({ params }: { params: any }) {
|
export function ThreadBlock({ params }: { params: any }) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { status, data, isFetching } = useQuery(
|
const { status, data, isFetching } = useQuery(
|
||||||
["thread", params.content],
|
["thread", params.content],
|
||||||
async () => {
|
async () => {
|
||||||
@@ -19,16 +20,18 @@ export function ThreadBlock({ params }: { params: any }) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = data ? parser(data) : null;
|
const block = useMutation({
|
||||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
mutationFn: (id: string) => removeBlock(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const close = () => {
|
const content = data ? parser(data) : null;
|
||||||
removeBlock(params.id, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
<div className="shrink-0 w-[400px] border-r border-zinc-900">
|
||||||
<TitleBar title={params.title} onClick={() => close()} />
|
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
|
||||||
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto">
|
||||||
{status === "loading" || isFetching ? (
|
{status === "loading" || isFetching ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
|
|||||||
@@ -4,15 +4,41 @@ import { FollowingBlock } from "@app/space/components/blocks/following";
|
|||||||
import { ImageBlock } from "@app/space/components/blocks/image";
|
import { ImageBlock } from "@app/space/components/blocks/image";
|
||||||
import { ThreadBlock } from "@app/space/components/blocks/thread";
|
import { ThreadBlock } from "@app/space/components/blocks/thread";
|
||||||
import { getBlocks } from "@libs/storage";
|
import { getBlocks } from "@libs/storage";
|
||||||
|
import { LoaderIcon } from "@shared/icons";
|
||||||
const blocks = await getBlocks();
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export function SpaceScreen() {
|
export function SpaceScreen() {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
data: blocks,
|
||||||
|
isFetching,
|
||||||
|
} = useQuery(
|
||||||
|
["blocks"],
|
||||||
|
async () => {
|
||||||
|
return await getBlocks();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
staleTime: Infinity,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
||||||
<FollowingBlock block={1} />
|
<FollowingBlock block={1} />
|
||||||
{!blocks ? (
|
{status === "loading" ? (
|
||||||
<p>Loading...</p>
|
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex-1 flex items-center justify-center p-3">
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
blocks.map((block: any) => {
|
blocks.map((block: any) => {
|
||||||
switch (block.kind) {
|
switch (block.kind) {
|
||||||
@@ -27,6 +53,18 @@ export function SpaceScreen() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
{isFetching && (
|
||||||
|
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex-1 flex items-center justify-center p-3">
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="shrink-0 w-[90px]">
|
<div className="shrink-0 w-[90px]">
|
||||||
<div className="w-full h-full inline-flex items-center justify-center">
|
<div className="w-full h-full inline-flex items-center justify-center">
|
||||||
<AddBlock />
|
<AddBlock />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
import { usePageContext } from "@utils/hooks/usePageContext";
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { compactNumber } from "@utils/number";
|
import { compactNumber } from "@utils/number";
|
||||||
@@ -18,6 +18,7 @@ export function UserScreen() {
|
|||||||
const searchParams: any = pageContext.urlParsed.search;
|
const searchParams: any = pageContext.urlParsed.search;
|
||||||
const pubkey = searchParams.pubkey || "";
|
const pubkey = searchParams.pubkey || "";
|
||||||
|
|
||||||
|
const { account } = useAccount();
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
const { data: userStats, error } = useQuery(["user", pubkey], async () => {
|
const { data: userStats, error } = useQuery(["user", pubkey], async () => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -28,7 +29,6 @@ export function UserScreen() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
const follows = account ? JSON.parse(account.follows) : [];
|
const follows = account ? JSON.parse(account.follows) : [];
|
||||||
|
|
||||||
const follow = (pubkey: string) => {
|
const follow = (pubkey: string) => {
|
||||||
|
|||||||
360
src/libs/openGraph.tsx
Normal file
360
src/libs/openGraph.tsx
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
import { OPENGRAPH } from "@stores/constants";
|
||||||
|
import { FetchOptions, ResponseType, fetch } from "@tauri-apps/api/http";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
interface ILinkPreviewOptions {
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
imagesPropertyType?: string;
|
||||||
|
proxyUrl?: string;
|
||||||
|
timeout?: number;
|
||||||
|
followRedirects?: `follow` | `error` | `manual`;
|
||||||
|
resolveDNSHost?: (url: string) => Promise<string>;
|
||||||
|
handleRedirects?: (baseURL: string, forwardedURL: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPreFetchedResource {
|
||||||
|
headers: Record<string, string>;
|
||||||
|
status?: number;
|
||||||
|
imagesPropertyType?: string;
|
||||||
|
proxyUrl?: string;
|
||||||
|
url: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaTag(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
||||||
|
const nodes = doc(`meta[${attr}='${type}']`);
|
||||||
|
return nodes.length ? nodes : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaTagContent(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
||||||
|
return doc(`meta[${attr}='${type}']`).attr("content");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTitle(doc: cheerio.CheerioAPI) {
|
||||||
|
let title =
|
||||||
|
metaTagContent(doc, "og:title", "property") ||
|
||||||
|
metaTagContent(doc, "og:title", "name");
|
||||||
|
if (!title) {
|
||||||
|
title = doc("title").text();
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSiteName(doc: cheerio.CheerioAPI) {
|
||||||
|
const siteName =
|
||||||
|
metaTagContent(doc, "og:site_name", "property") ||
|
||||||
|
metaTagContent(doc, "og:site_name", "name");
|
||||||
|
return siteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDescription(doc: cheerio.CheerioAPI) {
|
||||||
|
const description =
|
||||||
|
metaTagContent(doc, "description", "name") ||
|
||||||
|
metaTagContent(doc, "Description", "name") ||
|
||||||
|
metaTagContent(doc, "og:description", "property");
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMediaType(doc: cheerio.CheerioAPI) {
|
||||||
|
const node = metaTag(doc, "medium", "name");
|
||||||
|
if (node) {
|
||||||
|
const content = node.attr("content");
|
||||||
|
return content === "image" ? "photo" : content;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
metaTagContent(doc, "og:type", "property") ||
|
||||||
|
metaTagContent(doc, "og:type", "name")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImages(
|
||||||
|
doc: cheerio.CheerioAPI,
|
||||||
|
rootUrl: string,
|
||||||
|
imagesPropertyType?: string,
|
||||||
|
) {
|
||||||
|
let images: string[] = [];
|
||||||
|
let nodes: cheerio.Cheerio<cheerio.Element> | null;
|
||||||
|
let src: string | undefined;
|
||||||
|
let dic: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
const imagePropertyType = imagesPropertyType ?? "og";
|
||||||
|
nodes =
|
||||||
|
metaTag(doc, `${imagePropertyType}:image`, "property") ||
|
||||||
|
metaTag(doc, `${imagePropertyType}:image`, "name");
|
||||||
|
|
||||||
|
if (nodes) {
|
||||||
|
nodes.each((_: number, node: cheerio.Element) => {
|
||||||
|
if (node.type === "tag") {
|
||||||
|
src = node.attribs.content;
|
||||||
|
if (src) {
|
||||||
|
src = new URL(src, rootUrl).href;
|
||||||
|
images.push(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (images.length <= 0 && !imagesPropertyType) {
|
||||||
|
src = doc("link[rel=image_src]").attr("href");
|
||||||
|
if (src) {
|
||||||
|
src = new URL(src, rootUrl).href;
|
||||||
|
images = [src];
|
||||||
|
} else {
|
||||||
|
nodes = doc("img");
|
||||||
|
|
||||||
|
if (nodes?.length) {
|
||||||
|
dic = {};
|
||||||
|
images = [];
|
||||||
|
nodes.each((_: number, node: cheerio.Element) => {
|
||||||
|
if (node.type === "tag") src = node.attribs.src;
|
||||||
|
if (src && !dic[src]) {
|
||||||
|
dic[src] = true;
|
||||||
|
// width = node.attribs.width;
|
||||||
|
// height = node.attribs.height;
|
||||||
|
images.push(new URL(src, rootUrl).href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideos(doc: cheerio.CheerioAPI) {
|
||||||
|
const videos = [];
|
||||||
|
let nodeTypes;
|
||||||
|
let nodeSecureUrls;
|
||||||
|
let nodeType;
|
||||||
|
let nodeSecureUrl;
|
||||||
|
let video;
|
||||||
|
let videoType;
|
||||||
|
let videoSecureUrl;
|
||||||
|
let width;
|
||||||
|
let height;
|
||||||
|
let videoObj;
|
||||||
|
let index;
|
||||||
|
|
||||||
|
const nodes =
|
||||||
|
metaTag(doc, "og:video", "property") || metaTag(doc, "og:video", "name");
|
||||||
|
|
||||||
|
if (nodes?.length) {
|
||||||
|
nodeTypes =
|
||||||
|
metaTag(doc, "og:video:type", "property") ||
|
||||||
|
metaTag(doc, "og:video:type", "name");
|
||||||
|
nodeSecureUrls =
|
||||||
|
metaTag(doc, "og:video:secure_url", "property") ||
|
||||||
|
metaTag(doc, "og:video:secure_url", "name");
|
||||||
|
width =
|
||||||
|
metaTagContent(doc, "og:video:width", "property") ||
|
||||||
|
metaTagContent(doc, "og:video:width", "name");
|
||||||
|
height =
|
||||||
|
metaTagContent(doc, "og:video:height", "property") ||
|
||||||
|
metaTagContent(doc, "og:video:height", "name");
|
||||||
|
|
||||||
|
for (index = 0; index < nodes.length; index += 1) {
|
||||||
|
const node = nodes[index];
|
||||||
|
if (node.type === "tag") video = node.attribs.content;
|
||||||
|
|
||||||
|
nodeType = nodeTypes?.[index];
|
||||||
|
if (nodeType?.type === "tag") {
|
||||||
|
videoType = nodeType ? nodeType.attribs.content : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeSecureUrl = nodeSecureUrls?.[index];
|
||||||
|
if (nodeSecureUrl?.type === "tag") {
|
||||||
|
videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoObj = {
|
||||||
|
url: video,
|
||||||
|
secureUrl: videoSecureUrl,
|
||||||
|
type: videoType,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
if (videoType && videoType.indexOf("video/") === 0) {
|
||||||
|
videos.splice(0, 0, videoObj);
|
||||||
|
} else {
|
||||||
|
videos.push(videoObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns default favicon (//hostname/favicon.ico) for a url
|
||||||
|
function getDefaultFavicon(rootUrl: string) {
|
||||||
|
return `${new URL(rootUrl).origin}/favicon.ico`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns an array of URLs to favicon images
|
||||||
|
function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
|
||||||
|
const images = [];
|
||||||
|
let nodes: cheerio.Cheerio<cheerio.Element> | never[] = [];
|
||||||
|
let src: string | undefined;
|
||||||
|
|
||||||
|
const relSelectors = [
|
||||||
|
"rel=icon",
|
||||||
|
`rel="shortcut icon"`,
|
||||||
|
"rel=apple-touch-icon",
|
||||||
|
];
|
||||||
|
|
||||||
|
relSelectors.forEach((relSelector) => {
|
||||||
|
// look for all icon tags
|
||||||
|
nodes = doc(`link[${relSelector}]`);
|
||||||
|
|
||||||
|
// collect all images from icon tags
|
||||||
|
if (nodes.length) {
|
||||||
|
nodes.each((_: number, node: cheerio.Element) => {
|
||||||
|
if (node.type === "tag") src = node.attribs.href;
|
||||||
|
if (src) {
|
||||||
|
src = new URL(rootUrl).href;
|
||||||
|
images.push(src);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if no icon images, use default favicon location
|
||||||
|
if (images.length <= 0) {
|
||||||
|
images.push(getDefaultFavicon(rootUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImageResponse(url: string, contentType: string) {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
mediaType: "image",
|
||||||
|
contentType,
|
||||||
|
favicons: [getDefaultFavicon(url)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAudioResponse(url: string, contentType: string) {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
mediaType: "audio",
|
||||||
|
contentType,
|
||||||
|
favicons: [getDefaultFavicon(url)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVideoResponse(url: string, contentType: string) {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
mediaType: "video",
|
||||||
|
contentType,
|
||||||
|
favicons: [getDefaultFavicon(url)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseApplicationResponse(url: string, contentType: string) {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
mediaType: "application",
|
||||||
|
contentType,
|
||||||
|
favicons: [getDefaultFavicon(url)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTextResponse(
|
||||||
|
body: string,
|
||||||
|
url: string,
|
||||||
|
options: ILinkPreviewOptions = {},
|
||||||
|
contentType?: string,
|
||||||
|
) {
|
||||||
|
const doc = cheerio.load(body);
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
title: getTitle(doc),
|
||||||
|
siteName: getSiteName(doc),
|
||||||
|
description: getDescription(doc),
|
||||||
|
mediaType: getMediaType(doc) || "website",
|
||||||
|
contentType,
|
||||||
|
images: getImages(doc, url, options.imagesPropertyType),
|
||||||
|
videos: getVideos(doc),
|
||||||
|
favicons: getFavicons(doc, url),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUnknownResponse(
|
||||||
|
body: string,
|
||||||
|
url: string,
|
||||||
|
options: ILinkPreviewOptions = {},
|
||||||
|
contentType?: string,
|
||||||
|
) {
|
||||||
|
return parseTextResponse(body, url, options, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResponse(
|
||||||
|
response: IPreFetchedResource,
|
||||||
|
options?: ILinkPreviewOptions,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
let contentType = response.headers["content-type"];
|
||||||
|
// console.warn(`original content type`, contentType);
|
||||||
|
if (contentType?.indexOf(";")) {
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
contentType = contentType.split(";")[0];
|
||||||
|
// console.warn(`splitting content type`, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contentType) {
|
||||||
|
return parseUnknownResponse(response.data, response.url, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((contentType as any) instanceof Array) {
|
||||||
|
// eslint-disable-next-line no-param-reassign, prefer-destructuring
|
||||||
|
contentType = contentType[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse response depending on content type
|
||||||
|
if (OPENGRAPH.REGEX_CONTENT_TYPE_IMAGE.test(contentType)) {
|
||||||
|
return parseImageResponse(response.url, contentType);
|
||||||
|
}
|
||||||
|
if (OPENGRAPH.REGEX_CONTENT_TYPE_AUDIO.test(contentType)) {
|
||||||
|
return parseAudioResponse(response.url, contentType);
|
||||||
|
}
|
||||||
|
if (OPENGRAPH.REGEX_CONTENT_TYPE_VIDEO.test(contentType)) {
|
||||||
|
return parseVideoResponse(response.url, contentType);
|
||||||
|
}
|
||||||
|
if (OPENGRAPH.REGEX_CONTENT_TYPE_TEXT.test(contentType)) {
|
||||||
|
const htmlString = response.data;
|
||||||
|
return parseTextResponse(htmlString, response.url, options, contentType);
|
||||||
|
}
|
||||||
|
if (OPENGRAPH.REGEX_CONTENT_TYPE_APPLICATION.test(contentType)) {
|
||||||
|
return parseApplicationResponse(response.url, contentType);
|
||||||
|
}
|
||||||
|
const htmlString = response.data;
|
||||||
|
return parseUnknownResponse(htmlString, response.url, options);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`link-preview-js could not fetch link information ${(
|
||||||
|
e as any
|
||||||
|
).toString()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLinkPreview(text: string) {
|
||||||
|
const fetchUrl = text;
|
||||||
|
const options: FetchOptions = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 30,
|
||||||
|
responseType: ResponseType.Text,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = await fetch(fetchUrl, options);
|
||||||
|
|
||||||
|
if (response.status > 300 && response.status < 309) {
|
||||||
|
const forwardedUrl = response.headers.location || "";
|
||||||
|
response = await fetch(forwardedUrl, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResponse(response);
|
||||||
|
}
|
||||||
@@ -414,20 +414,16 @@ export async function getBlocks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create block
|
// create block
|
||||||
export async function addBlockToDB(
|
export async function createBlock(kind: number, title: string, content: any) {
|
||||||
account_id: number,
|
|
||||||
kind: number,
|
|
||||||
title: string,
|
|
||||||
content: any,
|
|
||||||
) {
|
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
|
const activeAccount = await getActiveAccount();
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
"INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);",
|
"INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);",
|
||||||
[account_id, kind, title, content],
|
[activeAccount.id, kind, title, content],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeBlockFromDB(id: string) {
|
export async function removeBlock(id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
return await db.execute(`DELETE FROM blocks WHERE id = "${id}";`);
|
return await db.execute(`DELETE FROM blocks WHERE id = "${id}";`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import {
|
|||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ComposeIcon,
|
ComposeIcon,
|
||||||
} from "@shared/icons";
|
} from "@shared/icons";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { useComposer } from "@stores/composer";
|
import { useComposer } from "@stores/composer";
|
||||||
import { COMPOSE_SHORTCUT } from "@stores/shortcuts";
|
import { COMPOSE_SHORTCUT } from "@stores/shortcuts";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
|
||||||
export function Composer() {
|
export function Composer() {
|
||||||
const account = useActiveAccount((state) => state.account);
|
const { account } = useAccount();
|
||||||
|
|
||||||
const [toggle, open] = useComposer((state: any) => [
|
const [toggle, open] = useComposer((state: any) => [
|
||||||
state.toggleModal,
|
state.toggleModal,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function Navigation({ reverse = false }: { reverse?: boolean }) {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/app/space"
|
to="/app/space"
|
||||||
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200",
|
"flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200",
|
||||||
@@ -44,6 +45,7 @@ export function Navigation({ reverse = false }: { reverse?: boolean }) {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/app/trending"
|
to="/app/trending"
|
||||||
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200",
|
"flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { createBlock } from "@libs/storage";
|
||||||
import { Kind1 } from "@shared/notes/contents/kind1";
|
import { Kind1 } from "@shared/notes/contents/kind1";
|
||||||
import { Kind1063 } from "@shared/notes/contents/kind1063";
|
import { Kind1063 } from "@shared/notes/contents/kind1063";
|
||||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||||
import { User } from "@shared/user";
|
import { User } from "@shared/user";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEvent } from "@utils/hooks/useEvent";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
|
||||||
@@ -11,8 +13,30 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
const kind1 = data?.kind === 1 ? data.content : null;
|
const kind1 = data?.kind === 1 ? data.content : null;
|
||||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const block = useMutation({
|
||||||
|
mutationFn: (data: any) => createBlock(data.kind, data.title, data.content),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const openThread = (event: any, thread: string) => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection.toString().length === 0) {
|
||||||
|
block.mutate({ kind: 2, title: "Thread", content: thread });
|
||||||
|
} else {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 rounded-lg border border-zinc-800 px-3 py-3">
|
<div
|
||||||
|
onClick={(e) => openThread(e, id)}
|
||||||
|
onKeyDown={(e) => openThread(e, id)}
|
||||||
|
className="mt-3 rounded-lg bg-zinc-800 border-t border-zinc-700/50 px-3 py-3"
|
||||||
|
>
|
||||||
{isFetching || status === "loading" ? (
|
{isFetching || status === "loading" ? (
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
|
import { createBlock } from "@libs/storage";
|
||||||
import { ReplyIcon } from "@shared/icons";
|
import { ReplyIcon } from "@shared/icons";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { compactNumber } from "@utils/number";
|
import { compactNumber } from "@utils/number";
|
||||||
|
|
||||||
export function NoteReply({
|
export function NoteReply({
|
||||||
id,
|
id,
|
||||||
replies,
|
replies,
|
||||||
currentBlock,
|
|
||||||
}: { id: string; replies: number; currentBlock?: number }) {
|
}: { id: string; replies: number; currentBlock?: number }) {
|
||||||
const addTempBlock = useActiveAccount((state: any) => state.addTempBlock);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const block = useMutation({
|
||||||
|
mutationFn: (data: any) => createBlock(data.kind, data.title, data.content),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const openThread = (event: any, thread: string) => {
|
const openThread = (event: any, thread: string) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
addTempBlock(currentBlock, 2, "Thread", thread);
|
block.mutate({ kind: 2, title: "Thread", content: thread });
|
||||||
} else {
|
} else {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,12 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { useOpenGraph } from "@utils/hooks/useOpenGraph";
|
import { useOpenGraph } from "@utils/hooks/useOpenGraph";
|
||||||
|
|
||||||
function isValidURL(string: string) {
|
|
||||||
let url: URL;
|
|
||||||
try {
|
|
||||||
url = new URL(string);
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LinkPreview({ urls }: { urls: string[] }) {
|
export function LinkPreview({ urls }: { urls: string[] }) {
|
||||||
const domain = new URL(urls[0]);
|
const domain = new URL(urls[0]);
|
||||||
const { status, data, error, isFetching } = useOpenGraph(urls[0]);
|
const { status, data, isFetching } = useOpenGraph(urls[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
|
<div className="mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
|
||||||
{error && <p>failed to load</p>}
|
|
||||||
{isFetching || status === "loading" ? (
|
{isFetching || status === "loading" ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="w-full h-44 bg-zinc-700 animate-pulse" />
|
<div className="w-full h-44 bg-zinc-700 animate-pulse" />
|
||||||
@@ -29,20 +18,6 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : !data ? (
|
|
||||||
<a
|
|
||||||
className="flex flex-col px-3 py-3 rounded-lg border border-transparent hover:border-fuchsia-900"
|
|
||||||
href={urls[0]}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<p className="leading-none text-sm text-zinc-400 line-clamp-3">
|
|
||||||
Can't fetch open graph, click to open website directly
|
|
||||||
</p>
|
|
||||||
<span className="mt-2.5 leading-none text-sm text-zinc-500">
|
|
||||||
{domain.hostname}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
className="flex flex-col rounded-lg border border-transparent hover:border-fuchsia-900"
|
className="flex flex-col rounded-lg border border-transparent hover:border-fuchsia-900"
|
||||||
@@ -50,31 +25,20 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{isValidURL(data["og:image"]) ? (
|
<Image
|
||||||
<Image
|
src={data.images[0]}
|
||||||
src={data["og:image"]}
|
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||||
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
alt={urls[0]}
|
||||||
alt={urls[0]}
|
className="w-full h-44 object-cover rounded-t-lg"
|
||||||
className="w-full h-44 object-cover rounded-t-lg bg-white"
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
src="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
|
||||||
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
|
||||||
alt={urls[0]}
|
|
||||||
className="w-full h-44 object-cover rounded-t-lg bg-white"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-2 px-3 py-3">
|
<div className="flex flex-col gap-2 px-3 py-3">
|
||||||
<h5 className="leading-none font-medium text-zinc-200">
|
<h5 className="leading-none font-medium text-zinc-200 line-clamp-1">
|
||||||
{data["og:title"]}
|
{data.title}
|
||||||
</h5>
|
</h5>
|
||||||
{data["og:description"] ? (
|
{data.description && (
|
||||||
<p className="leading-none text-sm text-zinc-400 line-clamp-3">
|
<p className="text-sm text-zinc-400 break-all line-clamp-3">
|
||||||
{data["og:description"]}
|
{data.description}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
)}
|
||||||
<span className="mt-2.5 leading-none text-sm text-zinc-500">
|
<span className="mt-2.5 leading-none text-sm text-zinc-500">
|
||||||
{domain.hostname}
|
{domain.hostname}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import ReactPlayer from "react-player/es6";
|
||||||
|
|
||||||
export function VideoPreview({ urls }: { urls: string[] }) {
|
export function VideoPreview({ urls }: { urls: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative mt-3 max-w-[420px] flex w-full flex-col gap-2">
|
||||||
onClick={(e) => e.stopPropagation()}
|
{urls.map((url) => (
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
<div key={url} className="aspect-video">
|
||||||
className="relative mt-3 max-w-[420px] flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
|
<ReactPlayer url={url} width="100%" height="100%" />
|
||||||
/>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|||||||
import { Button } from "@shared/button";
|
import { Button } from "@shared/button";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
import { dateToUnix } from "@utils/date";
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { useAccount } from "@utils/hooks/useAccount";
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
|
|
||||||
export function NoteReplyForm({ id }: { id: string }) {
|
export function NoteReplyForm({ id }: { id: string }) {
|
||||||
const ndk = useContext(RelayContext);
|
const ndk = useContext(RelayContext);
|
||||||
const account = useActiveAccount((state) => state.account);
|
|
||||||
|
|
||||||
|
const { account } = useAccount();
|
||||||
const { status, user } = useProfile(account.npub);
|
const { status, user } = useProfile(account.npub);
|
||||||
|
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function TitleBar({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
|
className="group overflow-hidden shrink-0 h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
|
||||||
>
|
>
|
||||||
<div className="w-6" />
|
<div className="w-6" />
|
||||||
<h3 className="text-sm font-medium text-zinc-200">{title}</h3>
|
<h3 className="text-sm font-medium text-zinc-200">{title}</h3>
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||||
|
import { formatCreatedAt } from "@utils/createdAt";
|
||||||
import { useProfile } from "@utils/hooks/useProfile";
|
import { useProfile } from "@utils/hooks/useProfile";
|
||||||
import { shortenKey } from "@utils/shortenKey";
|
import { shortenKey } from "@utils/shortenKey";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
export function User({
|
export function User({
|
||||||
pubkey,
|
pubkey,
|
||||||
time,
|
time,
|
||||||
size,
|
size,
|
||||||
repost,
|
repost,
|
||||||
}: { pubkey: string; time: number; size?: string; repost?: boolean }) {
|
isChat = false,
|
||||||
|
}: {
|
||||||
|
pubkey: string;
|
||||||
|
time: number;
|
||||||
|
size?: string;
|
||||||
|
repost?: boolean;
|
||||||
|
isChat?: boolean;
|
||||||
|
}) {
|
||||||
const { user } = useProfile(pubkey);
|
const { user } = useProfile(pubkey);
|
||||||
|
const createdAt = formatCreatedAt(time, isChat);
|
||||||
|
|
||||||
const avatarWidth = size === "small" ? "w-6" : "w-11";
|
const avatarWidth = size === "small" ? "w-6" : "w-11";
|
||||||
const avatarHeight = size === "small" ? "h-6" : "h-11";
|
const avatarHeight = size === "small" ? "h-6" : "h-11";
|
||||||
@@ -54,9 +59,7 @@ export function User({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="leading-none text-zinc-500">·</span>
|
<span className="leading-none text-zinc-500">·</span>
|
||||||
<span className="leading-none text-zinc-500">
|
<span className="leading-none text-zinc-500">{createdAt}</span>
|
||||||
{dayjs().to(dayjs.unix(time), true)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import {
|
|
||||||
addBlockToDB,
|
|
||||||
createAccount,
|
|
||||||
getActiveAccount,
|
|
||||||
getBlocks,
|
|
||||||
getLastLogin,
|
|
||||||
removeBlockFromDB,
|
|
||||||
updateAccount,
|
|
||||||
} from "@libs/storage";
|
|
||||||
import { create } from "zustand";
|
|
||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
|
||||||
import { immer } from "zustand/middleware/immer";
|
|
||||||
|
|
||||||
export const useActiveAccount = create(
|
|
||||||
immer(
|
|
||||||
persist(
|
|
||||||
(set: any, get: any) => ({
|
|
||||||
tempProfile: {},
|
|
||||||
account: null,
|
|
||||||
blocks: null,
|
|
||||||
lastLogin: null,
|
|
||||||
createTempProfile: (data: any) => {
|
|
||||||
set({ tempProfile: data });
|
|
||||||
},
|
|
||||||
create: async (npub: string, pubkey: string, privkey: string) => {
|
|
||||||
const response = await createAccount(npub, pubkey, privkey, null, 1);
|
|
||||||
if (response) {
|
|
||||||
const activeAccount = await getActiveAccount();
|
|
||||||
await addBlockToDB(
|
|
||||||
activeAccount.id,
|
|
||||||
0,
|
|
||||||
"Lume ❤️ You",
|
|
||||||
"https://void.cat/d/5FdJcBP5ZXKAjYqV8hpcp3",
|
|
||||||
);
|
|
||||||
set({
|
|
||||||
account: activeAccount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetch: async () => {
|
|
||||||
const response = await getActiveAccount();
|
|
||||||
set({ account: response });
|
|
||||||
},
|
|
||||||
fetchLastLogin: async () => {
|
|
||||||
const response = await getLastLogin();
|
|
||||||
set({ lastLogin: parseInt(response) });
|
|
||||||
},
|
|
||||||
fetchBlocks: async () => {
|
|
||||||
const account = get().account;
|
|
||||||
const response = await getBlocks(account.id);
|
|
||||||
set({ blocks: response });
|
|
||||||
},
|
|
||||||
addTempBlock: (
|
|
||||||
block: number,
|
|
||||||
kind: number,
|
|
||||||
title: string,
|
|
||||||
content: string,
|
|
||||||
) => {
|
|
||||||
const account = get().account;
|
|
||||||
const target = get().blocks.findIndex(
|
|
||||||
(b: { id: number }) => b.id === block,
|
|
||||||
);
|
|
||||||
// update state
|
|
||||||
set((state: any) => {
|
|
||||||
state.blocks.splice(target, 0, {
|
|
||||||
id: account.id + kind,
|
|
||||||
account_id: account.id,
|
|
||||||
kind,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addBlock: (kind: number, title: string, content: string) => {
|
|
||||||
const account = get().account;
|
|
||||||
// add to db
|
|
||||||
addBlockToDB(account.id, kind, title, content);
|
|
||||||
// update state
|
|
||||||
set((state: any) => ({
|
|
||||||
blocks: [
|
|
||||||
...state.blocks,
|
|
||||||
{
|
|
||||||
id: account.id + kind,
|
|
||||||
account_id: account.id,
|
|
||||||
kind,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
removeBlock: (id: string, db?: false) => {
|
|
||||||
if (db) {
|
|
||||||
// remove from db
|
|
||||||
removeBlockFromDB(id);
|
|
||||||
}
|
|
||||||
// update state
|
|
||||||
set((state: any) => {
|
|
||||||
const target = state.blocks.findIndex(
|
|
||||||
(b: { id: string }) => b.id === id,
|
|
||||||
);
|
|
||||||
state.blocks.splice(target, 1);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateFollows: (list: any) => {
|
|
||||||
const account = get().account;
|
|
||||||
// update db
|
|
||||||
updateAccount("follows", list, account.pubkey);
|
|
||||||
// update state
|
|
||||||
set((state: any) => ({
|
|
||||||
account: { ...state.account, follows: JSON.stringify(list) },
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "account",
|
|
||||||
storage: createJSONStorage(() => sessionStorage),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
@@ -2,7 +2,66 @@ export const APP_VERSION = "1.0.0";
|
|||||||
|
|
||||||
export const DEFAULT_AVATAR = "https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih";
|
export const DEFAULT_AVATAR = "https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih";
|
||||||
|
|
||||||
export const OPENGRAPH_KEY = "9EJG4SY-19Q4M5J-H8R29C9-091XPCC";
|
export const OPENGRAPH = {
|
||||||
|
REGEX_VALID_URL: new RegExp(
|
||||||
|
"^" +
|
||||||
|
// protocol identifier
|
||||||
|
"(?:(?:https?|ftp)://)" +
|
||||||
|
// user:pass authentication
|
||||||
|
"(?:\\S+(?::\\S*)?@)?" +
|
||||||
|
"(?:" +
|
||||||
|
// IP address exclusion
|
||||||
|
// private & local networks
|
||||||
|
"(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
|
||||||
|
"(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
|
||||||
|
"(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
|
||||||
|
// IP address dotted notation octets
|
||||||
|
// excludes loopback network 0.0.0.0
|
||||||
|
// excludes reserved space >= 224.0.0.0
|
||||||
|
// excludes network & broacast addresses
|
||||||
|
// (first & last IP address of each class)
|
||||||
|
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||||
|
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||||
|
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||||
|
"|" +
|
||||||
|
// host name
|
||||||
|
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
|
||||||
|
// domain name
|
||||||
|
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
|
||||||
|
// TLD identifier
|
||||||
|
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" +
|
||||||
|
// TLD may end with dot
|
||||||
|
"\\.?" +
|
||||||
|
")" +
|
||||||
|
// port number
|
||||||
|
"(?::\\d{2,5})?" +
|
||||||
|
// resource path
|
||||||
|
"(?:[/?#]\\S*)?" +
|
||||||
|
"$",
|
||||||
|
"i",
|
||||||
|
),
|
||||||
|
|
||||||
|
REGEX_LOOPBACK: new RegExp(
|
||||||
|
"^" +
|
||||||
|
"(?:(?:10|127)(?:\\.\\d{1,3}){3})" +
|
||||||
|
"|" +
|
||||||
|
"(?:(?:169\\.254|192\\.168|192\\.0)(?:\\.\\d{1,3}){2})" +
|
||||||
|
"|" +
|
||||||
|
"(?:172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
|
||||||
|
"$",
|
||||||
|
"i",
|
||||||
|
),
|
||||||
|
|
||||||
|
REGEX_CONTENT_TYPE_IMAGE: new RegExp("image/.*", "i"),
|
||||||
|
|
||||||
|
REGEX_CONTENT_TYPE_AUDIO: new RegExp("audio/.*", "i"),
|
||||||
|
|
||||||
|
REGEX_CONTENT_TYPE_VIDEO: new RegExp("video/.*", "i"),
|
||||||
|
|
||||||
|
REGEX_CONTENT_TYPE_TEXT: new RegExp("text/.*", "i"),
|
||||||
|
|
||||||
|
REGEX_CONTENT_TYPE_APPLICATION: new RegExp("application/.*", "i"),
|
||||||
|
};
|
||||||
|
|
||||||
export const FULL_RELAYS = [
|
export const FULL_RELAYS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
|
|||||||
43
src/utils/createdAt.tsx
Normal file
43
src/utils/createdAt.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import updateLocale from "dayjs/plugin/updateLocale";
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.extend(updateLocale);
|
||||||
|
|
||||||
|
dayjs.updateLocale("en", {
|
||||||
|
relativeTime: {
|
||||||
|
past: "%s ago",
|
||||||
|
s: "just now",
|
||||||
|
m: "1m",
|
||||||
|
mm: "%dm",
|
||||||
|
h: "1h",
|
||||||
|
hh: "%dh",
|
||||||
|
d: "1d",
|
||||||
|
dd: "%dd",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function formatCreatedAt(time, message = false) {
|
||||||
|
let formated;
|
||||||
|
|
||||||
|
const now = dayjs();
|
||||||
|
const inputTime = dayjs.unix(time);
|
||||||
|
const diff = now.diff(inputTime, "hour");
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
if (diff < 12) {
|
||||||
|
formated = inputTime.format("HH:mm A");
|
||||||
|
} else {
|
||||||
|
formated = inputTime.format("MMM DD");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (diff < 24) {
|
||||||
|
formated = inputTime.from(now, true);
|
||||||
|
} else {
|
||||||
|
formated = inputTime.format("MMM DD");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formated;
|
||||||
|
}
|
||||||
@@ -1,42 +1,17 @@
|
|||||||
import { OPENGRAPH_KEY } from "@stores/constants";
|
import { getLinkPreview } from "@libs/openGraph";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetch } from "@tauri-apps/api/http";
|
|
||||||
|
|
||||||
export function useOpenGraph(url: string) {
|
export function useOpenGraph(url: string) {
|
||||||
const { status, data, error, isFetching } = useQuery(
|
const { status, data, error, isFetching } = useQuery(
|
||||||
["preview", url],
|
["preview", url],
|
||||||
async () => {
|
async () => {
|
||||||
const result = await fetch(
|
return await getLinkPreview(url);
|
||||||
`https://skrape.dev/api/opengraph/?url=${url}&key=${OPENGRAPH_KEY}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
timeout: 10,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (result.ok) {
|
|
||||||
if (Object.keys(result.data).length === 0) {
|
|
||||||
const origin = new URL(url).origin;
|
|
||||||
const result = await fetch(
|
|
||||||
`https://skrape.dev/api/opengraph/?url=${origin}&key=${OPENGRAPH_KEY}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
timeout: 10,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (result.ok) {
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
|
staleTime: Infinity,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user