wip
This commit is contained in:
@@ -20,7 +20,7 @@ export function ActiveAccount() {
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { user } = useProfile(ark.account.npub);
|
||||
const { profile } = useProfile(ark.account.npub);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
@@ -32,7 +32,7 @@ export function ActiveAccount() {
|
||||
)}
|
||||
>
|
||||
<Avatar.Image
|
||||
src={user?.picture}
|
||||
src={profile?.picture}
|
||||
alt={ark.account.npub}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
|
||||
@@ -3,17 +3,11 @@ export * from "./user";
|
||||
export * from "./note";
|
||||
export * from "./column";
|
||||
|
||||
// Note Primities
|
||||
export * from "./note/primitives/text";
|
||||
export * from "./note/primitives/repost";
|
||||
export * from "./note/primitives/thread";
|
||||
|
||||
// Deprecated
|
||||
export * from "./routes/event";
|
||||
export * from "./routes/user";
|
||||
export * from "./routes/suggest";
|
||||
export * from "./mentions";
|
||||
export * from "./replyList";
|
||||
export * from "./emptyFeed";
|
||||
export * from "./translateRegisterModal";
|
||||
export * from "./account/active";
|
||||
|
||||
44
packages/ui/src/note/buttons/downvote.tsx
Normal file
44
packages/ui/src/note/buttons/downvote.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ArrowDownIcon, ArrowUpIcon, LoaderIcon } from "@lume/icons";
|
||||
import { useState } from "react";
|
||||
import { useNoteContext } from "../provider";
|
||||
import { useArk } from "@lume/ark";
|
||||
import { cn } from "@lume/utils";
|
||||
|
||||
export function NoteDownvote() {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
|
||||
const [reaction, setReaction] = useState<"-" | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const down = async () => {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const res = await ark.downvote(event.id, event.pubkey);
|
||||
if (res) setReaction("-");
|
||||
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={down}
|
||||
disabled={!!reaction || loading}
|
||||
className={cn(
|
||||
"inline-flex size-7 items-center justify-center rounded-full",
|
||||
reaction === "-"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowDownIcon className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +1,11 @@
|
||||
import { ArrowDownIcon, ArrowUpIcon } from "@lume/icons";
|
||||
import { useState } from "react";
|
||||
import { useNoteContext } from "../provider";
|
||||
import { useArk } from "@lume/ark";
|
||||
import { cn } from "@lume/utils";
|
||||
import { NoteUpvote } from "./upvote";
|
||||
import { NoteDownvote } from "./downvote";
|
||||
|
||||
export function NoteReaction() {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
|
||||
const [reaction, setReaction] = useState<"+" | "-">(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const up = async () => {
|
||||
// start loading
|
||||
setLoading(false);
|
||||
|
||||
const res = await ark.upvote(event.id, event.pubkey);
|
||||
if (res) setReaction("+");
|
||||
|
||||
// stop loading
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const down = async () => {
|
||||
// start loading
|
||||
setLoading(false);
|
||||
|
||||
const res = await ark.downvote(event.id, event.pubkey);
|
||||
if (res) setReaction("-");
|
||||
|
||||
// stop loading
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={up}
|
||||
disabled={!!reaction || loading}
|
||||
className={cn(
|
||||
"inline-flex size-7 items-center justify-center rounded-full",
|
||||
reaction === "+"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||
)}
|
||||
>
|
||||
<ArrowUpIcon className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={down}
|
||||
disabled={!!reaction || loading}
|
||||
className={cn(
|
||||
"inline-flex size-7 items-center justify-center rounded-full",
|
||||
reaction === "-"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||
)}
|
||||
>
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</button>
|
||||
<NoteUpvote />
|
||||
<NoteDownvote />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
44
packages/ui/src/note/buttons/upvote.tsx
Normal file
44
packages/ui/src/note/buttons/upvote.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ArrowUpIcon, LoaderIcon } from "@lume/icons";
|
||||
import { useState } from "react";
|
||||
import { useNoteContext } from "../provider";
|
||||
import { useArk } from "@lume/ark";
|
||||
import { cn } from "@lume/utils";
|
||||
|
||||
export function NoteUpvote() {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
|
||||
const [reaction, setReaction] = useState<"+" | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const up = async () => {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const res = await ark.upvote(event.id, event.pubkey);
|
||||
if (res) setReaction("+");
|
||||
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={up}
|
||||
disabled={!!reaction || loading}
|
||||
className={cn(
|
||||
"inline-flex size-7 items-center justify-center rounded-full",
|
||||
reaction === "+"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowUpIcon className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PinIcon } from "@lume/icons";
|
||||
import { NOSTR_MENTIONS } from "@lume/utils";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -6,7 +5,7 @@ import reactStringReplace from "react-string-replace";
|
||||
import { User } from "../../user";
|
||||
import { Hashtag } from "./hashtag";
|
||||
import { MentionUser } from "./user";
|
||||
import { useEvent } from "@lume/ark";
|
||||
import { useArk, useEvent } from "@lume/ark";
|
||||
|
||||
export function MentionNote({
|
||||
eventId,
|
||||
@@ -18,6 +17,7 @@ export function MentionNote({
|
||||
const { t } = useTranslation();
|
||||
const { isLoading, isError, data } = useEvent(eventId);
|
||||
|
||||
const ark = useArk();
|
||||
const richContent = useMemo(() => {
|
||||
if (!data) return "";
|
||||
|
||||
@@ -117,17 +117,18 @@ export function MentionNote({
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="line-clamp-4 select-text whitespace-pre-line text-balance px-3 leading-normal">
|
||||
<div className="line-clamp-4 select-text whitespace-normal text-balance px-3 leading-normal">
|
||||
{richContent}
|
||||
</div>
|
||||
{openable ? (
|
||||
<div className="flex h-10 items-center justify-between px-3">
|
||||
<a
|
||||
href={`/events/${data.id}`}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => ark.open_thread(data.id)}
|
||||
className="text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{t("note.showMore")}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-3" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export function NoteMenu() {
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="text-neutral-500 hover:text-blue-500 dark:text-neutral-400"
|
||||
className="group inline-flex size-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
<HorizontalDotsIcon className="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function TextNote({
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="size-10 shrink-0" />
|
||||
<div className="size-11 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<Note.Thread className="mb-2" />
|
||||
<Note.Content />
|
||||
|
||||
@@ -16,7 +16,7 @@ export function NoteUser({ className }: { className?: string }) {
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<HoverCard.Trigger>
|
||||
<User.Avatar className="size-10 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
||||
<User.Avatar className="size-11 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
||||
</HoverCard.Trigger>
|
||||
<div>
|
||||
<User.Name className="font-semibold leading-tight text-neutral-950 dark:text-neutral-50" />
|
||||
|
||||
@@ -34,10 +34,6 @@ export function ReplyList({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ReplyForm
|
||||
eventId={eventId}
|
||||
className="border-t border-neutral-100 py-4 dark:border-neutral-900"
|
||||
/>
|
||||
{!data ? (
|
||||
<div className="mt-4 flex h-16 items-center justify-center p-3">
|
||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||
|
||||
@@ -4,34 +4,9 @@ import { useUserContext } from "./provider";
|
||||
export function UserAbout({ className }: { className?: string }) {
|
||||
const user = useUserContext();
|
||||
|
||||
if (!user.profile) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className={cn(
|
||||
"h-4 w-20 animate-pulse rounded bg-black/20 dark:bg-white/20",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"h-4 w-full animate-pulse rounded bg-black/20 dark:bg-white/20",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"h-4 w-24 animate-pulse rounded bg-black/20 dark:bg-white/20",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("content-break select-text", className)}>
|
||||
{user.profile.about?.trim() || "No bio"}
|
||||
{user.profile?.about?.trim() || "No bio"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,28 +11,15 @@ export function UserAvatar({ className }: { className?: string }) {
|
||||
const fallbackAvatar = useMemo(
|
||||
() =>
|
||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(user?.pubkey || nanoid(), 90, 50),
|
||||
minidenticon(user.pubkey || nanoid(), 90, 50),
|
||||
)}`,
|
||||
[user],
|
||||
);
|
||||
|
||||
if (!user.profile) {
|
||||
return (
|
||||
<div className="shrink-0">
|
||||
<div
|
||||
className={cn(
|
||||
"animate-pulse rounded bg-black/20 dark:bg-white/20",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user.profile.picture}
|
||||
src={user.profile?.picture}
|
||||
alt={user.pubkey}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { cn } from "@lume/utils";
|
||||
import { cn, displayNpub } from "@lume/utils";
|
||||
import { useUserContext } from "./provider";
|
||||
|
||||
export function UserName({ className }: { className?: string }) {
|
||||
const user = useUserContext();
|
||||
|
||||
if (!user.profile) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-1 h-3 w-20 animate-pulse self-center rounded bg-black/20 dark:bg-white/20",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("max-w-[12rem] truncate", className)}>
|
||||
{user.profile.display_name || user.profile.name || "Anon"}
|
||||
{user.profile?.display_name ||
|
||||
user.profile?.name ||
|
||||
displayNpub(user.pubkey, 16)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,25 +18,12 @@ export function UserNip05({ className }: { className?: string }) {
|
||||
enabled: !!user.profile,
|
||||
});
|
||||
|
||||
if (!user.profile) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-3 w-20 animate-pulse rounded bg-black/20 dark:bg-white/20",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<p className={cn("text-sm", className)}>
|
||||
{!user?.profile?.nip05
|
||||
{!user.profile?.nip05
|
||||
? displayNpub(user.pubkey, 16)
|
||||
: user?.profile?.nip05?.startsWith("_@")
|
||||
? user?.profile?.nip05?.replace("_@", "")
|
||||
: user?.profile?.nip05}
|
||||
: user.profile?.nip05.replace("_@", "")}
|
||||
</p>
|
||||
{!isLoading && verified ? (
|
||||
<VerifiedIcon className="size-4 text-teal-500" />
|
||||
|
||||
@@ -1,40 +1,25 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { useProfile } from "@lume/ark";
|
||||
import { Metadata } from "@lume/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ReactNode, createContext, useContext } from "react";
|
||||
|
||||
const UserContext = createContext<{ pubkey: string; profile: Metadata }>(null);
|
||||
const UserContext = createContext<{
|
||||
pubkey: string;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
profile: Metadata;
|
||||
}>(null);
|
||||
|
||||
export function UserProvider({
|
||||
pubkey,
|
||||
children,
|
||||
embed,
|
||||
}: {
|
||||
pubkey: string;
|
||||
children: ReactNode;
|
||||
embed?: string;
|
||||
}) {
|
||||
const ark = useArk();
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ["user", pubkey],
|
||||
queryFn: async () => {
|
||||
if (embed) return JSON.parse(embed) as Metadata;
|
||||
try {
|
||||
const profile: Metadata = await ark.get_profile(pubkey);
|
||||
return profile;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Infinity,
|
||||
retry: 2,
|
||||
});
|
||||
const { isLoading, isError, profile } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ pubkey, profile }}>
|
||||
<UserContext.Provider value={{ pubkey, isError, isLoading, profile }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user