mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 03:03:31 +00:00
wip
This commit is contained in:
parent
85b30f770c
commit
fe25dbaed0
@ -19,6 +19,7 @@
|
||||
"@tanstack/react-query": "^4.29.15",
|
||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||
"@tauri-apps/api": "^1.4.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"dayjs": "^1.11.8",
|
||||
"destr": "^1.2.2",
|
||||
"framer-motion": "^10.12.17",
|
||||
@ -30,6 +31,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.0",
|
||||
"react-hotkeys-hook": "^4.4.0",
|
||||
"react-player": "^2.12.0",
|
||||
"react-resizable-panels": "^0.0.48",
|
||||
"react-router-dom": "^6.14.0",
|
||||
"react-string-replace": "^1.1.1",
|
||||
|
148
pnpm-lock.yaml
148
pnpm-lock.yaml
@ -19,6 +19,9 @@ dependencies:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
cheerio:
|
||||
specifier: 1.0.0-rc.12
|
||||
version: 1.0.0-rc.12
|
||||
dayjs:
|
||||
specifier: ^1.11.8
|
||||
version: 1.11.8
|
||||
@ -52,6 +55,9 @@ dependencies:
|
||||
react-hotkeys-hook:
|
||||
specifier: ^4.4.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:
|
||||
specifier: ^0.0.48
|
||||
version: 0.0.48(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -1399,6 +1405,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
dev: false
|
||||
|
||||
/brace-expansion@1.1.11:
|
||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||
dependencies:
|
||||
@ -1511,6 +1521,30 @@ packages:
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
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:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@ -1645,6 +1679,21 @@ packages:
|
||||
shebang-command: 2.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@ -1721,6 +1770,11 @@ packages:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1781,6 +1835,33 @@ packages:
|
||||
esutils: 2.0.3
|
||||
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:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
@ -1799,6 +1880,11 @@ packages:
|
||||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2560,6 +2646,15 @@ packages:
|
||||
lru-cache: 6.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
|
||||
dev: false
|
||||
@ -2986,6 +3081,10 @@ packages:
|
||||
strip-bom: 3.0.0
|
||||
dev: false
|
||||
|
||||
/load-script@1.0.0:
|
||||
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
||||
dev: false
|
||||
|
||||
/locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
@ -3107,6 +3206,10 @@ packages:
|
||||
engines: {node: '>=16'}
|
||||
dev: false
|
||||
|
||||
/memoize-one@5.2.1:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
dev: false
|
||||
|
||||
/memorystream@0.3.1:
|
||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
@ -3428,10 +3531,15 @@ packages:
|
||||
set-blocking: 2.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/object-hash@3.0.0:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
@ -3560,6 +3668,19 @@ packages:
|
||||
lines-and-columns: 1.2.4
|
||||
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:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
@ -3754,7 +3875,6 @@ packages:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
dev: true
|
||||
|
||||
/punycode@2.3.0:
|
||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||
@ -3790,6 +3910,10 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
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):
|
||||
resolution: {integrity: sha512-AbHeZ4ad+0dEIknSW9dBgIwcvRDfZ1O97sgj75WaMdOX0eg8TBiUf9wxzVkIjZbk76BBIE9lmFOzyD4PN80ZQg==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
@ -3811,12 +3935,24 @@ packages:
|
||||
|
||||
/react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
dev: true
|
||||
|
||||
/react-is@18.2.0:
|
||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||
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):
|
||||
resolution: {integrity: sha512-fJa3itmJ3HLLmVG7y8tka80wFW63N6ai76q7MGwU8nSXeA0qkX36vnmPyXm34lvtsGjn1Cgi5IPhPQnf42SVpA==}
|
||||
peerDependencies:
|
||||
@ -3981,8 +4117,8 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: false
|
||||
|
||||
/rollup@3.25.1:
|
||||
resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==}
|
||||
/rollup@3.25.2:
|
||||
resolution: {integrity: sha512-VLnkxZMDr3jpxgtmS8pQZ0UvhslmF4ADq/9w4erkctbgjCqLW9oa89fJuXEs4ZmgyoF7Dm8rMDKSS5b5u2hHUg==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
@ -4753,7 +4889,7 @@ packages:
|
||||
'@types/node': 18.16.18
|
||||
esbuild: 0.17.19
|
||||
postcss: 8.4.24
|
||||
rollup: 3.25.1
|
||||
rollup: 3.25.2
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
@ -22,11 +22,7 @@
|
||||
"http": {
|
||||
"all": true,
|
||||
"request": true,
|
||||
"scope": [
|
||||
"https://void.cat/*",
|
||||
"https://skrape.dev/*",
|
||||
"https://lume.nu/*"
|
||||
]
|
||||
"scope": ["http://**", "https://**"]
|
||||
},
|
||||
"fs": {
|
||||
"all": false,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createAccount } from "@libs/storage";
|
||||
import { createAccount, createBlock } from "@libs/storage";
|
||||
import { Button } from "@shared/button";
|
||||
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
@ -30,6 +30,11 @@ export function CreateStep1Screen() {
|
||||
mutationFn: (data: any) =>
|
||||
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||
onSuccess: () => {
|
||||
createBlock(
|
||||
0,
|
||||
"Preserve your freedom",
|
||||
"https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv",
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||
// redirect to next step
|
||||
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 { useAccount } from "@utils/hooks/useAccount";
|
||||
import { useContext, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function CreateStep3Screen() {
|
||||
const ndk = useContext(RelayContext);
|
||||
const profile = useOnboarding((state: any) => state.profile);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { account } = useAccount();
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
@ -48,6 +51,7 @@ export function CreateStep3Screen() {
|
||||
event.publish();
|
||||
|
||||
// redirect to step 4
|
||||
navigate("/auth/create/step-4", { replace: true });
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
|
@ -131,8 +131,6 @@ export function CreateStep4Screen() {
|
||||
updateAccount("follows", follows, account.pubkey),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||
// redirect to next step
|
||||
navigate("/auth/onboarding", { replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
@ -156,6 +154,9 @@ export function CreateStep4Screen() {
|
||||
|
||||
// update
|
||||
update.mutate(follows);
|
||||
|
||||
// redirect to next step
|
||||
setTimeout(() => navigate("/auth/onboarding", { replace: true }), 1200);
|
||||
} catch {
|
||||
console.log("error");
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createAccount } from "@libs/storage";
|
||||
import { createAccount, createBlock } from "@libs/storage";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
@ -31,6 +31,11 @@ export function ImportStep1Screen() {
|
||||
mutationFn: (data: any) =>
|
||||
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||
onSuccess: () => {
|
||||
createBlock(
|
||||
0,
|
||||
"Preserve your freedom",
|
||||
"https://void.cat/d/949GNg7ZjSLHm2eTR3jZqv",
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||
// redirect to next step
|
||||
navigate("/auth/import/step-2", { replace: true });
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { User } from "@shared/user";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { useContext } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
export function OnboardingScreen() {
|
||||
@ -12,9 +13,12 @@ export function OnboardingScreen() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { status, account } = useAccount();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const publish = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
@ -30,7 +34,7 @@ export function OnboardingScreen() {
|
||||
event.publish();
|
||||
|
||||
// redirect to home
|
||||
navigate("/", { replace: true });
|
||||
setTimeout(() => navigate("/", { replace: true }), 1200);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@ -80,9 +84,15 @@ export function OnboardingScreen() {
|
||||
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"
|
||||
>
|
||||
<span className="w-5" />
|
||||
<span>Publish</span>
|
||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Publish</span>
|
||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
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">
|
||||
<form
|
||||
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
|
||||
type={"hidden"}
|
||||
|
@ -7,6 +7,7 @@ export function ChannelsListItem({ data }: { data: any }) {
|
||||
return (
|
||||
<NavLink
|
||||
to={`/app/channel/${data.event_id}`}
|
||||
preventScrollReset={true}
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"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 { RelayContext } from "@shared/relayProvider";
|
||||
import { Tooltip } from "@shared/tooltip";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { Fragment, useContext, useState } from "react";
|
||||
|
||||
export function MessageHideButton({ id }: { id: string }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state: any) => state.account);
|
||||
const hide = useChannelMessages((state: any) => state.hideMessage);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { account } = useAccount();
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
||||
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="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]">
|
||||
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
{content.parsed}
|
||||
|
@ -3,18 +3,19 @@ import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { CancelIcon, MuteIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { Tooltip } from "@shared/tooltip";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { Fragment, useContext, useState } from "react";
|
||||
|
||||
export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state: any) => state.account);
|
||||
const mute = useChannelMessages((state: any) => state.muteUser);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { account } = useAccount();
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
@ -109,29 +109,31 @@ export function ChannelScreen() {
|
||||
>
|
||||
<h3 className="font-semibold text-zinc-100">Public Channel</h3>
|
||||
</div>
|
||||
<div className="w-full flex-1 p-3">
|
||||
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
|
||||
{!messages ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messages}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
alignToBottom={true}
|
||||
followOutput={true}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
className="scrollbar-hide overflow-y-auto h-full w-full"
|
||||
components={{
|
||||
Header: () => Header,
|
||||
EmptyPlaceholder: () => Empty,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="w-full inline-flex shrink-0 px-5 py-3 border-t border-zinc-800">
|
||||
<div className="w-full h-full flex-1 p-3">
|
||||
<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-1 w-full h-full">
|
||||
{!messages ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messages}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
alignToBottom={true}
|
||||
followOutput={true}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
className="scrollbar-hide overflow-y-auto"
|
||||
components={{
|
||||
Header: () => Header,
|
||||
EmptyPlaceholder: () => Empty,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,6 +20,7 @@ export function ChatsListItem({ data }: { data: any }) {
|
||||
) : (
|
||||
<NavLink
|
||||
to={`/app/chat/${data.sender_pubkey}`}
|
||||
preventScrollReset={true}
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
|
||||
|
@ -26,7 +26,11 @@ export function ChatMessageItem({
|
||||
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 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]">
|
||||
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
{content.parsed}
|
||||
|
@ -94,7 +94,7 @@ export function NewMessageModal() {
|
||||
</Dialog.Description>
|
||||
</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 ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
|
@ -20,6 +20,7 @@ export function ChatsListSelfItem({ data }: { data: any }) {
|
||||
) : (
|
||||
<NavLink
|
||||
to={`/app/chat/${data.pubkey}`}
|
||||
preventScrollReset={true}
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"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>
|
||||
</div>
|
||||
<div className="w-full flex-1 p-3">
|
||||
{account && (
|
||||
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
|
||||
<div className="w-full h-full flex-1 p-3">
|
||||
<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-1 w-full h-full">
|
||||
{status === "loading" ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div className="h-full w-full">
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={data}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={data.length - 1}
|
||||
alignToBottom={true}
|
||||
followOutput={true}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
||||
components={{
|
||||
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}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={data}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={data.length - 1}
|
||||
alignToBottom={true}
|
||||
followOutput={true}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
className="relative scrollbar-hide overflow-y-auto"
|
||||
components={{
|
||||
EmptyPlaceholder: () => Empty,
|
||||
}}
|
||||
/>
|
||||
</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 className="col-span-1">
|
||||
@ -92,18 +90,16 @@ export function ChatScreen() {
|
||||
data-tauri-drag-region
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const Empty = (
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h3 className="text-base font-semibold leading-none text-white">
|
||||
Nothing to see here yet
|
||||
</h3>
|
||||
<p className="text-base leading-none text-zinc-400">
|
||||
<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="mb-2 text-4xl">🙌</h3>
|
||||
<p className="leading-none text-zinc-400">
|
||||
You two didn't talk yet, let's send first message
|
||||
</p>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
getLastLogin,
|
||||
} from "@libs/storage";
|
||||
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
||||
import { LumeIcon } from "@shared/icons";
|
||||
import { LoaderIcon, LumeIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
@ -177,27 +177,7 @@ export function Root() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-16 left-1/2 -translate-x-1/2 transform">
|
||||
<svg
|
||||
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>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { createBlock } from "@libs/storage";
|
||||
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 { Fragment, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
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 [isOpen, setIsOpen] = useState(true);
|
||||
@ -18,6 +21,13 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
||||
parentState(false);
|
||||
};
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: any) => createBlock(data.kind, data.title, data.content),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -35,7 +45,7 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
||||
}
|
||||
|
||||
// insert to database
|
||||
addBlock(1, data.title, pubkey);
|
||||
block.mutate({ kind: 1, title: data.title, content: pubkey });
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
@ -43,7 +53,7 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
||||
reset();
|
||||
// close modal
|
||||
closeModal();
|
||||
}, 1000);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -70,7 +80,7 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
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="flex flex-col gap-2">
|
||||
<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">
|
||||
<form
|
||||
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">
|
||||
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||
|
@ -1,28 +1,29 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { createBlock } from "@libs/storage";
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { CancelIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { Body, fetch } from "@tauri-apps/api/http";
|
||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export function AddImageBlock({ parentState }: { parentState: any }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [account, addBlock] = useActiveAccount((state: any) => [
|
||||
state.account,
|
||||
state.addBlock,
|
||||
]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [image, setImage] = useState("");
|
||||
|
||||
const { account } = useAccount();
|
||||
|
||||
const tags = useRef(null);
|
||||
|
||||
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) => {
|
||||
setLoading(true);
|
||||
|
||||
@ -105,8 +113,8 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// insert to database
|
||||
addBlock(0, data.title, data.content);
|
||||
// mutate
|
||||
block.mutate({ kind: 0, title: data.title, content: data.content });
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
@ -114,7 +122,7 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
||||
reset();
|
||||
// close modal
|
||||
closeModal();
|
||||
}, 1000);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -145,7 +153,7 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
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="flex flex-col gap-2">
|
||||
<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">
|
||||
<form
|
||||
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
|
||||
type={"hidden"}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { getNotesByAuthor } from "@libs/storage";
|
||||
import { getNotesByAuthor, removeBlock } from "@libs/storage";
|
||||
import { Note } from "@shared/notes/note";
|
||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||
import { TitleBar } from "@shared/titleBar";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
@ -11,12 +14,7 @@ const ITEM_PER_PAGE = 10;
|
||||
const TIME = Math.floor(Date.now() / 1000);
|
||||
|
||||
export function FeedBlock({ params }: { params: any }) {
|
||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
||||
|
||||
const close = () => {
|
||||
removeBlock(params.id, true);
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
status,
|
||||
data,
|
||||
@ -65,6 +63,13 @@ export function FeedBlock({ params }: { params: any }) {
|
||||
}
|
||||
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (id: string) => removeBlock(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||
},
|
||||
});
|
||||
|
||||
const renderItem = (index: string | number) => {
|
||||
const note = notes[index];
|
||||
|
||||
@ -78,7 +83,7 @@ export function FeedBlock({ params }: { params: any }) {
|
||||
|
||||
return (
|
||||
<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
|
||||
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"
|
||||
|
@ -4,7 +4,6 @@ import { Note } from "@shared/notes/note";
|
||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { TitleBar } from "@shared/titleBar";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
@ -63,17 +62,16 @@ export function FollowingBlock({ block }: { block: number }) {
|
||||
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
|
||||
|
||||
useEffect(() => {
|
||||
let sub: NDKSubscription;
|
||||
const follows = account ? JSON.parse(account.follows) : [];
|
||||
|
||||
if (account) {
|
||||
const follows = JSON.parse(account.follows);
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1, 6],
|
||||
authors: follows,
|
||||
since: dateToUnix(),
|
||||
};
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1, 6],
|
||||
authors: follows,
|
||||
since: dateToUnix(),
|
||||
};
|
||||
|
||||
sub = ndk.subscribe(filter);
|
||||
const sub = account ? ndk.subscribe(filter) : null;
|
||||
if (sub) {
|
||||
sub.addListener("event", (event: NDKEvent) => {
|
||||
createNote(
|
||||
event.id,
|
||||
@ -87,7 +85,9 @@ export function FollowingBlock({ block }: { block: number }) {
|
||||
}
|
||||
|
||||
return () => {
|
||||
sub.stop();
|
||||
if (sub) {
|
||||
sub.stop();
|
||||
}
|
||||
};
|
||||
}, [account]);
|
||||
|
||||
|
@ -1,25 +1,30 @@
|
||||
import { removeBlock } from "@libs/storage";
|
||||
import { Image } from "@shared/image";
|
||||
import { TitleBar } from "@shared/titleBar";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export function ImageBlock({ params }: { params: any }) {
|
||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const close = () => {
|
||||
removeBlock(params.id, true);
|
||||
};
|
||||
const block = useMutation({
|
||||
mutationFn: (id: string) => removeBlock(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
|
||||
<TitleBar title={params.title} onClick={() => close()} />
|
||||
<div className="w-full flex-1 p-3">
|
||||
<Image
|
||||
src={params.content}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt={params.title}
|
||||
className="w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
<div className="shrink-0 w-[350px] h-full flex flex-col justify-between border-r border-zinc-900">
|
||||
<div className="flex-1 w-full h-full overflow-hidden p-3">
|
||||
<div className="w-full h-full">
|
||||
<Image
|
||||
src={params.content}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt={params.title}
|
||||
className="w-full h-full object-cover rounded-xl border-t border-zinc-800/50"
|
||||
/>
|
||||
</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 { Kind1063 } from "@shared/notes/contents/kind1063";
|
||||
import { NoteMetadata } from "@shared/notes/metadata";
|
||||
@ -7,11 +7,12 @@ import { RepliesList } from "@shared/notes/replies/list";
|
||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||
import { TitleBar } from "@shared/titleBar";
|
||||
import { User } from "@shared/user";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { parser } from "@utils/parser";
|
||||
|
||||
export function ThreadBlock({ params }: { params: any }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { status, data, isFetching } = useQuery(
|
||||
["thread", params.content],
|
||||
async () => {
|
||||
@ -19,16 +20,18 @@ export function ThreadBlock({ params }: { params: any }) {
|
||||
},
|
||||
);
|
||||
|
||||
const content = data ? parser(data) : null;
|
||||
const removeBlock = useActiveAccount((state: any) => state.removeBlock);
|
||||
const block = useMutation({
|
||||
mutationFn: (id: string) => removeBlock(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||
},
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
removeBlock(params.id, false);
|
||||
};
|
||||
const content = data ? parser(data) : null;
|
||||
|
||||
return (
|
||||
<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">
|
||||
{status === "loading" || isFetching ? (
|
||||
<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 { ThreadBlock } from "@app/space/components/blocks/thread";
|
||||
import { getBlocks } from "@libs/storage";
|
||||
|
||||
const blocks = await getBlocks();
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export function SpaceScreen() {
|
||||
const {
|
||||
status,
|
||||
data: blocks,
|
||||
isFetching,
|
||||
} = useQuery(
|
||||
["blocks"],
|
||||
async () => {
|
||||
return await getBlocks();
|
||||
},
|
||||
{
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
||||
<FollowingBlock block={1} />
|
||||
{!blocks ? (
|
||||
<p>Loading...</p>
|
||||
{status === "loading" ? (
|
||||
<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) => {
|
||||
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="w-full h-full inline-flex items-center justify-center">
|
||||
<AddBlock />
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { Image } from "@shared/image";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { compactNumber } from "@utils/number";
|
||||
@ -18,6 +18,7 @@ export function UserScreen() {
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
const pubkey = searchParams.pubkey || "";
|
||||
|
||||
const { account } = useAccount();
|
||||
const { user } = useProfile(pubkey);
|
||||
const { data: userStats, error } = useQuery(["user", pubkey], async () => {
|
||||
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 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
|
||||
export async function addBlockToDB(
|
||||
account_id: number,
|
||||
kind: number,
|
||||
title: string,
|
||||
content: any,
|
||||
) {
|
||||
export async function createBlock(kind: number, title: string, content: any) {
|
||||
const db = await connect();
|
||||
const activeAccount = await getActiveAccount();
|
||||
return await db.execute(
|
||||
"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();
|
||||
return await db.execute(`DELETE FROM blocks WHERE id = "${id}";`);
|
||||
}
|
||||
|
@ -8,14 +8,14 @@ import {
|
||||
ChevronRightIcon,
|
||||
ComposeIcon,
|
||||
} from "@shared/icons";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useComposer } from "@stores/composer";
|
||||
import { COMPOSE_SHORTCUT } from "@stores/shortcuts";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { Fragment } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export function Composer() {
|
||||
const account = useActiveAccount((state) => state.account);
|
||||
const { account } = useAccount();
|
||||
|
||||
const [toggle, open] = useComposer((state: any) => [
|
||||
state.toggleModal,
|
||||
|
@ -30,6 +30,7 @@ export function Navigation({ reverse = false }: { reverse?: boolean }) {
|
||||
<div className="flex flex-col">
|
||||
<NavLink
|
||||
to="/app/space"
|
||||
preventScrollReset={true}
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"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
|
||||
to="/app/trending"
|
||||
preventScrollReset={true}
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"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 { Kind1063 } from "@shared/notes/contents/kind1063";
|
||||
import { NoteSkeleton } from "@shared/notes/skeleton";
|
||||
import { User } from "@shared/user";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEvent } from "@utils/hooks/useEvent";
|
||||
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 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 (
|
||||
<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" ? (
|
||||
<NoteSkeleton />
|
||||
) : (
|
||||
|
@ -1,18 +1,25 @@
|
||||
import { createBlock } from "@libs/storage";
|
||||
import { ReplyIcon } from "@shared/icons";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { compactNumber } from "@utils/number";
|
||||
|
||||
export function NoteReply({
|
||||
id,
|
||||
replies,
|
||||
currentBlock,
|
||||
}: { 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 selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
addTempBlock(currentBlock, 2, "Thread", thread);
|
||||
block.mutate({ kind: 2, title: "Thread", content: thread });
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
@ -1,23 +1,12 @@
|
||||
import { Image } from "@shared/image";
|
||||
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[] }) {
|
||||
const domain = new URL(urls[0]);
|
||||
const { status, data, error, isFetching } = useOpenGraph(urls[0]);
|
||||
const { status, data, isFetching } = useOpenGraph(urls[0]);
|
||||
|
||||
return (
|
||||
<div className="mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
|
||||
{error && <p>failed to load</p>}
|
||||
{isFetching || status === "loading" ? (
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full h-44 bg-zinc-700 animate-pulse" />
|
||||
@ -29,20 +18,6 @@ export function LinkPreview({ urls }: { urls: string[] }) {
|
||||
</span>
|
||||
</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
|
||||
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"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{isValidURL(data["og:image"]) ? (
|
||||
<Image
|
||||
src={data["og:image"]}
|
||||
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||
alt={urls[0]}
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
src={data.images[0]}
|
||||
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
|
||||
alt={urls[0]}
|
||||
className="w-full h-44 object-cover rounded-t-lg"
|
||||
/>
|
||||
<div className="flex flex-col gap-2 px-3 py-3">
|
||||
<h5 className="leading-none font-medium text-zinc-200">
|
||||
{data["og:title"]}
|
||||
<h5 className="leading-none font-medium text-zinc-200 line-clamp-1">
|
||||
{data.title}
|
||||
</h5>
|
||||
{data["og:description"] ? (
|
||||
<p className="leading-none text-sm text-zinc-400 line-clamp-3">
|
||||
{data["og:description"]}
|
||||
{data.description && (
|
||||
<p className="text-sm text-zinc-400 break-all line-clamp-3">
|
||||
{data.description}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<span className="mt-2.5 leading-none text-sm text-zinc-500">
|
||||
{domain.hostname}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import ReactPlayer from "react-player/es6";
|
||||
|
||||
export function VideoPreview({ urls }: { urls: string[] }) {
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="relative mt-3 max-w-[420px] flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
|
||||
/>
|
||||
<div className="relative mt-3 max-w-[420px] flex w-full flex-col gap-2">
|
||||
{urls.map((url) => (
|
||||
<div key={url} className="aspect-video">
|
||||
<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 { Image } from "@shared/image";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { useContext, useState } from "react";
|
||||
|
||||
export function NoteReplyForm({ id }: { id: string }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state) => state.account);
|
||||
|
||||
const { account } = useAccount();
|
||||
const { status, user } = useProfile(account.npub);
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
|
@ -7,7 +7,7 @@ export function TitleBar({
|
||||
return (
|
||||
<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"
|
||||
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" />
|
||||
<h3 className="text-sm font-medium text-zinc-200">{title}</h3>
|
||||
|
@ -1,22 +1,27 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { formatCreatedAt } from "@utils/createdAt";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function User({
|
||||
pubkey,
|
||||
time,
|
||||
size,
|
||||
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 createdAt = formatCreatedAt(time, isChat);
|
||||
|
||||
const avatarWidth = size === "small" ? "w-6" : "w-11";
|
||||
const avatarHeight = size === "small" ? "h-6" : "h-11";
|
||||
@ -54,9 +59,7 @@ export function User({
|
||||
</span>
|
||||
)}
|
||||
<span className="leading-none text-zinc-500">·</span>
|
||||
<span className="leading-none text-zinc-500">
|
||||
{dayjs().to(dayjs.unix(time), true)}
|
||||
</span>
|
||||
<span className="leading-none text-zinc-500">{createdAt}</span>
|
||||
</div>
|
||||
<Transition
|
||||
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 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 = [
|
||||
"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 { fetch } from "@tauri-apps/api/http";
|
||||
|
||||
export function useOpenGraph(url: string) {
|
||||
const { status, data, error, isFetching } = useQuery(
|
||||
["preview", url],
|
||||
async () => {
|
||||
const result = await fetch(
|
||||
`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;
|
||||
}
|
||||
return await getLinkPreview(url);
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user