wip
This commit is contained in:
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user