adding subs
This commit is contained in:
parent
a3aa61cd29
commit
3039083bd9
44
app/(app)/(profile)/[npub]/_components/MySubscription.tsx
Normal file
44
app/(app)/(profile)/[npub]/_components/MySubscription.tsx
Normal 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} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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,9 +104,16 @@ export default function ProfilePage({
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
{!follows.includes(pubkey) && (
|
||||
{currentUser &&
|
||||
!Array.from(follows).find((i) => i.pubkey === pubkey) && (
|
||||
<Button
|
||||
onClick={() => modal?.show(<EditProfileModal />)}
|
||||
onClick={() =>
|
||||
void currentUser.follow(
|
||||
new NDKUser({
|
||||
hexpubkey: pubkey,
|
||||
}),
|
||||
)
|
||||
}
|
||||
variant={"default"}
|
||||
className="rounded-sm px-5 max-sm:h-8 max-sm:text-xs"
|
||||
>
|
||||
@ -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
|
||||
|
@ -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"], () => {
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
|
@ -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"), {
|
||||
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
|
||||
|
224
app/(app)/sub/[naddr]/_components/Header.tsx
Normal file
224
app/(app)/sub/[naddr]/_components/Header.tsx
Normal 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>
|
||||
);
|
||||
}
|
44
app/(app)/sub/[naddr]/_components/ProfileInfo.tsx
Normal file
44
app/(app)/sub/[naddr]/_components/ProfileInfo.tsx
Normal 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>
|
||||
);
|
||||
}
|
71
app/(app)/sub/[naddr]/page.tsx
Normal file
71
app/(app)/sub/[naddr]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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(),
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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}>
|
||||
|
@ -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")}
|
||||
|
@ -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 && (
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 (
|
||||
|
@ -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(),
|
||||
});
|
||||
|
||||
|
134
components/Modals/CreateSubscriptionTier.tsx
Normal file
134
components/Modals/CreateSubscriptionTier.tsx
Normal 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",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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";
|
||||
|
@ -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,
|
||||
{
|
||||
const result = await createEventHandler(ndk, {
|
||||
content: data.content,
|
||||
kind: 1,
|
||||
tags: [...urlTags],
|
||||
},
|
||||
data.isPrivate,
|
||||
);
|
||||
});
|
||||
if (result) {
|
||||
toast.success("Note added!");
|
||||
modal?.hide();
|
||||
@ -122,33 +120,39 @@ export default function ShortTextNoteModal() {
|
||||
type: "text-area",
|
||||
slug: "content",
|
||||
},
|
||||
...(mySubscription
|
||||
? ([
|
||||
{
|
||||
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",
|
||||
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}
|
||||
|
161
components/Modals/ShortTextNoteOnList.tsx
Normal file
161
components/Modals/ShortTextNoteOnList.tsx
Normal 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",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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>
|
||||
{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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
35
lib/hooks/useSubscriptions.ts
Normal file
35
lib/hooks/useSubscriptions.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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) =>
|
||||
|
15
lib/stores/subscriptions.ts
Normal file
15
lib/stores/subscriptions.ts
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user