adding subs

This commit is contained in:
zmeyer44 2023-10-20 15:26:55 -04:00
parent a3aa61cd29
commit 3039083bd9
31 changed files with 1081 additions and 144 deletions

View File

@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import SubscriptionCard from "@/components/SubscriptionCard";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { useNDK } from "@/app/_providers/ndk";
import { type NDKKind, type NDKEvent } from "@nostr-dev-kit/ndk";
import { getTagsValues } from "@/lib/nostr/utils";
type MySubscription = {
pubkey: string;
};
export default function MySubscription({ pubkey }: MySubscription) {
const { fetchEvents } = useNDK();
const { currentUser, mySubscription, follows } = useCurrentUser();
const [subscriptionTiers, setSubscriptionTiers] = useState<NDKEvent[]>([]);
useEffect(() => {
void handleFetchSubscriptionTiers();
}, [pubkey]);
async function handleFetchSubscriptionTiers() {
try {
const events = await fetchEvents({
kinds: [30044 as NDKKind],
authors: [pubkey],
});
setSubscriptionTiers(events);
} catch (err) {
console.log("error", err);
}
}
if (!subscriptionTiers.length) return null;
return (
<>
{subscriptionTiers.map((e) => {
const isMember =
currentUser &&
getTagsValues("p", e.tags).includes(currentUser.pubkey);
if (isMember) return null;
return <SubscriptionCard key={e.id} event={e} />;
})}
</>
);
}

View File

@ -1,8 +1,8 @@
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import SubscriptionCard from "@/components/SubscriptionCard";
import { HiCheckBadge } from "react-icons/hi2";
import Tabs from "@/components/Tabs";
import useProfile from "@/lib/hooks/useProfile";
@ -11,9 +11,23 @@ import ProfileFeed from "./_components/Feed";
import Subscriptions from "./_components/Subscriptions";
import { nip19 } from "nostr-tools";
import useLists from "@/lib/hooks/useLists";
import EditProfileModal from "@/components/Modals/EditProfile";
import { useModal } from "@/app/_providers/modal/provider";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { NDKUser } from "@nostr-dev-kit/ndk";
import MySubscription from "./_components/MySubscription";
const EditProfileModal = dynamic(
() => import("@/components/Modals/EditProfile"),
{
ssr: false,
},
);
const CreateSubecriptionTierModal = dynamic(
() => import("@/components/Modals/CreateSubscriptionTier"),
{
ssr: false,
},
);
export default function ProfilePage({
params: { npub },
}: {
@ -22,7 +36,7 @@ export default function ProfilePage({
};
}) {
const modal = useModal();
const { currentUser, follows } = useCurrentUser();
const { currentUser, mySubscription, follows } = useCurrentUser();
const [activeTab, setActiveTab] = useState("feed");
const { type, data } = nip19.decode(npub);
@ -31,7 +45,6 @@ export default function ProfilePage({
}
const pubkey = data.toString();
const { profile } = useProfile(pubkey);
const { init, lists } = useLists();
return (
<div className="relative mx-auto max-w-5xl space-y-6">
@ -74,6 +87,14 @@ export default function ProfilePage({
)}
</div>
<div className="flex items-center gap-3">
{currentUser?.pubkey === pubkey && !mySubscription && (
<Button
onClick={() => modal?.show(<CreateSubecriptionTierModal />)}
className="rounded-sm px-5 max-sm:h-8 max-sm:text-xs"
>
Add Subscription Tier
</Button>
)}
{currentUser?.pubkey === pubkey && (
<Button
onClick={() => modal?.show(<EditProfileModal />)}
@ -83,15 +104,22 @@ export default function ProfilePage({
Edit
</Button>
)}
{!follows.includes(pubkey) && (
<Button
onClick={() => modal?.show(<EditProfileModal />)}
variant={"default"}
className="rounded-sm px-5 max-sm:h-8 max-sm:text-xs"
>
Follow
</Button>
)}
{currentUser &&
!Array.from(follows).find((i) => i.pubkey === pubkey) && (
<Button
onClick={() =>
void currentUser.follow(
new NDKUser({
hexpubkey: pubkey,
}),
)
}
variant={"default"}
className="rounded-sm px-5 max-sm:h-8 max-sm:text-xs"
>
Follow
</Button>
)}
</div>
</div>
<div className="mx-auto max-w-[800px] space-y-1 px-4">
@ -123,9 +151,7 @@ export default function ProfilePage({
</div>
<div className="mx-auto max-w-[800px] space-y-6">
<div className="flex max-w-2xl flex-col gap-4 px-4">
{/* {[].map((e) => (
<SubscriptionCard key={e.id} {...e} />
))} */}
<MySubscription pubkey={pubkey} />
</div>
<div className="">
<Tabs

View File

@ -37,7 +37,8 @@ const LoginModal = dynamic(() => import("@/components/Modals/Login"), {
export default function AuthActions() {
const router = useRouter();
const modal = useModal();
const { currentUser, logout, attemptLogin } = useCurrentUser();
const { currentUser, logout, attemptLogin, initSubscriptions } =
useCurrentUser();
const { ndk } = useNDK();
useKeyboardShortcut(["shift", "ctrl", "u"], () => {

View File

@ -20,7 +20,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
{/* Sidebar */}
<Sidebar />
<div className="relative flex flex-1 shrink-0 grow justify-center overflow-x-hidden">
<div className="flex-1 overflow-x-hidden pb-5">{children}</div>
<div className="flex-1 pb-5">{children}</div>
</div>
{/* Mobile Banner */}
<MobileBanner />

View File

@ -1,7 +1,11 @@
"use client";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { useModal } from "@/app/_providers/modal/provider";
import CreateSubscriptionTier from "@/components/Modals/CreateSubscriptionTier";
export default function BecomeACreator() {
const modal = useModal();
return (
<div className="padded-container overflow-x-clip pb-8 max-sm:-mx-5 md:py-[120px]">
<div className="flex w-full flex-col items-center justify-between gap-8 lg:flex-row">
@ -19,10 +23,12 @@ export default function BecomeACreator() {
Start earning on Nostr
</h2>
<div className="mb-6 mt-2 text-muted-foreground">
Start earning bitcoin from your content. Create lists that for fans
can subscribe to!
Create a subscrition tier so you can start offering your users
access to private content!
</div>
<Button>Become a Creator</Button>
<Button onClick={() => modal?.show(<CreateSubscriptionTier />)}>
Become a Creator
</Button>
</div>
</div>
</div>

View File

@ -23,9 +23,12 @@ import { formatDate } from "@/lib/utils/dates";
const EditListModal = dynamic(() => import("@/components/Modals/EditList"), {
ssr: false,
});
const CreateEventModal = dynamic(() => import("@/components/Modals/NewEvent"), {
ssr: false,
});
const CreateListEvent = dynamic(
() => import("@/components/Modals/ShortTextNoteOnList"),
{
ssr: false,
},
);
const ConfirmModal = dynamic(() => import("@/components/Modals/Confirm"), {
ssr: false,
});
@ -151,7 +154,7 @@ export default function Header({ event }: { event: NDKEvent }) {
<div className="flex flex-wrap items-center gap-3">
{!!currentUser && currentUser.pubkey === pubkey && (
<>
<Button onClick={() => modal?.show(<CreateEventModal />)}>
<Button onClick={() => modal?.show(<CreateListEvent />)}>
Add Event
</Button>
<Button

View File

@ -0,0 +1,224 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import dynamic from "next/dynamic";
import { Button } from "@/components/ui/button";
import useProfile from "@/lib/hooks/useProfile";
import { HiOutlineLightningBolt } from "react-icons/hi";
import Spinner from "@/components/spinner";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import ProfileInfo from "./ProfileInfo";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { useNDK } from "@/app/_providers/ndk";
import { toast } from "sonner";
import {
sendZap,
checkPayment,
updateListUsersFromZaps,
} from "@/lib/actions/zap";
import { useModal } from "@/app/_providers/modal/provider";
import { type NDKEvent } from "@nostr-dev-kit/ndk";
import { btcToSats, formatNumber } from "@/lib/utils";
import { formatDate } from "@/lib/utils/dates";
const EditListModal = dynamic(() => import("@/components/Modals/EditList"), {
ssr: false,
});
const CreateEventModal = dynamic(() => import("@/components/Modals/NewEvent"), {
ssr: false,
});
const ConfirmModal = dynamic(() => import("@/components/Modals/Confirm"), {
ssr: false,
});
export default function Header({ event }: { event: NDKEvent }) {
const { currentUser } = useCurrentUser();
const modal = useModal();
const { ndk } = useNDK();
const [checkingPayment, setCheckingPayment] = useState(false);
const [hasValidPayment, setHasValidPayment] = useState(false);
const [syncingUsers, setSyncingUsers] = useState(false);
const pubkey = event.pubkey;
const { profile } = useProfile(pubkey);
const noteIds = getTagsValues("e", event.tags).filter(Boolean);
const title =
getTagValues("title", event.tags) ??
getTagValues("name", event.tags) ??
"Untitled";
const image =
getTagValues("image", event.tags) ??
getTagValues("picture", event.tags) ??
getTagValues("banner", event.tags) ??
profile?.banner;
const description = getTagValues("description", event.tags);
const rawEvent = event.rawEvent();
const subscriptionsEnabled = !!getTagValues("subscriptions", rawEvent.tags);
const priceInBTC = parseFloat(getTagValues("price", rawEvent.tags) ?? "0");
const isMember =
currentUser &&
getTagsValues("p", rawEvent.tags).includes(currentUser.pubkey);
useEffect(() => {
if (!currentUser || !subscriptionsEnabled) return;
if (!isMember && !checkingPayment && !hasValidPayment) {
void handleCheckPayment();
}
}, [isMember, currentUser]);
async function handleCheckPayment() {
if (!event || !currentUser || !ndk) return;
setCheckingPayment(true);
console.log("Checking payment");
try {
const result = await checkPayment(
ndk,
event.tagId(),
currentUser.pubkey,
rawEvent,
);
console.log("Payment result", result);
if (result) {
setHasValidPayment(true);
}
} catch (err) {
console.log("error sending zap", err);
} finally {
setCheckingPayment(false);
}
}
async function handleSyncUsers() {
if (!event || !ndk) return;
setSyncingUsers(true);
try {
console.log("handleSyncUsers");
await updateListUsersFromZaps(ndk, event.tagId(), rawEvent);
toast.success("Users Synced!");
} catch (err) {
console.log("error syncing users", err);
} finally {
setSyncingUsers(false);
}
}
async function handleSendZap() {
try {
const result = await sendZap(
ndk!,
btcToSats(priceInBTC),
rawEvent,
`Access payment: ${title}`,
);
toast.success("Payment Sent!");
void handleCheckPayment();
} catch (err) {
console.log("error sending zap", err);
} finally {
}
}
if (!event) {
return (
<div className="center pt-20 text-primary">
<Spinner />
</div>
);
}
return (
<div className="relative overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
<div className="overflow-hidden rounded-[0.5rem] p-0">
<div className="relative w-full overflow-hidden bg-gradient-to-b from-primary pb-[50%] @5xl:rounded-[20px] md:pb-[40%]">
{!!image && (
<Image
className="absolute inset-0 h-full w-full object-cover align-middle"
src={image}
width={400}
height={100}
alt="banner"
unoptimized
/>
)}
</div>
</div>
<div className="space-y-1 p-3 @sm:px-3.5 @sm:pb-2 @sm:pt-5">
<div className="flex items-start justify-between gap-x-1.5 @lg:gap-x-2.5">
<div className="space-y-1 @sm:space-y-2">
<h2 className="font-condensed text-2xl font-semibold sm:text-3xl lg:text-4xl">
{title}
</h2>
<div className="flex items-center">
<ProfileInfo pubkey={pubkey} />
</div>
</div>
<div className="flex flex-wrap items-center gap-3">
{!!currentUser && currentUser.pubkey === pubkey && (
<>
<Button onClick={() => modal?.show(<CreateEventModal />)}>
Add Note
</Button>
<Button
variant={"outline"}
loading={syncingUsers}
onClick={() => void handleSyncUsers()}
>
Sync users
</Button>
{/* <Button
variant="ghost"
onClick={() =>
modal?.show(<EditListModal listEvent={rawEvent} />)
}
>
Edit
</Button> */}
</>
)}
{subscriptionsEnabled &&
!isMember &&
(hasValidPayment ? (
<Button variant={"outline"}>Pending Sync</Button>
) : (
<Button
onClick={() =>
modal?.show(
<ConfirmModal
title={`Subscribe to ${title}`}
onConfirm={handleSendZap}
ctaBody={
<>
<span>Zap to Subscribe</span>
<HiOutlineLightningBolt className="h-4 w-4" />
</>
}
>
<p className="text-muted-forground">
{`Pay ${priceInBTC} BTC (${formatNumber(
btcToSats(priceInBTC),
)} sats) for year long access until ${formatDate(
new Date(
new Date().setFullYear(
new Date().getFullYear() + 1,
),
),
"MMM Do, YYYY",
)}`}
</p>
</ConfirmModal>,
)
}
>
Subscribe
</Button>
))}
</div>
</div>
<div className="pt-1 @md:pt-2">
{!!description && (
<p className="line-clamp-3 text-sm text-muted-foreground md:text-sm">
{description}
</p>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,44 @@
import Link from "next/link";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import useProfile from "@/lib/hooks/useProfile";
import { nip19 } from "nostr-tools";
import { getTwoLetters, getNameToShow } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
import { HiMiniChevronRight, HiCheckBadge } from "react-icons/hi2";
type ProfileInfoProps = {
pubkey: string;
};
export default function ProfileInfo({ pubkey }: ProfileInfoProps) {
const { profile } = useProfile(pubkey);
const npub = nip19.npubEncode(pubkey);
return (
<Link
href={`/${npub}`}
className="center group gap-x-2 rounded-sm rounded-r-full border bg-background/50 pl-0.5 pr-1 text-muted-foreground hover:shadow"
>
<Avatar className="center h-[16px] w-[16px] overflow-hidden rounded-[.25rem] bg-muted @sm:h-[18px] @sm:w-[18px]">
<AvatarImage src={profile?.image} alt={profile?.displayName} />
<AvatarFallback className="text-[8px]">
{getTwoLetters({ npub, profile })}
</AvatarFallback>
</Avatar>
<div className="flex items-center gap-1">
<span className="text-[14px] ">{getNameToShow({ npub, profile })}</span>
{!!profile?.nip05 && <HiCheckBadge className="h-3 w-3 text-primary" />}
</div>
<HiMiniChevronRight className="h-4 w-4" />
</Link>
);
}
export function LoadingProfileInfo() {
return (
<div className="center group gap-x-1">
<Avatar className="center h-[16px] w-[16px] overflow-hidden rounded-[.25rem] bg-muted @sm:h-[18px] @sm:w-[18px]"></Avatar>
<div className="space-y-1">
<Skeleton className="h-2 w-[70px] bg-muted" />
</div>
</div>
);
}

View File

@ -0,0 +1,71 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import SubscriptionCard from "@/components/SubscriptionCard";
import { HiCheckBadge } from "react-icons/hi2";
import Tabs from "@/components/Tabs";
import useProfile from "@/lib/hooks/useProfile";
import { getTwoLetters, truncateText } from "@/lib/utils";
import { nip19 } from "nostr-tools";
import useEvents from "@/lib/hooks/useEvents";
import Spinner from "@/components/spinner";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import ProfileInfo from "./_components/ProfileInfo";
import Feed from "@/containers/Feed";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import Header from "./_components/Header";
export default function ListPage({
params: { naddr },
}: {
params: {
naddr: string;
};
}) {
const { type, data } = nip19.decode(naddr);
if (type !== "naddr") {
throw new Error("Invalid list");
}
const { identifier, kind, pubkey } = data;
const { events } = useEvents({
filter: {
authors: [pubkey],
kinds: [kind],
["#d"]: [identifier],
limit: 1,
},
});
const event = events[0];
if (!event) {
return (
<div className="center pt-20 text-primary">
<Spinner />
</div>
);
}
const noteIds = getTagsValues("e", event.tags).filter(Boolean);
console.log("notes", noteIds);
console.log("tags", event.tags);
return (
<div className="relative mx-auto max-w-5xl space-y-4 p-2 sm:p-4">
<Header event={event} />
<div className="relative overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
<div className="space-y-3 overflow-hidden rounded-[0.5rem] p-0">
<Feed
filter={{
ids: noteIds,
}}
empty={() => (
<div className="text-center text-muted-foreground">
<p>No notes yet</p>
</div>
)}
/>
</div>
</div>
</div>
);
}

View File

@ -3,7 +3,7 @@ import Footer from "./Footer";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<main className="absolute inset-0 w-screen bg-white">
<main className="absolute inset-0 w-screen bg-white pb-20">
<Header />
<div className="relative z-0 flex shrink-0 grow flex-col justify-center">
<div className="flex-1">{children}</div>

View File

@ -16,6 +16,7 @@ import {
} from "@nostr-dev-kit/ndk";
import { useNDK } from "../ndk";
import { log } from "@/lib/utils";
import { getTagValues } from "@/lib/nostr/utils";
export type SignerStoreItem = {
signer: NDKPrivateKeySigner;
@ -52,7 +53,10 @@ export default function SignerProvider({
async function getDelegatedSignerName(list: NDKList) {
let name = "";
console.log("getDelegatedSignerName", list);
console.log("getDelegatedSignerName", JSON.stringify(list));
if (list.kind === 30044) {
return list.title;
}
if (!currentUser?.profile) {
console.log("fetching user profile");
await currentUser?.fetchProfile();
@ -64,7 +68,9 @@ export default function SignerProvider({
`${currentUser?.npub.slice(0, 9)}...`
}'s `;
return name + list.title ?? "List";
const suffix = list.kind === 30044 ? "Subscription" : "List";
return name + list.title ?? suffix;
}
async function getSigner(list: NDKList): Promise<SignerStoreItem> {
@ -80,7 +86,7 @@ export default function SignerProvider({
});
if (signer) {
log("info", `found a signer for list ${list.title}`, signer.toString());
log("info", `found a signer for ${list.title}`, JSON.stringify(signer));
item = {
signer: signer!,
user: await signer.user(),
@ -88,9 +94,10 @@ export default function SignerProvider({
id,
};
} else {
log("info", `no signer found for list ${list.title}`);
log("info", `no signer found for ${list.title}`);
signer = NDKPrivateKeySigner.generate();
log("info", "generated signer", signer.toString());
log("info", "generated signer", JSON.stringify(signer));
item = {
signer,
user: await signer.user(),

View File

@ -1,20 +1,21 @@
import Container from "./components/Container";
import { CardTitle, CardDescription } from "@/components/ui/card";
import { type Event } from "nostr-tools";
import { RenderText } from "../TextRendering";
import { getTagsValues } from "@/lib/nostr/utils";
import LinkCard from "@/components/LinkCard";
import { copyText } from "@/lib/utils";
import { nip19 } from "nostr-tools";
import { toast } from "sonner";
import { type KindCardProps } from "./";
export default function Kind1(props: Event) {
export default function Kind1(props: KindCardProps) {
const { content, pubkey, tags, created_at: createdAt } = props;
const r = getTagsValues("r", tags).filter(Boolean);
const npub = nip19.npubEncode(pubkey);
return (
<Container
event={props}
actionOptions={[
{
label: "View profile",

View File

@ -2,13 +2,13 @@
import Container from "./components/Container";
import { CardTitle, CardDescription } from "@/components/ui/card";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import { type Event } from "nostr-tools";
import { nip19 } from "nostr-tools";
import { removeDuplicates } from "@/lib/utils";
import { toast } from "sonner";
import { copyText } from "@/lib/utils";
import { type KindCardProps } from "./";
export default function Kind30023(props: Event) {
export default function Kind30023(props: KindCardProps) {
const { content, pubkey, tags, created_at: createdAt } = props;
const npub = nip19.npubEncode(pubkey);
const title = getTagValues("title", tags);

View File

@ -1,15 +1,15 @@
"use client";
import Container from "./components/Container";
import { CardTitle, CardDescription } from "@/components/ui/card";
import { type Event } from "nostr-tools";
import { nip19 } from "nostr-tools";
import { toast } from "sonner";
import { copyText } from "@/lib/utils";
import { RenderText } from "../TextRendering";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import ReactPlayer from "react-player";
import { type KindCardProps } from "./";
export default function Kind30311(props: Event) {
export default function Kind30311(props: KindCardProps) {
const { pubkey, tags } = props;
const streamingUrl =
getTagValues("streaming", tags) ?? getTagValues("recording", tags);

View File

@ -7,19 +7,32 @@ import { type Event } from "nostr-tools";
import { cn } from "@/lib/utils";
import { useNDK } from "@/app/_providers/ndk";
import { RiArrowRightLine, RiLockLine } from "react-icons/ri";
import { HiOutlineLockOpen } from "react-icons/hi";
import { decryptMessage } from "@/lib/nostr";
import { NDKUser } from "@nostr-dev-kit/ndk";
import { log } from "@/lib/utils";
import { EventSchema } from "@/types";
import KindCard from "@/components/KindCard";
import Spinner from "../spinner";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { unlockEvent } from "@/lib/actions/create";
import { type KindCardProps } from "./";
export default function Kind3745(props: Event) {
export default function Kind3745(props: KindCardProps) {
const { pubkey, content, id } = props;
const { currentUser } = useCurrentUser();
const [error, setError] = useState("");
const [passphrase, setPassphrase] = useState("");
const [fetchingEvent, setFetchingEvent] = useState(false);
const [decryptedEvent, setDecryptedEvent] = useState<Event>();
const { ndk } = useNDK();
const { ndk, fetchEvents } = useNDK();
useEffect(() => {
if (ndk && !fetchingEvent && !decryptedEvent) {
void handleFetchEvent();
@ -27,6 +40,9 @@ export default function Kind3745(props: Event) {
}, [ndk]);
async function handleFetchEvent() {
if (!ndk) return;
log("func", `handleFetchEvent(${pubkey})`);
setFetchingEvent(true);
try {
const directMessageEvent = await ndk!.fetchEvent({
@ -35,16 +51,19 @@ export default function Kind3745(props: Event) {
["#e"]: [id],
});
if (directMessageEvent) {
log("info", "direct msg decryption");
console.log(directMessageEvent);
await directMessageEvent.decrypt(
new NDKUser({ hexpubkey: pubkey }),
ndk!.signer,
);
const passphrase = directMessageEvent.content;
if (!passphrase) {
const passphrase_ = directMessageEvent.content;
if (!passphrase_) {
setError("Unable to parse event");
return;
}
const decrypedData = await decryptMessage(content, passphrase);
setPassphrase(passphrase_);
const decrypedData = await decryptMessage(content, passphrase_);
console.log("Decrypted", decrypedData);
const hiddenEvent = EventSchema.safeParse(
JSON.parse(decrypedData ?? ""),
@ -57,12 +76,48 @@ export default function Kind3745(props: Event) {
}
} catch (err) {
setError("Unable to parse event");
console.log("ERROR", err);
} finally {
setFetchingEvent(false);
}
}
async function handleUnlockEvent() {
await unlockEvent(ndk!, props, passphrase);
window.location.reload();
}
if (decryptedEvent) {
return <KindCard {...decryptedEvent} />;
return (
<div className="group relative">
<KindCard {...decryptedEvent} locked={true} />
{currentUser?.pubkey === decryptedEvent.pubkey && (
<div
className={cn(
"absolute bottom-[5px] right-[5px] opacity-20 transition-opacity group-hover:opacity-50",
"hover:!opacity-100",
)}
>
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger>
<Button
onClick={handleUnlockEvent}
size={"icon"}
className=""
>
<HiOutlineLockOpen className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent align="end">
<p>Unlock Content</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
);
}
return (
<Container event={props}>

View File

@ -11,8 +11,9 @@ import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import Actions from "./Actions";
import Tags from "./Tags";
import DropDownMenu from "@/components/DropDownMenu";
import { NostrEvent } from "@nostr-dev-kit/ndk";
import { removeDuplicates } from "@/lib/utils";
import { type KindCardProps } from "..";
type OptionLink = {
href: string;
type: "link";
@ -28,7 +29,7 @@ type Option = {
type CreatorCardProps = {
children: ReactNode;
actionOptions?: Option[];
event?: NostrEvent;
event?: KindCardProps;
};
export default function Container({
@ -60,20 +61,19 @@ export default function Container({
</Card>
);
}
const { pubkey, tags, created_at: createdAt } = event;
const { pubkey, tags, created_at: createdAt, locked } = event;
const contentTags = removeDuplicates(getTagsValues("t", tags)).filter(
Boolean,
);
return (
<Card
onClick={() => {
console.log("CLICK IN CONTAINER");
}}
className="relative flex h-full w-full flex-col overflow-hidden @container"
>
<Card className="relative flex h-full w-full flex-col overflow-hidden @container">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-4">
{pubkey ? <ProfileHeader pubkey={pubkey} /> : <LoadingProfileHeader />}
{pubkey ? (
<ProfileHeader pubkey={pubkey} locked={locked} />
) : (
<LoadingProfileHeader />
)}
<div className="-mr-1 flex items-center gap-x-1.5 text-xs text-muted-foreground">
{!!createdAt &&
formatDate(new Date(createdAt * 1000), "MMM Do, h:mm a")}

View File

@ -1,5 +1,5 @@
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { HiCheckBadge } from "react-icons/hi2";
import { HiCheckBadge, HiMiniKey } from "react-icons/hi2";
import Link from "next/link";
import useProfile from "@/lib/hooks/useProfile";
import { nip19 } from "nostr-tools";
@ -8,8 +8,9 @@ import { Skeleton } from "@/components/ui/skeleton";
type ProfileHeaderProps = {
pubkey: string;
locked?: boolean;
};
export default function ProfileHeader({ pubkey }: ProfileHeaderProps) {
export default function ProfileHeader({ pubkey, locked }: ProfileHeaderProps) {
const { profile } = useProfile(pubkey);
const npub = nip19.npubEncode(pubkey);
return (
@ -29,6 +30,7 @@ export default function ProfileHeader({ pubkey }: ProfileHeaderProps) {
{!!profile?.nip05 && (
<HiCheckBadge className="h-4 w-4 text-primary" />
)}
{!!locked && <HiMiniKey className="h-4 w-4 text-primary" />}
</div>
<div className="flex items-center gap-1">
{!!profile.nip05 && (

View File

@ -1,14 +1,14 @@
import Container from "./components/Container";
import { CardTitle, CardDescription } from "@/components/ui/card";
import { type Event } from "nostr-tools";
import { nip19 } from "nostr-tools";
import { toast } from "sonner";
import { copyText } from "@/lib/utils";
import { RenderText } from "../TextRendering";
import { getTagsValues } from "@/lib/nostr/utils";
import LinkCard from "@/components/LinkCard";
import { type KindCardProps } from "./";
export default function KindDefault(props: Event) {
export default function KindDefault(props: KindCardProps) {
const { pubkey, created_at: createdAt, tags } = props;
const r = getTagsValues("r", tags).filter(Boolean);

View File

@ -27,7 +27,9 @@ const componentMap: Record<number, ComponentType<KindCardProps>> = {
30311: KindCard30311,
};
type KindCardProps = Event<number>;
export type KindCardProps = Event<number> & {
locked?: boolean;
};
export default function KindCard(props: KindCardProps) {
const { kind } = props;
const KindCard_ = componentMap[kind] ?? KindCardDefault;

View File

@ -1,7 +1,6 @@
import Container from "./components/Container";
import { CardTitle, CardDescription } from "@/components/ui/card";
import { type Event } from "nostr-tools";
import { Skeleton } from "@/components/ui/skeleton";
import { type KindCardProps } from "./";
export default function KindLoading() {
return (

View File

@ -19,7 +19,7 @@ const CreateListSchema = z.object({
title: z.string(),
image: z.string().optional(),
description: z.string().optional(),
subscriptions: z.boolean(),
subscriptions: z.boolean().optional(),
price: z.number().optional(),
});

View File

@ -0,0 +1,134 @@
import { useState } from "react";
import FormModal from "./FormModal";
import { z } from "zod";
import { useModal } from "@/app/_providers/modal/provider";
import { toast } from "sonner";
import { randomId } from "@/lib/nostr";
import { satsToBtc } from "@/lib/utils";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { useNDK } from "@/app/_providers/ndk";
import { useSigner, type SignerStoreItem } from "@/app/_providers/signer";
import { createEvent, updateList } from "@/lib/actions/create";
import { getTagValues } from "@/lib/nostr/utils";
import { NDKList, NDKUser } from "@nostr-dev-kit/ndk";
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
import { useRouter } from "next/navigation";
import { log } from "@/lib/utils";
const CreateSubscriptionTierSchema = z.object({
title: z.string(),
image: z.string().optional(),
description: z.string().optional(),
price: z.number().optional(),
});
type CreateSubscriptionTierType = z.infer<typeof CreateSubscriptionTierSchema>;
export default function CreateSubscriptionTier() {
const [isLoading, setIsLoading] = useState(false);
const modal = useModal();
const router = useRouter();
const { currentUser, initSubscriptions } = useCurrentUser();
const { ndk } = useNDK();
const { getSigner } = useSigner()!;
async function handleSubmit(data: CreateSubscriptionTierType) {
if (!currentUser) return;
setIsLoading(true);
const random = randomId();
const tags = [
["title", data.title],
["name", data.title],
["d", random],
["p", currentUser!.pubkey, "", "self", "4000000000"],
];
if (data.description) {
tags.push(["description", data.description]);
tags.push(["summary", data.description]);
}
if (data.image) {
tags.push(["image", data.image]);
tags.push(["picture", data.image]);
}
if (data.price) {
tags.push(["price", satsToBtc(data.price).toString(), "btc", "year"]);
}
const event = await createEvent(ndk!, {
content: "[]",
kind: 30044,
tags: tags,
});
if (event) {
await getSigner(new NDKList(ndk, event.rawEvent()))
.then((delegateSigner) =>
saveEphemeralSigner(ndk!, delegateSigner.signer, {
associatedEvent: event,
keyProfile: {
name: delegateSigner.title,
picture: currentUser?.profile?.image,
lud06: currentUser?.profile?.lud06,
lud16: currentUser?.profile?.lud16,
},
}),
)
.then((savedSigner) => {
log("info", "savedSigner", savedSigner.toString());
currentUser.follow(
new NDKUser({
hexpubkey: savedSigner.pubkey,
}),
);
return updateList(ndk!, event.rawEvent(), [
["delegate", savedSigner.pubkey],
]);
})
.catch((err) => console.log("Error creating delegate"));
}
// getSubscriptionTiers(currentUser!.hexpubkey);
setIsLoading(false);
if (event) {
initSubscriptions(currentUser.pubkey);
toast.success("Subscription Tier Created!");
modal?.hide();
router.push(`/sub/${event.encode()}`);
} else {
toast.error("An error occured");
}
}
return (
<FormModal
title="Create Subscription Tier"
fields={[
{
label: "Tier name",
type: "input",
slug: "title",
},
{
label: "Description",
type: "text-area",
slug: "description",
},
{
label: "Image",
type: "input",
slug: "image",
},
{
label: "Price",
subtitle: "sats/year",
type: "number",
slug: "price",
},
]}
formSchema={CreateSubscriptionTierSchema}
onSubmit={handleSubmit}
isSubmitting={isLoading}
cta={{
text: "Create Subscription Tier",
}}
/>
);
}

View File

@ -12,6 +12,7 @@ import {
HiChatBubbleLeftEllipsis,
HiBookmarkSquare,
HiNewspaper,
HiUserGroup,
} from "react-icons/hi2";
import { RiSubtractFill, RiAddFill } from "react-icons/ri";
import { formatCount } from "@/lib/utils";

View File

@ -13,27 +13,26 @@ import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
import useLists from "@/lib/hooks/useLists";
import { getUrls } from "@/components/TextRendering";
import { log } from "@/lib/utils";
const ShortTextNoteSchema = z.object({
content: z.string(),
list: z.string().optional(),
isPrivate: z.boolean().optional(),
subscriptionTiers: z.string().array().optional(),
});
type ShortTextNoteType = z.infer<typeof ShortTextNoteSchema>;
export default function ShortTextNoteModal() {
const modal = useModal();
const { lists, init } = useLists();
const [isLoading, setIsLoading] = useState(false);
const { currentUser } = useCurrentUser();
const { currentUser, initSubscriptions, mySubscription } = useCurrentUser();
const [sent, setSent] = useState(false);
const { ndk } = useNDK();
const { getSigner } = useSigner()!;
useEffect(() => {
if (currentUser) {
void init(currentUser.pubkey);
void initSubscriptions(currentUser.pubkey);
}
}, [currentUser]);
@ -54,25 +53,24 @@ export default function ShortTextNoteModal() {
}
const urls = getUrls(data.content);
const urlTags = urls.map((u) => ["r", u]);
if (data.list) {
const list = lists.find((l) => getTagValues("d", l.tags) === data.list);
if (!list) {
if (data.isPrivate) {
if (!mySubscription) {
toast.error("No list found");
return;
}
let listSigner: SignerStoreItem | undefined = undefined;
let delegateSigner: SignerStoreItem | undefined = undefined;
if (data.isPrivate) {
listSigner = await getSigner(list);
if (!listSigner?.signer) {
delegateSigner = await getSigner(mySubscription);
if (!delegateSigner?.signer) {
toast.error("Error creating signer");
return;
}
if (!listSigner?.saved) {
if (!delegateSigner?.saved) {
console.log("Saving delegate...");
await saveEphemeralSigner(ndk!, listSigner.signer, {
associatedEvent: list,
await saveEphemeralSigner(ndk!, delegateSigner.signer, {
associatedEvent: mySubscription,
keyProfile: {
name: listSigner.title,
name: delegateSigner.title,
picture: currentUser?.profile?.image,
lud06: currentUser?.profile?.lud06,
lud16: currentUser?.profile?.lud16,
@ -80,7 +78,11 @@ export default function ShortTextNoteModal() {
});
}
}
console.log("about to create private event with ", listSigner);
log(
"info",
"about to create private event with ",
JSON.stringify(delegateSigner),
);
const result = await createEventHandler(
ndk,
{
@ -89,23 +91,19 @@ export default function ShortTextNoteModal() {
tags: [...urlTags],
},
data.isPrivate,
list,
listSigner?.signer,
mySubscription,
delegateSigner?.signer,
);
if (result) {
toast.success("Note added!");
modal?.hide();
}
} else {
const result = await createEventHandler(
ndk,
{
content: data.content,
kind: 1,
tags: [...urlTags],
},
data.isPrivate,
);
const result = await createEventHandler(ndk, {
content: data.content,
kind: 1,
tags: [...urlTags],
});
if (result) {
toast.success("Note added!");
modal?.hide();
@ -122,33 +120,39 @@ export default function ShortTextNoteModal() {
type: "text-area",
slug: "content",
},
{
label: "Add to list",
type: "select-search",
placeholder: "Search your lists",
slug: "list",
options: lists
.map((l) => {
const title =
getTagValues("title", l.tags) ??
getTagValues("name", l.tags) ??
"Untitled";
const description = getTagValues("description", l.tags);
const value = getTagValues("d", l.tags);
if (!value) return;
return {
label: title,
description,
value: value,
};
})
.filter(Boolean),
},
{
label: "Private",
type: "toggle",
slug: "isPrivate",
},
...(mySubscription
? ([
{
label: "Subs only",
type: "toggle",
slug: "isPrivate",
},
] as const)
: []),
// {
// label: "Choose Tiers",
// type: "select-search",
// placeholder: "Search your lists",
// slug: "subscriptionTiers",
// options: lists
// .map((l) => {
// const title =
// getTagValues("title", l.tags) ??
// getTagValues("name", l.tags) ??
// "Untitled";
// const description = getTagValues("description", l.tags);
// const value = getTagValues("d", l.tags);
// if (!value) return;
// return {
// label: title,
// description,
// value: value,
// };
// })
// .filter(Boolean),
// condition: "subscriptions",
// },
]}
formSchema={ShortTextNoteSchema}
onSubmit={handleSubmit}

View File

@ -0,0 +1,161 @@
import { useEffect, useState } from "react";
import FormModal from "./FormModal";
import { z } from "zod";
import useEvents from "@/lib/hooks/useEvents";
import { createEventHandler } from "@/lib/actions/create";
import { unixTimeNowInSeconds } from "@/lib/nostr/dates";
import { useModal } from "@/app/_providers/modal/provider";
import { toast } from "sonner";
import { useNDK } from "@/app/_providers/ndk";
import { useSigner, type SignerStoreItem } from "@/app/_providers/signer";
import { getTagValues } from "@/lib/nostr/utils";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
import useLists from "@/lib/hooks/useLists";
import { getUrls } from "@/components/TextRendering";
const ShortTextNoteSchema = z.object({
content: z.string(),
list: z.string().optional(),
isPrivate: z.boolean().optional(),
});
type ShortTextNoteType = z.infer<typeof ShortTextNoteSchema>;
export default function ShortTextNoteModal() {
const modal = useModal();
const { lists, init } = useLists();
const [isLoading, setIsLoading] = useState(false);
const { currentUser } = useCurrentUser();
const [sent, setSent] = useState(false);
const { ndk } = useNDK();
const { getSigner } = useSigner()!;
useEffect(() => {
if (currentUser) {
void init(currentUser.pubkey);
}
}, [currentUser]);
// useEffect(() => {
// if (events.length) {
// console.log("Done!");
// setIsLoading(false);
// toast.success("List Updated!");
// modal?.hide();
// }
// }, [events]);
async function handleSubmit(data: ShortTextNoteType) {
setIsLoading(true);
if (!ndk) {
toast.error("Error connecting");
return;
}
const urls = getUrls(data.content);
const urlTags = urls.map((u) => ["r", u]);
if (data.list) {
const list = lists.find((l) => getTagValues("d", l.tags) === data.list);
if (!list) {
toast.error("No list found");
return;
}
let listSigner: SignerStoreItem | undefined = undefined;
if (data.isPrivate) {
listSigner = await getSigner(list);
if (!listSigner?.signer) {
toast.error("Error creating signer");
return;
}
if (!listSigner?.saved) {
console.log("Saving delegate...");
await saveEphemeralSigner(ndk!, listSigner.signer, {
associatedEvent: list,
keyProfile: {
name: listSigner.title,
picture: currentUser?.profile?.image,
lud06: currentUser?.profile?.lud06,
lud16: currentUser?.profile?.lud16,
},
});
}
}
console.log("about to create private event with ", listSigner);
const result = await createEventHandler(
ndk,
{
content: data.content,
kind: 1,
tags: [...urlTags],
},
data.isPrivate,
list,
listSigner?.signer,
);
if (result) {
toast.success("Note added!");
modal?.hide();
}
} else {
const result = await createEventHandler(
ndk,
{
content: data.content,
kind: 1,
tags: [...urlTags],
},
data.isPrivate,
);
if (result) {
toast.success("Note added!");
modal?.hide();
}
}
}
return (
<FormModal
title="Short Text Note"
fields={[
{
label: "Content",
type: "text-area",
slug: "content",
},
{
label: "Add to list",
type: "select-search",
placeholder: "Search your lists",
slug: "list",
options: lists
.map((l) => {
const title =
getTagValues("title", l.tags) ??
getTagValues("name", l.tags) ??
"Untitled";
const description = getTagValues("description", l.tags);
const value = getTagValues("d", l.tags);
if (!value) return;
return {
label: title,
description,
value: value,
};
})
.filter(Boolean),
},
{
label: "Private",
type: "toggle",
slug: "isPrivate",
},
]}
formSchema={ShortTextNoteSchema}
onSubmit={handleSubmit}
isSubmitting={isLoading}
cta={{
text: "Publish",
}}
/>
);
}

View File

@ -1,4 +1,6 @@
import { useState, useEffect } from "react";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Card,
@ -8,28 +10,79 @@ import {
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { HiOutlineCheckBadge } from "react-icons/hi2";
import { NDKEvent, NDKUser, NostrEvent } from "@nostr-dev-kit/ndk";
import { useNDK } from "@/app/_providers/ndk";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { toast } from "sonner";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import { sendZap, checkPayment } from "@/lib/actions/zap";
import { btcToSats, formatNumber } from "@/lib/utils";
type SubscriptionCardProps = {
id: string;
title: string;
picture: string;
description: string;
tags: string[];
};
export default function SubscriptionCard({
picture,
title,
tags,
description,
}: SubscriptionCardProps) {
export default function SubscriptionCard({ event }: { event: NDKEvent }) {
const { currentUser } = useCurrentUser();
const { ndk } = useNDK();
const [checkingPayment, setCheckingPayment] = useState(false);
const [hasValidPayment, setHasValidPayment] = useState(false);
const { tags } = event;
const rawEvent = event.rawEvent();
const title = getTagValues("title", tags) ?? getTagValues("name", tags) ?? "";
const image =
getTagValues("image", tags) ?? getTagValues("picture", tags) ?? "";
const description =
getTagValues("description", tags) ?? getTagValues("summary", tags) ?? "";
const delegate = getTagValues("delegate", tags);
const priceInBTC = parseFloat(getTagValues("price", rawEvent.tags) ?? "0");
async function handleSubscribe() {
try {
if (!currentUser) return;
await currentUser.follow(
new NDKUser({
hexpubkey: delegate,
}),
);
const result = await sendZap(
ndk!,
btcToSats(priceInBTC),
rawEvent,
`Access payment: ${title}`,
);
toast.success("Payment Sent!");
void handleCheckPayment();
} catch (err) {
console.log("error sending zap", err);
} finally {
}
}
async function handleCheckPayment() {
if (!event || !currentUser || !ndk) return;
setCheckingPayment(true);
console.log("Checking payment");
try {
const result = await checkPayment(
ndk,
event.tagId(),
currentUser.pubkey,
rawEvent,
);
console.log("Payment result", result);
if (result) {
setHasValidPayment(true);
}
} catch (err) {
console.log("error sending zap", err);
} finally {
setCheckingPayment(false);
}
}
return (
<Card className="group sm:flex">
<div className="overflow-hidden max-sm:h-[100px] max-sm:rounded-t-md sm:w-[250px] sm:rounded-l-md">
<Image
width={250}
height={150}
src={picture}
src={image}
alt={title}
unoptimized
className={cn(
@ -45,10 +98,26 @@ export default function SubscriptionCard({
</CardDescription>
</CardHeader>
<CardContent className="items-strech flex w-full flex-col items-center gap-2 sm:max-w-md sm:flex-row sm:gap-4">
<Button className="w-full">Buy now</Button>
<Button variant={"secondary"} className="w-full">
Details
</Button>
{hasValidPayment ? (
<Button disabled={true} variant={"ghost"} className="w-full">
Pending sync
</Button>
) : (
<>
<Button
loading={checkingPayment}
onClick={handleSubscribe}
className="w-full"
>
Join now
</Button>
<Link href={`/sub/${event.encode()}`}>
<Button variant={"secondary"} className="w-full">
Details
</Button>
</Link>
</>
)}
</CardContent>
</div>
</Card>

View File

@ -7,7 +7,12 @@ import NDK, {
type NostrEvent,
NDKUser,
} from "@nostr-dev-kit/ndk";
import { generateRandomString, encryptMessage, randomId } from "@/lib/nostr";
import { EventSchema } from "@/types";
import {
generateRandomString,
encryptMessage,
decryptMessage,
} from "@/lib/nostr";
import { unixTimeNowInSeconds } from "@/lib/nostr/dates";
import { getTagsValues } from "@/lib/nostr/utils";
import { log } from "@/lib/utils";
@ -262,3 +267,16 @@ export async function updateList(
tags: tags.filter(([_, value]) => value !== undefined),
});
}
export async function unlockEvent(
ndk: NDK,
event: NostrEvent,
passphrase: string,
) {
const decrypedData = await decryptMessage(event.content, passphrase);
const hiddenEvent = EventSchema.parse(JSON.parse(decrypedData ?? ""));
// Create New public event
const publishedEvent = await new NDKEvent(ndk, hiddenEvent).publish();
await deleteEvent(ndk, [["e", event.id ?? ""]], "Content unlocked");
return publishedEvent;
}

View File

@ -1,21 +1,24 @@
"use client";
import { useEffect } from "react";
import currentUserStore from "@/lib/stores/currentUser";
// import useEvents from "@/lib/hooks/useEvents";
import { UserSchema } from "@/types";
import { useNDK } from "@/app/_providers/ndk";
import { nip19 } from "nostr-tools";
import useLists from "./useLists";
import useSubscriptions from "./useSubscriptions";
export default function useCurrentUser() {
const {
currentUser,
setCurrentUser,
setFollows,
updateCurrentUser,
follows,
setFollows,
} = currentUserStore();
const { loginWithNip07, getProfile, ndk } = useNDK();
const { loginWithNip07, getProfile, ndk, fetchEvents } = useNDK();
const { init } = useLists();
const { init: initSubscriptions, mySubscription } = useSubscriptions();
async function attemptLogin() {
try {
const shouldReconnect = localStorage.getItem("shouldReconnect");
@ -24,8 +27,9 @@ export default function useCurrentUser() {
if (!user) {
throw new Error("NO auth");
}
console.log("LOGIN", user);
await loginWithPubkey(nip19.decode(user.npub).data.toString());
const pubkey = nip19.decode(user.npub).data.toString();
await loginWithPubkey(pubkey);
void initSubscriptions(pubkey);
if (typeof window.webln !== "undefined") {
await window.webln.enable();
}
@ -66,6 +70,15 @@ export default function useCurrentUser() {
void init(user.pubkey);
}
useEffect(() => {
if (!currentUser) return;
console.log("fetching follows");
(async () => {
const following = await currentUser.follows();
setFollows(following);
})();
}, [currentUser]);
return {
currentUser,
isLoading: false,
@ -75,5 +88,7 @@ export default function useCurrentUser() {
updateUser: handleUpdateUser,
loginWithPubkey,
attemptLogin,
initSubscriptions,
mySubscription,
};
}

View File

@ -0,0 +1,35 @@
"use client";
import subscriptionsStore from "@/lib/stores/subscriptions";
import { useNDK } from "@/app/_providers/ndk";
import { useState } from "react";
import { NDKList, NDKKind } from "@nostr-dev-kit/ndk";
export default function useSubscriptions() {
const [isLoading, setIsLoading] = useState(false);
const { mySubscription, setMySubscription } = subscriptionsStore();
const { fetchEvents, ndk } = useNDK();
async function init(pubkey: string) {
setIsLoading(true);
try {
const subscriptionLists = await fetchEvents({
kinds: [30044 as NDKKind],
authors: [pubkey],
});
if (subscriptionLists[0]) {
setMySubscription(new NDKList(ndk, subscriptionLists[0].rawEvent()));
}
} catch (err) {
console.log("error in init", err);
} finally {
setIsLoading(false);
}
}
return {
setMySubscription,
isLoading,
init,
mySubscription,
};
}

View File

@ -5,16 +5,16 @@ type Settings = {};
interface CurrentUserState {
currentUser: NDKUser | null;
follows: string[];
follows: Set<NDKUser>;
settings: Settings;
setCurrentUser: (user: NDKUser | null) => void;
updateCurrentUser: (user: Partial<NDKUser>) => void;
setFollows: (follows: string[]) => void;
setFollows: (follows: Set<NDKUser>) => void;
}
const currentUserStore = create<CurrentUserState>()((set) => ({
currentUser: null,
follows: [],
follows: new Set(),
settings: {},
setCurrentUser: (user) => set((state) => ({ ...state, currentUser: user })),
updateCurrentUser: (user) =>

View File

@ -0,0 +1,15 @@
import { create } from "zustand";
import { type NDKList } from "@nostr-dev-kit/ndk";
interface SubscriptionsState {
mySubscription: NDKList | undefined;
setMySubscription: (sub: NDKList) => void;
}
const subscriptionsStore = create<SubscriptionsState>()((set) => ({
mySubscription: undefined,
setMySubscription: (sub) =>
set((state) => ({ ...state, mySubscription: sub })),
}));
export default subscriptionsStore;