diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 3601f317..d0086416 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -198,13 +198,13 @@ export class Ark { } } - public async createContact({ pubkey }: { pubkey: string }) { + public async createContact(pubkey: string) { const user = this.ndk.getUser({ pubkey: this.account.pubkey }); const contacts = await user.follows(); return await user.follow(new NDKUser({ pubkey: pubkey }), contacts); } - public async deleteContact({ pubkey }: { pubkey: string }) { + public async deleteContact(pubkey: string) { const user = this.ndk.getUser({ pubkey: this.account.pubkey }); const contacts = await user.follows(); contacts.delete(new NDKUser({ pubkey: pubkey })); @@ -549,17 +549,12 @@ export class Ark { signal, }); - if (!res.ok) { - console.log(res); - throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); - } + if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); const data: NIP05 = await res.json(); - if (!data.names) return false; if (data.names[localPath.toLowerCase()] === pubkey) return true; if (data.names[localPath] === pubkey) return true; - return false; } diff --git a/packages/ark/src/components/note/child.tsx b/packages/ark/src/components/note/child.tsx index d4fa7328..b5e0de16 100644 --- a/packages/ark/src/components/note/child.tsx +++ b/packages/ark/src/components/note/child.tsx @@ -4,7 +4,7 @@ import { ReactNode, useMemo } from "react"; import { Link } from "react-router-dom"; import reactStringReplace from "react-string-replace"; import { useEvent } from "../../hooks/useEvent"; -import { NoteChildUser } from "./childUser"; +import { User } from "../user"; import { Hashtag } from "./mentions/hashtag"; import { MentionUser } from "./mentions/user"; @@ -120,10 +120,17 @@ export function NoteChild({ {richContent} - + + + +
+ +
+ {isRoot ? "posted:" : "replied:"} +
+
+
+
); } diff --git a/packages/ark/src/components/note/childUser.tsx b/packages/ark/src/components/note/childUser.tsx deleted file mode 100644 index 0b054ffa..00000000 --- a/packages/ark/src/components/note/childUser.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { displayNpub } from '@lume/utils'; -import * as Avatar from '@radix-ui/react-avatar'; -import { minidenticon } from 'minidenticons'; -import { useMemo } from 'react'; -import { useProfile } from '../../hooks/useProfile'; - -export function NoteChildUser({ pubkey, subtext }: { pubkey: string; subtext: string }) { - const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]); - const fallbackAvatar = useMemo( - () => `data:image/svg+xml;utf8,${encodeURIComponent(minidenticon(pubkey, 90, 50))}`, - [pubkey] - ); - - const { isLoading, user } = useProfile(pubkey); - - if (isLoading) { - return ( - <> - - - -
-
{fallbackName}
-
- {subtext}: -
-
- - ); - } - - return ( - <> - - - - {pubkey} - - -
-
- {user?.display_name || user?.name || user?.displayName || fallbackName}{' '} -
-
- {subtext}: -
-
- - ); -} diff --git a/packages/ark/src/components/note/mentions/note.tsx b/packages/ark/src/components/note/mentions/note.tsx index df220c48..49de8d79 100644 --- a/packages/ark/src/components/note/mentions/note.tsx +++ b/packages/ark/src/components/note/mentions/note.tsx @@ -1,7 +1,8 @@ import { memo } from "react"; import { Link } from "react-router-dom"; -import { Note } from ".."; +import { Note } from "../"; import { useEvent } from "../../../hooks/useEvent"; +import { User } from "../../user"; export const MentionNote = memo(function MentionNote({ eventId, @@ -34,9 +35,19 @@ export const MentionNote = memo(function MentionNote({ return ( -
- -
+ + + +
+ + · + +
+
+
{openable ? ( diff --git a/packages/ark/src/components/note/primitives/repost.tsx b/packages/ark/src/components/note/primitives/repost.tsx index 446e1494..56ec4431 100644 --- a/packages/ark/src/components/note/primitives/repost.tsx +++ b/packages/ark/src/components/note/primitives/repost.tsx @@ -1,7 +1,9 @@ +import { RepostIcon } from "@lume/icons"; import { NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk"; import { useQuery } from "@tanstack/react-query"; import { Note } from ".."; import { useArk } from "../../../hooks/useArk"; +import { User } from "../../user"; export function RepostNote({ event, @@ -39,9 +41,20 @@ export function RepostNote({ if (isError || !repostEvent) { return ( - - - + + +
+ +
+
+ +
+ + reposted +
+
+
+

Failed to get event

@@ -57,9 +70,20 @@ export function RepostNote({ return ( - - - + + +
+ +
+
+ +
+ + reposted +
+
+
+
diff --git a/packages/ark/src/components/note/primitives/thread.tsx b/packages/ark/src/components/note/primitives/thread.tsx index 0bb45939..7d33b2b2 100644 --- a/packages/ark/src/components/note/primitives/thread.tsx +++ b/packages/ark/src/components/note/primitives/thread.tsx @@ -1,5 +1,6 @@ import { Note } from ".."; import { useEvent } from "../../../hooks/useEvent"; +import { User } from "../../user"; export function ThreadNote({ eventId }: { eventId: string }) { const { isLoading, data } = useEvent(eventId); @@ -13,6 +14,19 @@ export function ThreadNote({ eventId }: { eventId: string }) {
+ + + +
+ +
+ + · + +
+
+
+
diff --git a/packages/ark/src/components/note/thread.tsx b/packages/ark/src/components/note/thread.tsx index ef38803d..ee9aec83 100644 --- a/packages/ark/src/components/note/thread.tsx +++ b/packages/ark/src/components/note/thread.tsx @@ -3,7 +3,9 @@ import { COL_TYPES } from "@lume/utils"; import { Link } from "react-router-dom"; import { twMerge } from "tailwind-merge"; import { Note } from "."; -import { useArk, useColumnContext, useNoteContext } from "../.."; +import { useArk } from "../../hooks/useArk"; +import { useColumnContext } from "../column/provider"; +import { useNoteContext } from "./provider"; export function NoteThread({ className, diff --git a/packages/ark/src/components/note/user.tsx b/packages/ark/src/components/note/user.tsx index a885d048..ac38b4f6 100644 --- a/packages/ark/src/components/note/user.tsx +++ b/packages/ark/src/components/note/user.tsx @@ -1,240 +1,26 @@ -import { RepostIcon } from "@lume/icons"; -import { displayNpub, formatCreatedAt } from "@lume/utils"; -import * as Avatar from "@radix-ui/react-avatar"; -import { minidenticon } from "minidenticons"; -import { useMemo } from "react"; -import { twMerge } from "tailwind-merge"; -import { useProfile } from "../../hooks/useProfile"; +import { cn } from "@lume/utils"; +import { User } from "../user"; import { useNoteContext } from "./provider"; export function NoteUser({ - variant = "text", className, }: { - variant?: "text" | "repost" | "mention" | "thread"; className?: string; }) { const event = useNoteContext(); - const createdAt = useMemo( - () => formatCreatedAt(event.created_at), - [event.created_at], - ); - const fallbackName = useMemo( - () => displayNpub(event.pubkey, 16), - [event.pubkey], - ); - const fallbackAvatar = useMemo( - () => - `data:image/svg+xml;utf8,${encodeURIComponent( - minidenticon(event.pubkey, 90, 50), - )}`, - [event.pubkey], - ); - - const { isLoading, user } = useProfile(event.pubkey); - - if (variant === "mention") { - if (isLoading) { - return ( -
- - - -
-
- {fallbackName} -
- · - - {createdAt} - -
-
- ); - } - - return ( -
- - - - {event.pubkey} - - -
-
- {user?.name || - user?.display_name || - user?.displayName || - fallbackName} -
- · - - {createdAt} - -
-
- ); - } - - if (variant === "repost") { - if (isLoading) { - return ( -
-
- -
-
-
-
-
-
- ); - } - - return ( -
-
- -
-
- - - - {event.pubkey} - - -
-
- {user?.name || - user?.display_name || - user?.displayName || - fallbackName} -
- reposted -
-
-
- ); - } - - if (variant === "thread") { - if (isLoading) { - return ( -
-
-
-
-
-
-
- ); - } - - return ( -
- - - - {event.pubkey} - - -
-
- {user?.name || user?.display_name || user?.displayName || "Anon"} -
-
- {createdAt} - · - {fallbackName} -
-
-
- ); - } - - if (isLoading) { - return ( -
- - - -
-
- {fallbackName} -
-
-
- ); - } return ( -
- - - - {event.pubkey} + + +
+ + - - -
-
- {user?.name || - user?.display_name || - user?.displayName || - fallbackName}
-
- {createdAt} -
-
-
+
+ ); } diff --git a/packages/ark/src/components/user/avatar.tsx b/packages/ark/src/components/user/avatar.tsx new file mode 100644 index 00000000..0840edab --- /dev/null +++ b/packages/ark/src/components/user/avatar.tsx @@ -0,0 +1,48 @@ +import { cn } from "@lume/utils"; +import * as Avatar from "@radix-ui/react-avatar"; +import { minidenticon } from "minidenticons"; +import { useMemo } from "react"; +import { useUserContext } from "./provider"; + +export function UserAvatar({ className }: { className?: string }) { + const user = useUserContext(); + const fallbackAvatar = useMemo( + () => + `data:image/svg+xml;utf8,${encodeURIComponent( + minidenticon(user?.pubkey, 90, 50), + )}`, + [user], + ); + + if (!user) { + return ( +
+
+
+ ); + } + + return ( + + + + {user.pubkey} + + + ); +} diff --git a/packages/ark/src/components/user/followButton.tsx b/packages/ark/src/components/user/followButton.tsx new file mode 100644 index 00000000..111697d5 --- /dev/null +++ b/packages/ark/src/components/user/followButton.tsx @@ -0,0 +1,35 @@ +import { cn } from "@lume/utils"; +import { useEffect, useState } from "react"; +import { useArk } from "../../hooks/useArk"; + +export function UserFollowButton({ + target, + className, +}: { target: string; className?: string }) { + const ark = useArk(); + const [followed, setFollowed] = useState(false); + + const toggleFollow = async () => { + if (!followed) { + const add = await ark.createContact(target); + if (add) setFollowed(true); + } else { + const remove = await ark.deleteContact(target); + if (remove) setFollowed(false); + } + }; + + useEffect(() => { + async function status() { + const contacts = await ark.getUserContacts(); + if (contacts?.includes(target)) setFollowed(true); + } + status(); + }, []); + + return ( + + ); +} diff --git a/packages/ark/src/components/user/index.ts b/packages/ark/src/components/user/index.ts new file mode 100644 index 00000000..7aa56cf9 --- /dev/null +++ b/packages/ark/src/components/user/index.ts @@ -0,0 +1,17 @@ +import { UserAvatar } from "./avatar"; +import { UserFollowButton } from "./followButton"; +import { UserName } from "./name"; +import { UserNip05 } from "./nip05"; +import { UserProvider } from "./provider"; +import { UserRoot } from "./root"; +import { UserTime } from "./time"; + +export const User = { + Provider: UserProvider, + Root: UserRoot, + Avatar: UserAvatar, + Name: UserName, + NIP05: UserNip05, + Time: UserTime, + Button: UserFollowButton, +}; diff --git a/packages/ark/src/components/user/name.tsx b/packages/ark/src/components/user/name.tsx new file mode 100644 index 00000000..aea16b19 --- /dev/null +++ b/packages/ark/src/components/user/name.tsx @@ -0,0 +1,23 @@ +import { cn } from "@lume/utils"; +import { useUserContext } from "./provider"; + +export function UserName({ className }: { className?: string }) { + const user = useUserContext(); + + if (!user) { + return ( +
+ ); + } + + return ( +
+ {user.displayName || user.name || "Anon"} +
+ ); +} diff --git a/packages/ark/src/components/user/nip05.tsx b/packages/ark/src/components/user/nip05.tsx new file mode 100644 index 00000000..2fd0ce9f --- /dev/null +++ b/packages/ark/src/components/user/nip05.tsx @@ -0,0 +1,47 @@ +import { UnverifiedIcon, VerifiedIcon } from "@lume/icons"; +import { cn } from "@lume/utils"; +import { useQuery } from "@tanstack/react-query"; +import { useArk } from "../../hooks/useArk"; +import { useUserContext } from "./provider"; + +export function UserNip05({ className }: { className?: string }) { + const ark = useArk(); + const user = useUserContext(); + + const { isLoading, data: verified } = useQuery({ + queryKey: ["nip05", user?.nip05], + queryFn: async ({ signal }: { signal: AbortSignal }) => { + return ark.validateNIP05({ + pubkey: user?.pubkey, + nip05: user?.nip05, + signal, + }); + }, + }); + + if (!user) { + return ( +
+ ); + } + + return ( +
+

+ {user.nip05.startsWith("_@") + ? user.nip05.replace("_@", "") + : user.nip05} +

+ {!isLoading && verified ? ( + + ) : ( + + )} +
+ ); +} diff --git a/packages/ark/src/components/user/provider.tsx b/packages/ark/src/components/user/provider.tsx new file mode 100644 index 00000000..3bdef978 --- /dev/null +++ b/packages/ark/src/components/user/provider.tsx @@ -0,0 +1,36 @@ +import { NDKUserProfile } from "@nostr-dev-kit/ndk"; +import { useQuery } from "@tanstack/react-query"; +import { ReactNode, createContext, useContext } from "react"; +import { useArk } from "../../hooks/useArk"; + +const UserContext = createContext(null); + +export function UserProvider({ + pubkey, + children, +}: { pubkey: string; children: ReactNode }) { + const ark = useArk(); + const { data: user } = useQuery({ + queryKey: ["user", pubkey], + queryFn: async () => { + const profile = await ark.getUserProfile(pubkey); + if (!profile) + throw new Error( + `Cannot get metadata for ${pubkey}, will be retry after 10 seconds`, + ); + return profile; + }, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + staleTime: Infinity, + retry: 2, + }); + + return {children}; +} + +export function useUserContext() { + const context = useContext(UserContext); + return context; +} diff --git a/packages/ark/src/components/user/root.tsx b/packages/ark/src/components/user/root.tsx new file mode 100644 index 00000000..8366e1ee --- /dev/null +++ b/packages/ark/src/components/user/root.tsx @@ -0,0 +1,12 @@ +import { cn } from "@lume/utils"; +import { ReactNode } from "react"; + +export function UserRoot({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return
{children}
; +} diff --git a/packages/ark/src/components/user/time.tsx b/packages/ark/src/components/user/time.tsx new file mode 100644 index 00000000..3da95cec --- /dev/null +++ b/packages/ark/src/components/user/time.tsx @@ -0,0 +1,11 @@ +import { cn, formatCreatedAt } from "@lume/utils"; +import { useMemo } from "react"; + +export function UserTime({ + time, + className, +}: { time: number; className?: string }) { + const createdAt = useMemo(() => formatCreatedAt(time), [time]); + + return
{createdAt}
; +} diff --git a/packages/ark/src/index.ts b/packages/ark/src/index.ts index 22a7efc2..8147d450 100644 --- a/packages/ark/src/index.ts +++ b/packages/ark/src/index.ts @@ -5,10 +5,10 @@ export * from "./hooks/useEvent"; export * from "./hooks/useArk"; export * from "./hooks/useProfile"; export * from "./hooks/useRelay"; -export * from "./components/column/provider"; +export * from "./components/user"; export * from "./components/column"; +export * from "./components/column/provider"; export * from "./components/note"; -export * from "./components/note/provider"; export * from "./components/note/primitives/text"; export * from "./components/note/primitives/repost"; export * from "./components/note/primitives/skeleton"; diff --git a/packages/ui/src/activity/reply.tsx b/packages/ui/src/activity/reply.tsx index 6fac4adf..0afae083 100644 --- a/packages/ui/src/activity/reply.tsx +++ b/packages/ui/src/activity/reply.tsx @@ -4,7 +4,10 @@ import { ActivityRootNote } from "./rootNote"; export function ReplyActivity({ event }: { event: NDKEvent }) { const ark = useArk(); - const thread = ark.getEventThread({ tags: event.tags }); + const thread = ark.getEventThread({ + content: event.content, + tags: event.tags, + }); return (