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 (
- <>
-
-
-
-
-
-
-
-
- {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 (
-
-
-
+
+
+
+
+
+
+
+
Failed to get event
@@ -57,9 +70,20 @@ export function RepostNote({
return (
-
-
-
+
+
+
+
+
+
+
+
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 (
-
-
-
-
-
-
-
-
-
- {user?.name ||
- user?.display_name ||
- user?.displayName ||
- fallbackName}
-
- ·
-
- {createdAt}
-
-
-
- );
- }
-
- if (variant === "repost") {
- if (isLoading) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {user?.name ||
- user?.display_name ||
- user?.displayName ||
- fallbackName}
-
- reposted
-
-
-
- );
- }
-
- if (variant === "thread") {
- if (isLoading) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
- {user?.name || user?.display_name || user?.displayName || "Anon"}
-
-
- {createdAt}
- ·
- {fallbackName}
-
-
-
- );
- }
-
- if (isLoading) {
- return (
-
- );
- }
return (
-
-
-
-
-
+
+
+
+
+
-
-
-
-
- {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 (
+
+
+
+
+
+
+ );
+}
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 (