wip: desktop2

This commit is contained in:
2024-02-15 14:32:40 +07:00
parent 70126ef1b3
commit 1de8c7240d
41 changed files with 1075 additions and 2271 deletions

View File

@@ -4,12 +4,12 @@
"private": true,
"main": "./src/index.ts",
"dependencies": {
"@getalby/sdk": "^3.2.3",
"@getalby/sdk": "^3.3.0",
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/storage": "workspace:^",
"@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.4.0",
"@nostr-dev-kit/ndk": "^2.4.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
@@ -19,22 +19,22 @@
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.18.1",
"@tanstack/react-router": "^1.16.0",
"framer-motion": "^11.0.3",
"@tanstack/react-query": "^5.20.5",
"@tanstack/react-router": "^1.16.2",
"framer-motion": "^11.0.5",
"get-urls": "^12.1.0",
"jotai": "^2.6.4",
"media-chrome": "^2.1.0",
"media-chrome": "^2.2.4",
"minidenticons": "^4.2.0",
"nanoid": "^5.0.5",
"qrcode.react": "^3.1.0",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-currency-input-field": "^3.6.14",
"react-currency-input-field": "^3.7.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.50.0",
"react-hook-form": "^7.50.1",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.0.2",
"react-i18next": "^14.0.5",
"react-router-dom": "^6.22.0",
"react-string-replace": "^1.1.1",
"slate": "^0.101.5",
@@ -43,13 +43,13 @@
"string-strip-html": "^13.4.6",
"uqr": "^0.1.2",
"use-debounce": "^10.0.0",
"virtua": "^0.23.3"
"virtua": "^0.27.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.52",
"@types/react": "^18.2.55",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@@ -37,7 +37,7 @@ export function ActiveAccount() {
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="aspect-square h-auto w-7 rounded-lg object-cover"
className="aspect-square h-auto w-7 rounded-full object-cover"
/>
<Avatar.Fallback delayMs={150}>
<img

View File

@@ -1,137 +1,25 @@
import { ReactionIcon } from "@lume/icons";
import * as HoverCard from "@radix-ui/react-hover-card";
import { ArrowDownIcon, ArrowUpIcon } from "@lume/icons";
import { useState } from "react";
import { useNoteContext } from "../provider";
const REACTIONS = [
{
content: "👏",
img: "/clapping_hands.png",
},
{
content: "🤪",
img: "/face_with_tongue.png",
},
{
content: "😮",
img: "/face_with_open_mouth.png",
},
{
content: "😢",
img: "/crying_face.png",
},
{
content: "🤡",
img: "/clown_face.png",
},
];
export function NoteReaction() {
const event = useNoteContext();
const event = useNoteContext();
const [reaction, setReaction] = useState<"+" | "-">(null);
const [open, setOpen] = useState(false);
const [reaction, setReaction] = useState<string | null>(null);
const getReactionImage = (content: string) => {
const reaction: { img: string } = REACTIONS.find(
(el) => el.content === content,
);
return reaction.img;
};
const react = async (content: string) => {
try {
setReaction(content);
// react
await event.react(content);
setOpen(false);
} catch (e) {
console.error(e);
}
};
return (
<HoverCard.Root open={open} onOpenChange={setOpen}>
<HoverCard.Trigger asChild>
<button
type="button"
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
>
{reaction ? (
<img
src={getReactionImage(reaction)}
alt={reaction}
className="size-6"
/>
) : (
<ReactionIcon className="size-5 group-hover:text-blue-500" />
)}
</button>
</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content
className="select-none rounded-lg bg-neutral-950 dark:bg-neutral-50 px-1 py-1 text-sm will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade"
sideOffset={0}
side="top"
>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => react("👏")}
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
>
<img
src="/clapping_hands.png"
alt="Clapping Hands"
className="size-6"
/>
</button>
<button
type="button"
onClick={() => react("🤪")}
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
>
<img
src="/face_with_tongue.png"
alt="Face with Tongue"
className="size-6"
/>
</button>
<button
type="button"
onClick={() => react("😮")}
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
>
<img
src="/face_with_open_mouth.png"
alt="Face with Open Mouth"
className="size-6"
/>
</button>
<button
type="button"
onClick={() => react("😢")}
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
>
<img
src="/crying_face.png"
alt="Crying Face"
className="size-6"
/>
</button>
<button
type="button"
onClick={() => react("🤡")}
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
>
<img src="/clown_face.png" alt="Clown Face" className="size-6" />
</button>
</div>
<HoverCard.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
return (
<div className="inline-flex items-center gap-2">
<button
type="button"
className="inline-flex size-7 items-center justify-center rounded-full 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"
className="inline-flex size-7 items-center justify-center rounded-full 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>
</div>
);
}

View File

@@ -13,7 +13,7 @@ export function NoteReply() {
<Tooltip.Trigger asChild>
<button
type="button"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
>
<ReplyIcon className="size-5 group-hover:text-blue-500" />
</button>

View File

@@ -9,109 +9,109 @@ import { toast } from "sonner";
import { useNoteContext } from "../provider";
export function NoteRepost() {
const event = useNoteContext();
const setEditorValue = useSetAtom(editorValueAtom);
const setIsEditorOpen = useSetAtom(editorAtom);
const event = useNoteContext();
const setEditorValue = useSetAtom(editorValueAtom);
const setIsEditorOpen = useSetAtom(editorAtom);
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false);
const [open, setOpen] = useState(false);
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false);
const [open, setOpen] = useState(false);
const repost = async () => {
try {
setLoading(true);
const repost = async () => {
try {
setLoading(true);
// repost
await event.repost(true);
// repost
await event.repost(true);
// update state
setLoading(false);
setIsRepost(true);
// update state
setLoading(false);
setIsRepost(true);
// notify
toast.success("You've reposted this post successfully");
} catch (e) {
setLoading(false);
toast.error("Repost failed, try again later");
}
};
// notify
toast.success("You've reposted this post successfully");
} catch (e) {
setLoading(false);
toast.error("Repost failed, try again later");
}
};
const quote = () => {
setEditorValue([
{
type: "paragraph",
children: [{ text: "" }],
},
{
type: "event",
// @ts-expect-error, useless
eventId: `nostr:${nip19.noteEncode(event.id)}`,
children: [{ text: "" }],
},
{
type: "paragraph",
children: [{ text: "" }],
},
]);
setIsEditorOpen(true);
};
const quote = () => {
setEditorValue([
{
type: "paragraph",
children: [{ text: "" }],
},
{
type: "event",
// @ts-expect-error, useless
eventId: `nostr:${nip19.noteEncode(event.id)}`,
children: [{ text: "" }],
},
{
type: "paragraph",
children: [{ text: "" }],
},
]);
setIsEditorOpen(true);
};
return (
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild>
<button
type="button"
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
>
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RepostIcon
className={cn(
"size-5 group-hover:text-blue-600",
isRepost ? "text-blue-500" : "",
)}
/>
)}
</button>
</Tooltip.Trigger>
</DropdownMenu.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
{t("note.buttons.repost")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
<DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none">
<DropdownMenu.Item asChild>
<button
type="button"
onClick={repost}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<RepostIcon className="size-4" />
{t("note.buttons.repost")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={quote}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<ReplyIcon className="size-4" />
{t("note.buttons.quote")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
return (
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild>
<button
type="button"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
>
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RepostIcon
className={cn(
"size-5 group-hover:text-blue-600",
isRepost ? "text-blue-500" : "",
)}
/>
)}
</button>
</Tooltip.Trigger>
</DropdownMenu.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] dark:bg-neutral-50 dark:text-neutral-950">
{t("note.buttons.repost")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
<DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-2xl bg-white/50 p-2 ring-1 ring-black/10 backdrop-blur-2xl focus:outline-none dark:bg-black/50 dark:ring-white/10">
<DropdownMenu.Item asChild>
<button
type="button"
onClick={repost}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<RepostIcon className="size-4" />
{t("note.buttons.repost")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={quote}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<ReplyIcon className="size-4" />
{t("note.buttons.quote")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}

View File

@@ -85,7 +85,7 @@ export function NoteZap() {
<button
type="button"
onClick={() => createZapRequest(true)}
className="group inline-flex size-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
>
{isLoading ? (
<LoaderIcon className="size-4 animate-spin" />
@@ -118,7 +118,7 @@ export function NoteZap() {
<Tooltip.Trigger asChild>
<button
type="button"
className="group inline-flex size-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
>
<ZapIcon className="size-5 group-hover:text-blue-500" />
</button>

View File

@@ -65,7 +65,7 @@ export function NoteChild({
href={url.toString()}
target="_blank"
rel="noreferrer"
className="break-p font-normal text-blue-500 hover:text-blue-600"
className="content-break font-normal text-blue-500 hover:text-blue-600"
>
{url.toString()}
</a>
@@ -104,7 +104,7 @@ export function NoteChild({
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800" />
<div className="break-p mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
<div className="content-break mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
{richContent}
</div>
</div>

View File

@@ -167,7 +167,7 @@ export function NoteContent({ className }: { className?: string }) {
href={url.toString()}
target="_blank"
rel="noreferrer"
className="break-p inline-block w-full truncate font-normal text-blue-500 hover:text-blue-600"
className="content-break inline-block w-full truncate font-normal text-blue-500 hover:text-blue-600"
>
{url.toString()}
</a>
@@ -224,7 +224,7 @@ export function NoteContent({ className }: { className?: string }) {
return (
<div className={cn(className)}>
<div className="break-p select-text whitespace-pre-line text-balance leading-normal">
<div className="content-break select-text whitespace-pre-line text-balance leading-normal">
{richContent}
</div>
{storage.settings.translation && translate.translatable ? (

View File

@@ -68,7 +68,7 @@ export function MentionNote({
href={url.toString()}
target="_blank"
rel="noreferrer"
className="break-p inline-block w-full truncate font-normal text-blue-500 hover:text-blue-600"
className="content-break inline-block w-full truncate font-normal text-blue-500 hover:text-blue-600"
>
{url.toString()}
</a>

View File

@@ -68,12 +68,12 @@ export function LinkPreview({ url }: { url: string }) {
<div className="flex flex-col items-start p-3">
<div className="flex flex-col items-start text-left">
{data.title ? (
<div className="break-p text-base font-semibold text-neutral-900 dark:text-neutral-100">
<div className="content-break text-base font-semibold text-neutral-900 dark:text-neutral-100">
{data.title}
</div>
) : null}
{data.description ? (
<div className="break-p mb-2 line-clamp-3 text-balance text-sm text-neutral-700 dark:text-neutral-400">
<div className="content-break mb-2 line-clamp-3 text-balance text-sm text-neutral-700 dark:text-neutral-400">
{data.description}
</div>
) : null}

View File

@@ -11,18 +11,23 @@ export function TextNote({
}) {
return (
<Note.Provider event={event}>
<Note.Root className={cn("flex flex-col", className)}>
<div className="flex h-14 items-center justify-between px-3">
<Note.Root
className={cn(
"mb-3 flex flex-col gap-2 border-b border-neutral-100 pb-3 dark:border-neutral-900",
className,
)}
>
<div className="flex items-start justify-between">
<Note.User className="flex-1 pr-2" />
<Note.Menu />
</div>
<div className="flex gap-3">
<div className="size-10 shrink-0" />
<div className="flex-1">
<div className="min-w-0 flex-1">
<Note.Content className="mb-2" />
<Note.Thread className="mb-2" />
<Note.Content className="min-w-0 px-3" />
<div className="flex h-14 items-center justify-between px-3">
<Note.Pin />
<div className="mt-5 flex items-center justify-between">
<Note.Reaction />
<div className="inline-flex items-center gap-4">
<Note.Reply />
<Note.Repost />

View File

@@ -18,14 +18,14 @@ export function NoteThread({ className }: { className?: string }) {
if (!thread) return null;
return (
<div className={cn("w-full px-3", className)}>
<div className={cn("w-full", className)}>
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
{thread.rootEventId ? (
<Note.Child eventId={thread.rootEventId} isRoot />
) : null}
{thread.replyEventId ? (
<Note.Child eventId={thread.replyEventId} />
) : null}
{thread.rootEventId ? (
<Note.Child eventId={thread.rootEventId} isRoot />
) : null}
<div className="inline-flex items-center justify-between">
<a
href={`/events/${thread?.rootEventId || thread?.replyEventId}`}

View File

@@ -10,15 +10,15 @@ export function NoteUser({ className }: { className?: string }) {
<User.Provider pubkey={event.pubkey}>
<HoverCard.Root>
<User.Root
className={cn("flex items-center justify-between", className)}
className={cn("flex items-start justify-between", className)}
>
<div className="flex gap-3">
<HoverCard.Trigger>
<User.Avatar className="size-11 shrink-0 rounded-xl object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<User.Avatar className="size-10 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 text-neutral-950 dark:text-neutral-50" />
<User.NIP05 className="text-neutral-600 dark:text-neutral-400" />
<User.Name className="font-semibold leading-tight text-neutral-950 dark:text-neutral-50" />
<User.NIP05 className="leading-tight text-neutral-600 dark:text-neutral-400" />
</div>
</div>
<User.Time

View File

@@ -2,36 +2,36 @@ import { cn } from "@lume/utils";
import { useUserContext } from "./provider";
export function UserAbout({ className }: { className?: string }) {
const user = useUserContext();
const user = useUserContext();
if (!user.profile) {
return (
<div className="flex flex-col gap-1">
<div
className={cn(
"h-4 w-20 bg-black/20 dark:bg-white/20 rounded animate-pulse",
className,
)}
/>
<div
className={cn(
"h-4 w-full bg-black/20 dark:bg-white/20 rounded animate-pulse",
className,
)}
/>
<div
className={cn(
"h-4 w-24 bg-black/20 dark:bg-white/20 rounded animate-pulse",
className,
)}
/>
</div>
);
}
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("select-text break-p", className)}>
{user.profile.about?.trim() || "No bio"}
</div>
);
return (
<div className={cn("content-break select-text", className)}>
{user.profile.about?.trim() || "No bio"}
</div>
);
}

View File

@@ -9,11 +9,11 @@ export function UserNip05({ className }: { className?: string }) {
const user = useUserContext();
const { isLoading, data: verified } = useQuery({
queryKey: ["nip05", user?.profile.nip05],
queryKey: ["nip05", user?.pubkey],
queryFn: async () => {
if (!user.profile?.nip05) return false;
const verify = await ark.verify_nip05(user.pubkey, user.profile?.nip05);
console.log(verify);
return verify;
},
enabled: !!user.profile,