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";
|
"use client";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import SubscriptionCard from "@/components/SubscriptionCard";
|
|
||||||
import { HiCheckBadge } from "react-icons/hi2";
|
import { HiCheckBadge } from "react-icons/hi2";
|
||||||
import Tabs from "@/components/Tabs";
|
import Tabs from "@/components/Tabs";
|
||||||
import useProfile from "@/lib/hooks/useProfile";
|
import useProfile from "@/lib/hooks/useProfile";
|
||||||
@ -11,9 +11,23 @@ import ProfileFeed from "./_components/Feed";
|
|||||||
import Subscriptions from "./_components/Subscriptions";
|
import Subscriptions from "./_components/Subscriptions";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import useLists from "@/lib/hooks/useLists";
|
import useLists from "@/lib/hooks/useLists";
|
||||||
import EditProfileModal from "@/components/Modals/EditProfile";
|
|
||||||
import { useModal } from "@/app/_providers/modal/provider";
|
import { useModal } from "@/app/_providers/modal/provider";
|
||||||
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
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({
|
export default function ProfilePage({
|
||||||
params: { npub },
|
params: { npub },
|
||||||
}: {
|
}: {
|
||||||
@ -22,7 +36,7 @@ export default function ProfilePage({
|
|||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const { currentUser, follows } = useCurrentUser();
|
const { currentUser, mySubscription, follows } = useCurrentUser();
|
||||||
const [activeTab, setActiveTab] = useState("feed");
|
const [activeTab, setActiveTab] = useState("feed");
|
||||||
const { type, data } = nip19.decode(npub);
|
const { type, data } = nip19.decode(npub);
|
||||||
|
|
||||||
@ -31,7 +45,6 @@ export default function ProfilePage({
|
|||||||
}
|
}
|
||||||
const pubkey = data.toString();
|
const pubkey = data.toString();
|
||||||
const { profile } = useProfile(pubkey);
|
const { profile } = useProfile(pubkey);
|
||||||
const { init, lists } = useLists();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative mx-auto max-w-5xl space-y-6">
|
<div className="relative mx-auto max-w-5xl space-y-6">
|
||||||
@ -74,6 +87,14 @@ export default function ProfilePage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<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 && (
|
{currentUser?.pubkey === pubkey && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => modal?.show(<EditProfileModal />)}
|
onClick={() => modal?.show(<EditProfileModal />)}
|
||||||
@ -83,15 +104,22 @@ export default function ProfilePage({
|
|||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!follows.includes(pubkey) && (
|
{currentUser &&
|
||||||
<Button
|
!Array.from(follows).find((i) => i.pubkey === pubkey) && (
|
||||||
onClick={() => modal?.show(<EditProfileModal />)}
|
<Button
|
||||||
variant={"default"}
|
onClick={() =>
|
||||||
className="rounded-sm px-5 max-sm:h-8 max-sm:text-xs"
|
void currentUser.follow(
|
||||||
>
|
new NDKUser({
|
||||||
Follow
|
hexpubkey: pubkey,
|
||||||
</Button>
|
}),
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
variant={"default"}
|
||||||
|
className="rounded-sm px-5 max-sm:h-8 max-sm:text-xs"
|
||||||
|
>
|
||||||
|
Follow
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto max-w-[800px] space-y-1 px-4">
|
<div className="mx-auto max-w-[800px] space-y-1 px-4">
|
||||||
@ -123,9 +151,7 @@ export default function ProfilePage({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mx-auto max-w-[800px] space-y-6">
|
<div className="mx-auto max-w-[800px] space-y-6">
|
||||||
<div className="flex max-w-2xl flex-col gap-4 px-4">
|
<div className="flex max-w-2xl flex-col gap-4 px-4">
|
||||||
{/* {[].map((e) => (
|
<MySubscription pubkey={pubkey} />
|
||||||
<SubscriptionCard key={e.id} {...e} />
|
|
||||||
))} */}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<Tabs
|
<Tabs
|
||||||
|
@ -37,7 +37,8 @@ const LoginModal = dynamic(() => import("@/components/Modals/Login"), {
|
|||||||
export default function AuthActions() {
|
export default function AuthActions() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const { currentUser, logout, attemptLogin } = useCurrentUser();
|
const { currentUser, logout, attemptLogin, initSubscriptions } =
|
||||||
|
useCurrentUser();
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
|
|
||||||
useKeyboardShortcut(["shift", "ctrl", "u"], () => {
|
useKeyboardShortcut(["shift", "ctrl", "u"], () => {
|
||||||
|
@ -20,7 +20,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="relative flex flex-1 shrink-0 grow justify-center overflow-x-hidden">
|
<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>
|
</div>
|
||||||
{/* Mobile Banner */}
|
{/* Mobile Banner */}
|
||||||
<MobileBanner />
|
<MobileBanner />
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
"use client";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useModal } from "@/app/_providers/modal/provider";
|
||||||
|
import CreateSubscriptionTier from "@/components/Modals/CreateSubscriptionTier";
|
||||||
|
|
||||||
export default function BecomeACreator() {
|
export default function BecomeACreator() {
|
||||||
|
const modal = useModal();
|
||||||
return (
|
return (
|
||||||
<div className="padded-container overflow-x-clip pb-8 max-sm:-mx-5 md:py-[120px]">
|
<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">
|
<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
|
Start earning on Nostr
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mb-6 mt-2 text-muted-foreground">
|
<div className="mb-6 mt-2 text-muted-foreground">
|
||||||
Start earning bitcoin from your content. Create lists that for fans
|
Create a subscrition tier so you can start offering your users
|
||||||
can subscribe to!
|
access to private content!
|
||||||
</div>
|
</div>
|
||||||
<Button>Become a Creator</Button>
|
<Button onClick={() => modal?.show(<CreateSubscriptionTier />)}>
|
||||||
|
Become a Creator
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,9 +23,12 @@ import { formatDate } from "@/lib/utils/dates";
|
|||||||
const EditListModal = dynamic(() => import("@/components/Modals/EditList"), {
|
const EditListModal = dynamic(() => import("@/components/Modals/EditList"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
const CreateEventModal = dynamic(() => import("@/components/Modals/NewEvent"), {
|
const CreateListEvent = dynamic(
|
||||||
ssr: false,
|
() => import("@/components/Modals/ShortTextNoteOnList"),
|
||||||
});
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
const ConfirmModal = dynamic(() => import("@/components/Modals/Confirm"), {
|
const ConfirmModal = dynamic(() => import("@/components/Modals/Confirm"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
@ -151,7 +154,7 @@ export default function Header({ event }: { event: NDKEvent }) {
|
|||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{!!currentUser && currentUser.pubkey === pubkey && (
|
{!!currentUser && currentUser.pubkey === pubkey && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => modal?.show(<CreateEventModal />)}>
|
<Button onClick={() => modal?.show(<CreateListEvent />)}>
|
||||||
Add Event
|
Add Event
|
||||||
</Button>
|
</Button>
|
||||||
<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 }) {
|
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<main className="absolute inset-0 w-screen bg-white">
|
<main className="absolute inset-0 w-screen bg-white pb-20">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="relative z-0 flex shrink-0 grow flex-col justify-center">
|
<div className="relative z-0 flex shrink-0 grow flex-col justify-center">
|
||||||
<div className="flex-1">{children}</div>
|
<div className="flex-1">{children}</div>
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
} from "@nostr-dev-kit/ndk";
|
} from "@nostr-dev-kit/ndk";
|
||||||
import { useNDK } from "../ndk";
|
import { useNDK } from "../ndk";
|
||||||
import { log } from "@/lib/utils";
|
import { log } from "@/lib/utils";
|
||||||
|
import { getTagValues } from "@/lib/nostr/utils";
|
||||||
|
|
||||||
export type SignerStoreItem = {
|
export type SignerStoreItem = {
|
||||||
signer: NDKPrivateKeySigner;
|
signer: NDKPrivateKeySigner;
|
||||||
@ -52,7 +53,10 @@ export default function SignerProvider({
|
|||||||
|
|
||||||
async function getDelegatedSignerName(list: NDKList) {
|
async function getDelegatedSignerName(list: NDKList) {
|
||||||
let name = "";
|
let name = "";
|
||||||
console.log("getDelegatedSignerName", list);
|
console.log("getDelegatedSignerName", JSON.stringify(list));
|
||||||
|
if (list.kind === 30044) {
|
||||||
|
return list.title;
|
||||||
|
}
|
||||||
if (!currentUser?.profile) {
|
if (!currentUser?.profile) {
|
||||||
console.log("fetching user profile");
|
console.log("fetching user profile");
|
||||||
await currentUser?.fetchProfile();
|
await currentUser?.fetchProfile();
|
||||||
@ -64,7 +68,9 @@ export default function SignerProvider({
|
|||||||
`${currentUser?.npub.slice(0, 9)}...`
|
`${currentUser?.npub.slice(0, 9)}...`
|
||||||
}'s `;
|
}'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> {
|
async function getSigner(list: NDKList): Promise<SignerStoreItem> {
|
||||||
@ -80,7 +86,7 @@ export default function SignerProvider({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (signer) {
|
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 = {
|
item = {
|
||||||
signer: signer!,
|
signer: signer!,
|
||||||
user: await signer.user(),
|
user: await signer.user(),
|
||||||
@ -88,9 +94,10 @@ export default function SignerProvider({
|
|||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
log("info", `no signer found for list ${list.title}`);
|
log("info", `no signer found for ${list.title}`);
|
||||||
|
|
||||||
signer = NDKPrivateKeySigner.generate();
|
signer = NDKPrivateKeySigner.generate();
|
||||||
log("info", "generated signer", signer.toString());
|
log("info", "generated signer", JSON.stringify(signer));
|
||||||
item = {
|
item = {
|
||||||
signer,
|
signer,
|
||||||
user: await signer.user(),
|
user: await signer.user(),
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import Container from "./components/Container";
|
import Container from "./components/Container";
|
||||||
import { CardTitle, CardDescription } from "@/components/ui/card";
|
import { CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { type Event } from "nostr-tools";
|
|
||||||
import { RenderText } from "../TextRendering";
|
import { RenderText } from "../TextRendering";
|
||||||
import { getTagsValues } from "@/lib/nostr/utils";
|
import { getTagsValues } from "@/lib/nostr/utils";
|
||||||
import LinkCard from "@/components/LinkCard";
|
import LinkCard from "@/components/LinkCard";
|
||||||
import { copyText } from "@/lib/utils";
|
import { copyText } from "@/lib/utils";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { toast } from "sonner";
|
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 { content, pubkey, tags, created_at: createdAt } = props;
|
||||||
const r = getTagsValues("r", tags).filter(Boolean);
|
const r = getTagsValues("r", tags).filter(Boolean);
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
|
event={props}
|
||||||
actionOptions={[
|
actionOptions={[
|
||||||
{
|
{
|
||||||
label: "View profile",
|
label: "View profile",
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
import Container from "./components/Container";
|
import Container from "./components/Container";
|
||||||
import { CardTitle, CardDescription } from "@/components/ui/card";
|
import { CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
||||||
import { type Event } from "nostr-tools";
|
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { removeDuplicates } from "@/lib/utils";
|
import { removeDuplicates } from "@/lib/utils";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { copyText } from "@/lib/utils";
|
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 { content, pubkey, tags, created_at: createdAt } = props;
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
const title = getTagValues("title", tags);
|
const title = getTagValues("title", tags);
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Container from "./components/Container";
|
import Container from "./components/Container";
|
||||||
import { CardTitle, CardDescription } from "@/components/ui/card";
|
import { CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { type Event } from "nostr-tools";
|
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { copyText } from "@/lib/utils";
|
import { copyText } from "@/lib/utils";
|
||||||
import { RenderText } from "../TextRendering";
|
import { RenderText } from "../TextRendering";
|
||||||
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
||||||
import ReactPlayer from "react-player";
|
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 { pubkey, tags } = props;
|
||||||
const streamingUrl =
|
const streamingUrl =
|
||||||
getTagValues("streaming", tags) ?? getTagValues("recording", tags);
|
getTagValues("streaming", tags) ?? getTagValues("recording", tags);
|
||||||
|
@ -7,19 +7,32 @@ import { type Event } from "nostr-tools";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useNDK } from "@/app/_providers/ndk";
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
import { RiArrowRightLine, RiLockLine } from "react-icons/ri";
|
import { RiArrowRightLine, RiLockLine } from "react-icons/ri";
|
||||||
|
import { HiOutlineLockOpen } from "react-icons/hi";
|
||||||
import { decryptMessage } from "@/lib/nostr";
|
import { decryptMessage } from "@/lib/nostr";
|
||||||
import { NDKUser } from "@nostr-dev-kit/ndk";
|
import { NDKUser } from "@nostr-dev-kit/ndk";
|
||||||
|
import { log } from "@/lib/utils";
|
||||||
import { EventSchema } from "@/types";
|
import { EventSchema } from "@/types";
|
||||||
import KindCard from "@/components/KindCard";
|
import KindCard from "@/components/KindCard";
|
||||||
import Spinner from "../spinner";
|
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 { pubkey, content, id } = props;
|
||||||
|
const { currentUser } = useCurrentUser();
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [passphrase, setPassphrase] = useState("");
|
||||||
const [fetchingEvent, setFetchingEvent] = useState(false);
|
const [fetchingEvent, setFetchingEvent] = useState(false);
|
||||||
const [decryptedEvent, setDecryptedEvent] = useState<Event>();
|
const [decryptedEvent, setDecryptedEvent] = useState<Event>();
|
||||||
const { ndk } = useNDK();
|
const { ndk, fetchEvents } = useNDK();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ndk && !fetchingEvent && !decryptedEvent) {
|
if (ndk && !fetchingEvent && !decryptedEvent) {
|
||||||
void handleFetchEvent();
|
void handleFetchEvent();
|
||||||
@ -27,6 +40,9 @@ export default function Kind3745(props: Event) {
|
|||||||
}, [ndk]);
|
}, [ndk]);
|
||||||
|
|
||||||
async function handleFetchEvent() {
|
async function handleFetchEvent() {
|
||||||
|
if (!ndk) return;
|
||||||
|
log("func", `handleFetchEvent(${pubkey})`);
|
||||||
|
|
||||||
setFetchingEvent(true);
|
setFetchingEvent(true);
|
||||||
try {
|
try {
|
||||||
const directMessageEvent = await ndk!.fetchEvent({
|
const directMessageEvent = await ndk!.fetchEvent({
|
||||||
@ -35,16 +51,19 @@ export default function Kind3745(props: Event) {
|
|||||||
["#e"]: [id],
|
["#e"]: [id],
|
||||||
});
|
});
|
||||||
if (directMessageEvent) {
|
if (directMessageEvent) {
|
||||||
|
log("info", "direct msg decryption");
|
||||||
|
console.log(directMessageEvent);
|
||||||
await directMessageEvent.decrypt(
|
await directMessageEvent.decrypt(
|
||||||
new NDKUser({ hexpubkey: pubkey }),
|
new NDKUser({ hexpubkey: pubkey }),
|
||||||
ndk!.signer,
|
ndk!.signer,
|
||||||
);
|
);
|
||||||
const passphrase = directMessageEvent.content;
|
const passphrase_ = directMessageEvent.content;
|
||||||
if (!passphrase) {
|
if (!passphrase_) {
|
||||||
setError("Unable to parse event");
|
setError("Unable to parse event");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const decrypedData = await decryptMessage(content, passphrase);
|
setPassphrase(passphrase_);
|
||||||
|
const decrypedData = await decryptMessage(content, passphrase_);
|
||||||
console.log("Decrypted", decrypedData);
|
console.log("Decrypted", decrypedData);
|
||||||
const hiddenEvent = EventSchema.safeParse(
|
const hiddenEvent = EventSchema.safeParse(
|
||||||
JSON.parse(decrypedData ?? ""),
|
JSON.parse(decrypedData ?? ""),
|
||||||
@ -57,12 +76,48 @@ export default function Kind3745(props: Event) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Unable to parse event");
|
setError("Unable to parse event");
|
||||||
|
console.log("ERROR", err);
|
||||||
} finally {
|
} finally {
|
||||||
setFetchingEvent(false);
|
setFetchingEvent(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleUnlockEvent() {
|
||||||
|
await unlockEvent(ndk!, props, passphrase);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
if (decryptedEvent) {
|
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 (
|
return (
|
||||||
<Container event={props}>
|
<Container event={props}>
|
||||||
|
@ -11,8 +11,9 @@ import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
|||||||
import Actions from "./Actions";
|
import Actions from "./Actions";
|
||||||
import Tags from "./Tags";
|
import Tags from "./Tags";
|
||||||
import DropDownMenu from "@/components/DropDownMenu";
|
import DropDownMenu from "@/components/DropDownMenu";
|
||||||
import { NostrEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import { removeDuplicates } from "@/lib/utils";
|
import { removeDuplicates } from "@/lib/utils";
|
||||||
|
import { type KindCardProps } from "..";
|
||||||
|
|
||||||
type OptionLink = {
|
type OptionLink = {
|
||||||
href: string;
|
href: string;
|
||||||
type: "link";
|
type: "link";
|
||||||
@ -28,7 +29,7 @@ type Option = {
|
|||||||
type CreatorCardProps = {
|
type CreatorCardProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
actionOptions?: Option[];
|
actionOptions?: Option[];
|
||||||
event?: NostrEvent;
|
event?: KindCardProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Container({
|
export default function Container({
|
||||||
@ -60,20 +61,19 @@ export default function Container({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { pubkey, tags, created_at: createdAt } = event;
|
const { pubkey, tags, created_at: createdAt, locked } = event;
|
||||||
const contentTags = removeDuplicates(getTagsValues("t", tags)).filter(
|
const contentTags = removeDuplicates(getTagsValues("t", tags)).filter(
|
||||||
Boolean,
|
Boolean,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card className="relative flex h-full w-full flex-col overflow-hidden @container">
|
||||||
onClick={() => {
|
|
||||||
console.log("CLICK IN CONTAINER");
|
|
||||||
}}
|
|
||||||
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">
|
<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">
|
<div className="-mr-1 flex items-center gap-x-1.5 text-xs text-muted-foreground">
|
||||||
{!!createdAt &&
|
{!!createdAt &&
|
||||||
formatDate(new Date(createdAt * 1000), "MMM Do, h:mm a")}
|
formatDate(new Date(createdAt * 1000), "MMM Do, h:mm a")}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
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 Link from "next/link";
|
||||||
import useProfile from "@/lib/hooks/useProfile";
|
import useProfile from "@/lib/hooks/useProfile";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
@ -8,8 +8,9 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
|
|
||||||
type ProfileHeaderProps = {
|
type ProfileHeaderProps = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
|
locked?: boolean;
|
||||||
};
|
};
|
||||||
export default function ProfileHeader({ pubkey }: ProfileHeaderProps) {
|
export default function ProfileHeader({ pubkey, locked }: ProfileHeaderProps) {
|
||||||
const { profile } = useProfile(pubkey);
|
const { profile } = useProfile(pubkey);
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
return (
|
return (
|
||||||
@ -29,6 +30,7 @@ export default function ProfileHeader({ pubkey }: ProfileHeaderProps) {
|
|||||||
{!!profile?.nip05 && (
|
{!!profile?.nip05 && (
|
||||||
<HiCheckBadge className="h-4 w-4 text-primary" />
|
<HiCheckBadge className="h-4 w-4 text-primary" />
|
||||||
)}
|
)}
|
||||||
|
{!!locked && <HiMiniKey className="h-4 w-4 text-primary" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{!!profile.nip05 && (
|
{!!profile.nip05 && (
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import Container from "./components/Container";
|
import Container from "./components/Container";
|
||||||
import { CardTitle, CardDescription } from "@/components/ui/card";
|
import { CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { type Event } from "nostr-tools";
|
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { copyText } from "@/lib/utils";
|
import { copyText } from "@/lib/utils";
|
||||||
import { RenderText } from "../TextRendering";
|
import { RenderText } from "../TextRendering";
|
||||||
import { getTagsValues } from "@/lib/nostr/utils";
|
import { getTagsValues } from "@/lib/nostr/utils";
|
||||||
import LinkCard from "@/components/LinkCard";
|
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 { pubkey, created_at: createdAt, tags } = props;
|
||||||
const r = getTagsValues("r", tags).filter(Boolean);
|
const r = getTagsValues("r", tags).filter(Boolean);
|
||||||
|
|
||||||
|
@ -27,7 +27,9 @@ const componentMap: Record<number, ComponentType<KindCardProps>> = {
|
|||||||
30311: KindCard30311,
|
30311: KindCard30311,
|
||||||
};
|
};
|
||||||
|
|
||||||
type KindCardProps = Event<number>;
|
export type KindCardProps = Event<number> & {
|
||||||
|
locked?: boolean;
|
||||||
|
};
|
||||||
export default function KindCard(props: KindCardProps) {
|
export default function KindCard(props: KindCardProps) {
|
||||||
const { kind } = props;
|
const { kind } = props;
|
||||||
const KindCard_ = componentMap[kind] ?? KindCardDefault;
|
const KindCard_ = componentMap[kind] ?? KindCardDefault;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Container from "./components/Container";
|
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 { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { type KindCardProps } from "./";
|
||||||
|
|
||||||
export default function KindLoading() {
|
export default function KindLoading() {
|
||||||
return (
|
return (
|
||||||
|
@ -19,7 +19,7 @@ const CreateListSchema = z.object({
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
subscriptions: z.boolean(),
|
subscriptions: z.boolean().optional(),
|
||||||
price: z.number().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,
|
HiChatBubbleLeftEllipsis,
|
||||||
HiBookmarkSquare,
|
HiBookmarkSquare,
|
||||||
HiNewspaper,
|
HiNewspaper,
|
||||||
|
HiUserGroup,
|
||||||
} from "react-icons/hi2";
|
} from "react-icons/hi2";
|
||||||
import { RiSubtractFill, RiAddFill } from "react-icons/ri";
|
import { RiSubtractFill, RiAddFill } from "react-icons/ri";
|
||||||
import { formatCount } from "@/lib/utils";
|
import { formatCount } from "@/lib/utils";
|
||||||
|
@ -13,27 +13,26 @@ import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
|||||||
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
|
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
|
||||||
import useLists from "@/lib/hooks/useLists";
|
import useLists from "@/lib/hooks/useLists";
|
||||||
import { getUrls } from "@/components/TextRendering";
|
import { getUrls } from "@/components/TextRendering";
|
||||||
|
import { log } from "@/lib/utils";
|
||||||
const ShortTextNoteSchema = z.object({
|
const ShortTextNoteSchema = z.object({
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
list: z.string().optional(),
|
|
||||||
isPrivate: z.boolean().optional(),
|
isPrivate: z.boolean().optional(),
|
||||||
|
subscriptionTiers: z.string().array().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type ShortTextNoteType = z.infer<typeof ShortTextNoteSchema>;
|
type ShortTextNoteType = z.infer<typeof ShortTextNoteSchema>;
|
||||||
|
|
||||||
export default function ShortTextNoteModal() {
|
export default function ShortTextNoteModal() {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const { lists, init } = useLists();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { currentUser } = useCurrentUser();
|
const { currentUser, initSubscriptions, mySubscription } = useCurrentUser();
|
||||||
const [sent, setSent] = useState(false);
|
const [sent, setSent] = useState(false);
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
const { getSigner } = useSigner()!;
|
const { getSigner } = useSigner()!;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
void init(currentUser.pubkey);
|
void initSubscriptions(currentUser.pubkey);
|
||||||
}
|
}
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
@ -54,25 +53,24 @@ export default function ShortTextNoteModal() {
|
|||||||
}
|
}
|
||||||
const urls = getUrls(data.content);
|
const urls = getUrls(data.content);
|
||||||
const urlTags = urls.map((u) => ["r", u]);
|
const urlTags = urls.map((u) => ["r", u]);
|
||||||
if (data.list) {
|
if (data.isPrivate) {
|
||||||
const list = lists.find((l) => getTagValues("d", l.tags) === data.list);
|
if (!mySubscription) {
|
||||||
if (!list) {
|
|
||||||
toast.error("No list found");
|
toast.error("No list found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let listSigner: SignerStoreItem | undefined = undefined;
|
let delegateSigner: SignerStoreItem | undefined = undefined;
|
||||||
if (data.isPrivate) {
|
if (data.isPrivate) {
|
||||||
listSigner = await getSigner(list);
|
delegateSigner = await getSigner(mySubscription);
|
||||||
if (!listSigner?.signer) {
|
if (!delegateSigner?.signer) {
|
||||||
toast.error("Error creating signer");
|
toast.error("Error creating signer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!listSigner?.saved) {
|
if (!delegateSigner?.saved) {
|
||||||
console.log("Saving delegate...");
|
console.log("Saving delegate...");
|
||||||
await saveEphemeralSigner(ndk!, listSigner.signer, {
|
await saveEphemeralSigner(ndk!, delegateSigner.signer, {
|
||||||
associatedEvent: list,
|
associatedEvent: mySubscription,
|
||||||
keyProfile: {
|
keyProfile: {
|
||||||
name: listSigner.title,
|
name: delegateSigner.title,
|
||||||
picture: currentUser?.profile?.image,
|
picture: currentUser?.profile?.image,
|
||||||
lud06: currentUser?.profile?.lud06,
|
lud06: currentUser?.profile?.lud06,
|
||||||
lud16: currentUser?.profile?.lud16,
|
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(
|
const result = await createEventHandler(
|
||||||
ndk,
|
ndk,
|
||||||
{
|
{
|
||||||
@ -89,23 +91,19 @@ export default function ShortTextNoteModal() {
|
|||||||
tags: [...urlTags],
|
tags: [...urlTags],
|
||||||
},
|
},
|
||||||
data.isPrivate,
|
data.isPrivate,
|
||||||
list,
|
mySubscription,
|
||||||
listSigner?.signer,
|
delegateSigner?.signer,
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
toast.success("Note added!");
|
toast.success("Note added!");
|
||||||
modal?.hide();
|
modal?.hide();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await createEventHandler(
|
const result = await createEventHandler(ndk, {
|
||||||
ndk,
|
content: data.content,
|
||||||
{
|
kind: 1,
|
||||||
content: data.content,
|
tags: [...urlTags],
|
||||||
kind: 1,
|
});
|
||||||
tags: [...urlTags],
|
|
||||||
},
|
|
||||||
data.isPrivate,
|
|
||||||
);
|
|
||||||
if (result) {
|
if (result) {
|
||||||
toast.success("Note added!");
|
toast.success("Note added!");
|
||||||
modal?.hide();
|
modal?.hide();
|
||||||
@ -122,33 +120,39 @@ export default function ShortTextNoteModal() {
|
|||||||
type: "text-area",
|
type: "text-area",
|
||||||
slug: "content",
|
slug: "content",
|
||||||
},
|
},
|
||||||
{
|
...(mySubscription
|
||||||
label: "Add to list",
|
? ([
|
||||||
type: "select-search",
|
{
|
||||||
placeholder: "Search your lists",
|
label: "Subs only",
|
||||||
slug: "list",
|
type: "toggle",
|
||||||
options: lists
|
slug: "isPrivate",
|
||||||
.map((l) => {
|
},
|
||||||
const title =
|
] as const)
|
||||||
getTagValues("title", l.tags) ??
|
: []),
|
||||||
getTagValues("name", l.tags) ??
|
|
||||||
"Untitled";
|
// {
|
||||||
const description = getTagValues("description", l.tags);
|
// label: "Choose Tiers",
|
||||||
const value = getTagValues("d", l.tags);
|
// type: "select-search",
|
||||||
if (!value) return;
|
// placeholder: "Search your lists",
|
||||||
return {
|
// slug: "subscriptionTiers",
|
||||||
label: title,
|
// options: lists
|
||||||
description,
|
// .map((l) => {
|
||||||
value: value,
|
// const title =
|
||||||
};
|
// getTagValues("title", l.tags) ??
|
||||||
})
|
// getTagValues("name", l.tags) ??
|
||||||
.filter(Boolean),
|
// "Untitled";
|
||||||
},
|
// const description = getTagValues("description", l.tags);
|
||||||
{
|
// const value = getTagValues("d", l.tags);
|
||||||
label: "Private",
|
// if (!value) return;
|
||||||
type: "toggle",
|
// return {
|
||||||
slug: "isPrivate",
|
// label: title,
|
||||||
},
|
// description,
|
||||||
|
// value: value,
|
||||||
|
// };
|
||||||
|
// })
|
||||||
|
// .filter(Boolean),
|
||||||
|
// condition: "subscriptions",
|
||||||
|
// },
|
||||||
]}
|
]}
|
||||||
formSchema={ShortTextNoteSchema}
|
formSchema={ShortTextNoteSchema}
|
||||||
onSubmit={handleSubmit}
|
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 Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -8,28 +10,79 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
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 = {
|
export default function SubscriptionCard({ event }: { event: NDKEvent }) {
|
||||||
id: string;
|
const { currentUser } = useCurrentUser();
|
||||||
title: string;
|
const { ndk } = useNDK();
|
||||||
picture: string;
|
const [checkingPayment, setCheckingPayment] = useState(false);
|
||||||
description: string;
|
const [hasValidPayment, setHasValidPayment] = useState(false);
|
||||||
tags: string[];
|
const { tags } = event;
|
||||||
};
|
const rawEvent = event.rawEvent();
|
||||||
export default function SubscriptionCard({
|
const title = getTagValues("title", tags) ?? getTagValues("name", tags) ?? "";
|
||||||
picture,
|
const image =
|
||||||
title,
|
getTagValues("image", tags) ?? getTagValues("picture", tags) ?? "";
|
||||||
tags,
|
const description =
|
||||||
description,
|
getTagValues("description", tags) ?? getTagValues("summary", tags) ?? "";
|
||||||
}: SubscriptionCardProps) {
|
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 (
|
return (
|
||||||
<Card className="group sm:flex">
|
<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">
|
<div className="overflow-hidden max-sm:h-[100px] max-sm:rounded-t-md sm:w-[250px] sm:rounded-l-md">
|
||||||
<Image
|
<Image
|
||||||
width={250}
|
width={250}
|
||||||
height={150}
|
height={150}
|
||||||
src={picture}
|
src={image}
|
||||||
alt={title}
|
alt={title}
|
||||||
unoptimized
|
unoptimized
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -45,10 +98,26 @@ export default function SubscriptionCard({
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="items-strech flex w-full flex-col items-center gap-2 sm:max-w-md sm:flex-row sm:gap-4">
|
<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 variant={"secondary"} className="w-full">
|
<Button disabled={true} variant={"ghost"} className="w-full">
|
||||||
Details
|
Pending sync
|
||||||
</Button>
|
</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>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -7,7 +7,12 @@ import NDK, {
|
|||||||
type NostrEvent,
|
type NostrEvent,
|
||||||
NDKUser,
|
NDKUser,
|
||||||
} from "@nostr-dev-kit/ndk";
|
} 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 { unixTimeNowInSeconds } from "@/lib/nostr/dates";
|
||||||
import { getTagsValues } from "@/lib/nostr/utils";
|
import { getTagsValues } from "@/lib/nostr/utils";
|
||||||
import { log } from "@/lib/utils";
|
import { log } from "@/lib/utils";
|
||||||
@ -262,3 +267,16 @@ export async function updateList(
|
|||||||
tags: tags.filter(([_, value]) => value !== undefined),
|
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";
|
"use client";
|
||||||
|
import { useEffect } from "react";
|
||||||
import currentUserStore from "@/lib/stores/currentUser";
|
import currentUserStore from "@/lib/stores/currentUser";
|
||||||
// import useEvents from "@/lib/hooks/useEvents";
|
// import useEvents from "@/lib/hooks/useEvents";
|
||||||
import { UserSchema } from "@/types";
|
import { UserSchema } from "@/types";
|
||||||
import { useNDK } from "@/app/_providers/ndk";
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import useLists from "./useLists";
|
import useLists from "./useLists";
|
||||||
|
import useSubscriptions from "./useSubscriptions";
|
||||||
|
|
||||||
export default function useCurrentUser() {
|
export default function useCurrentUser() {
|
||||||
const {
|
const {
|
||||||
currentUser,
|
currentUser,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
setFollows,
|
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
follows,
|
follows,
|
||||||
|
setFollows,
|
||||||
} = currentUserStore();
|
} = currentUserStore();
|
||||||
const { loginWithNip07, getProfile, ndk } = useNDK();
|
const { loginWithNip07, getProfile, ndk, fetchEvents } = useNDK();
|
||||||
const { init } = useLists();
|
const { init } = useLists();
|
||||||
|
const { init: initSubscriptions, mySubscription } = useSubscriptions();
|
||||||
async function attemptLogin() {
|
async function attemptLogin() {
|
||||||
try {
|
try {
|
||||||
const shouldReconnect = localStorage.getItem("shouldReconnect");
|
const shouldReconnect = localStorage.getItem("shouldReconnect");
|
||||||
@ -24,8 +27,9 @@ export default function useCurrentUser() {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("NO auth");
|
throw new Error("NO auth");
|
||||||
}
|
}
|
||||||
console.log("LOGIN", user);
|
const pubkey = nip19.decode(user.npub).data.toString();
|
||||||
await loginWithPubkey(nip19.decode(user.npub).data.toString());
|
await loginWithPubkey(pubkey);
|
||||||
|
void initSubscriptions(pubkey);
|
||||||
if (typeof window.webln !== "undefined") {
|
if (typeof window.webln !== "undefined") {
|
||||||
await window.webln.enable();
|
await window.webln.enable();
|
||||||
}
|
}
|
||||||
@ -66,6 +70,15 @@ export default function useCurrentUser() {
|
|||||||
void init(user.pubkey);
|
void init(user.pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser) return;
|
||||||
|
console.log("fetching follows");
|
||||||
|
(async () => {
|
||||||
|
const following = await currentUser.follows();
|
||||||
|
setFollows(following);
|
||||||
|
})();
|
||||||
|
}, [currentUser]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentUser,
|
currentUser,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -75,5 +88,7 @@ export default function useCurrentUser() {
|
|||||||
updateUser: handleUpdateUser,
|
updateUser: handleUpdateUser,
|
||||||
loginWithPubkey,
|
loginWithPubkey,
|
||||||
attemptLogin,
|
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 {
|
interface CurrentUserState {
|
||||||
currentUser: NDKUser | null;
|
currentUser: NDKUser | null;
|
||||||
follows: string[];
|
follows: Set<NDKUser>;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
setCurrentUser: (user: NDKUser | null) => void;
|
setCurrentUser: (user: NDKUser | null) => void;
|
||||||
updateCurrentUser: (user: Partial<NDKUser>) => void;
|
updateCurrentUser: (user: Partial<NDKUser>) => void;
|
||||||
setFollows: (follows: string[]) => void;
|
setFollows: (follows: Set<NDKUser>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserStore = create<CurrentUserState>()((set) => ({
|
const currentUserStore = create<CurrentUserState>()((set) => ({
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
follows: [],
|
follows: new Set(),
|
||||||
settings: {},
|
settings: {},
|
||||||
setCurrentUser: (user) => set((state) => ({ ...state, currentUser: user })),
|
setCurrentUser: (user) => set((state) => ({ ...state, currentUser: user })),
|
||||||
updateCurrentUser: (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