From cfcb9bc6edefa3327a52a5faa054a04fac8e9b8b Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 29 Feb 2024 13:02:16 +0700 Subject: [PATCH] feat: improve ui --- apps/desktop2/src/components/repost.tsx | 9 +- apps/desktop2/src/components/text.tsx | 13 +- .../src/routes/events/$eventId.lazy.tsx | 79 ++++---- .../src/routes/events/-components/reply.tsx | 92 ++++------ .../routes/events/-components/replyList.tsx | 7 +- .../routes/events/-components/subReply.tsx | 30 ++- packages/ark/src/ark.ts | 11 +- packages/icons/index.ts | 1 + packages/icons/src/link.tsx | 24 +++ packages/ui/src/box.tsx | 20 ++ packages/ui/src/container.tsx | 26 +++ packages/ui/src/index.ts | 5 +- packages/ui/src/note/buttons/reply.tsx | 2 +- packages/ui/src/note/buttons/repost.tsx | 2 +- packages/ui/src/note/buttons/zap.tsx | 2 +- packages/ui/src/note/child.tsx | 8 +- packages/ui/src/note/content.tsx | 171 ++++++------------ packages/ui/src/note/mentions/note.tsx | 19 +- packages/ui/src/note/mentions/user.tsx | 47 ++--- packages/ui/src/note/menu.tsx | 2 +- packages/ui/src/note/preview/image.tsx | 11 +- packages/ui/src/note/preview/video.tsx | 2 +- packages/ui/src/note/thread.tsx | 8 +- packages/ui/src/note/user.tsx | 30 +-- packages/ui/src/user/name.tsx | 9 +- packages/ui/src/user/time.tsx | 6 +- pnpm-lock.yaml | 116 +++++++++--- 27 files changed, 408 insertions(+), 344 deletions(-) create mode 100644 packages/icons/src/link.tsx create mode 100644 packages/ui/src/box.tsx create mode 100644 packages/ui/src/container.tsx diff --git a/apps/desktop2/src/components/repost.tsx b/apps/desktop2/src/components/repost.tsx index de7e9746..cca86d0f 100644 --- a/apps/desktop2/src/components/repost.tsx +++ b/apps/desktop2/src/components/repost.tsx @@ -71,7 +71,7 @@ export function RepostNote({ return ( @@ -96,14 +96,13 @@ export function RepostNote({
-
- -
+
+
-
+
diff --git a/apps/desktop2/src/components/text.tsx b/apps/desktop2/src/components/text.tsx index 71a9123c..fab9e334 100644 --- a/apps/desktop2/src/components/text.tsx +++ b/apps/desktop2/src/components/text.tsx @@ -13,7 +13,7 @@ export function TextNote({ @@ -21,16 +21,15 @@ export function TextNote({
- - -
- -
+ + +
+
-
+
diff --git a/apps/desktop2/src/routes/events/$eventId.lazy.tsx b/apps/desktop2/src/routes/events/$eventId.lazy.tsx index 3b6cc2d5..d62554c0 100644 --- a/apps/desktop2/src/routes/events/$eventId.lazy.tsx +++ b/apps/desktop2/src/routes/events/$eventId.lazy.tsx @@ -1,9 +1,10 @@ import { useEvent } from "@lume/ark"; import { LoaderIcon } from "@lume/icons"; -import { Note, User } from "@lume/ui"; +import { Box, Container, Note, User } from "@lume/ui"; import { createLazyFileRoute } from "@tanstack/react-router"; import { WindowVirtualizer } from "virtua"; import { ReplyList } from "./-components/replyList"; +import { Event } from "@lume/types"; export const Route = createLazyFileRoute("/events/$eventId")({ component: Event, @@ -29,45 +30,43 @@ function Event() { return ( -
-
-
-
-
- - -
- - - -
- -
- - · - -
-
-
-
- -
- - -
- -
- - -
-
-
-
- {data ? : null} -
-
-
-
+ + + + {data ? : null} + + ); } + +function MainNote({ data }: { data: Event }) { + return ( + + + + + +
+ +
+ + · + +
+
+
+
+ + +
+
+ + +
+ +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/events/-components/reply.tsx b/apps/desktop2/src/routes/events/-components/reply.tsx index 8083299f..5c01c9fb 100644 --- a/apps/desktop2/src/routes/events/-components/reply.tsx +++ b/apps/desktop2/src/routes/events/-components/reply.tsx @@ -1,65 +1,47 @@ -import { NavArrowDownIcon } from "@lume/icons"; import { EventWithReplies } from "@lume/types"; import { cn } from "@lume/utils"; -import * as Collapsible from "@radix-ui/react-collapsible"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Note } from "@lume/ui"; +import { Note, User } from "@lume/ui"; import { SubReply } from "./subReply"; export function Reply({ event }: { event: EventWithReplies }) { - const [t] = useTranslation(); - const [open, setOpen] = useState(false); - return ( - - - -
- - -
- -
- {event.replies?.length > 0 ? ( - -
- - {`${event.replies?.length} ${ - event.replies?.length === 1 - ? t("note.reply.single") - : t("note.reply.plural") - }`} -
-
- ) : ( -
- )} -
- - - + + + + +
+ +
+ + +
+ +
+
+ +
+
+ + +
-
- {event.replies?.length > 0 ? ( - - {event.replies?.map((childEvent) => ( - - ))} - - ) : null} -
- - - + +
+
0 + ? "my-3 mt-6 flex flex-col gap-3 divide-y divide-neutral-100 border-l-2 border-neutral-100 pl-6 dark:divide-neutral-900 dark:border-neutral-900" + : "", + )} + > + {event.replies?.length > 0 + ? event.replies?.map((childEvent) => ( + + )) + : null} +
+
+
); } diff --git a/apps/desktop2/src/routes/events/-components/replyList.tsx b/apps/desktop2/src/routes/events/-components/replyList.tsx index 2ab517e6..f1ba20b4 100644 --- a/apps/desktop2/src/routes/events/-components/replyList.tsx +++ b/apps/desktop2/src/routes/events/-components/replyList.tsx @@ -27,12 +27,7 @@ export function ReplyList({ }, [eventId]); return ( -
+
{!data ? (
diff --git a/apps/desktop2/src/routes/events/-components/subReply.tsx b/apps/desktop2/src/routes/events/-components/subReply.tsx index a8558edf..3bdc9a50 100644 --- a/apps/desktop2/src/routes/events/-components/subReply.tsx +++ b/apps/desktop2/src/routes/events/-components/subReply.tsx @@ -1,18 +1,30 @@ import { Event } from "@lume/types"; -import { Note } from "@lume/ui"; +import { Note, User } from "@lume/ui"; export function SubReply({ event }: { event: Event; rootEventId?: string }) { return ( - -
- - -
+ + + +
+ +
+ + +
+
+ +
+
-
- - +
+
+ + + +
+
diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index e34c6cb9..c9f9d803 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -394,15 +394,6 @@ export class Ark { } } - public async get_nwc_status() { - try { - const cmd: boolean = await invoke("get_nwc_status"); - return cmd; - } catch { - return false; - } - } - public async set_nwc(uri: string) { try { const cmd: boolean = await invoke("set_nwc", { uri }); @@ -490,7 +481,7 @@ export class Ark { title: "Thread", url: `/events/${id}`, minWidth: 500, - width: 500, + width: 600, height: 800, hiddenTitle: true, titleBarStyle: "overlay", diff --git a/packages/icons/index.ts b/packages/icons/index.ts index 7c7bbbc2..19a36255 100644 --- a/packages/icons/index.ts +++ b/packages/icons/index.ts @@ -115,3 +115,4 @@ export * from "./src/searchFilled"; export * from "./src/arrowUp"; export * from "./src/arrowUpSquare"; export * from "./src/arrowDown"; +export * from "./src/link"; diff --git a/packages/icons/src/link.tsx b/packages/icons/src/link.tsx new file mode 100644 index 00000000..6667aa06 --- /dev/null +++ b/packages/icons/src/link.tsx @@ -0,0 +1,24 @@ +import { SVGProps } from "react"; + +export function LinkIcon( + props: JSX.IntrinsicAttributes & SVGProps, +) { + return ( + + + + ); +} diff --git a/packages/ui/src/box.tsx b/packages/ui/src/box.tsx new file mode 100644 index 00000000..2704375f --- /dev/null +++ b/packages/ui/src/box.tsx @@ -0,0 +1,20 @@ +import { cn } from "@lume/utils"; +import { ReactNode } from "react"; + +export function Box({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +
+
+
+ {children} +
+
+
+ ); +} diff --git a/packages/ui/src/container.tsx b/packages/ui/src/container.tsx new file mode 100644 index 00000000..521cbbd6 --- /dev/null +++ b/packages/ui/src/container.tsx @@ -0,0 +1,26 @@ +import { cn } from "@lume/utils"; +import { ReactNode } from "react"; + +export function Container({ + children, + withDrag = false, + className, +}: { + children: ReactNode; + withDrag?: boolean; + className?: string; +}) { + return ( +
+ {withDrag ? ( +
+ ) : null} + {children} +
+ ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index f9213137..8c409699 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,5 +1,8 @@ -// New export * from "./user"; export * from "./note"; export * from "./column"; export * from "./emptyFeed"; + +// UI +export * from "./container"; +export * from "./box"; diff --git a/packages/ui/src/note/buttons/reply.tsx b/packages/ui/src/note/buttons/reply.tsx index ba55f974..34c01870 100644 --- a/packages/ui/src/note/buttons/reply.tsx +++ b/packages/ui/src/note/buttons/reply.tsx @@ -19,7 +19,7 @@ export function NoteReply() { diff --git a/packages/ui/src/note/buttons/repost.tsx b/packages/ui/src/note/buttons/repost.tsx index f0f348da..544242cf 100644 --- a/packages/ui/src/note/buttons/repost.tsx +++ b/packages/ui/src/note/buttons/repost.tsx @@ -43,7 +43,7 @@ export function NoteRepost() { diff --git a/packages/ui/src/note/child.tsx b/packages/ui/src/note/child.tsx index 0103f387..04980630 100644 --- a/packages/ui/src/note/child.tsx +++ b/packages/ui/src/note/child.tsx @@ -109,11 +109,11 @@ export function NoteChild({ -
- -
+
+ {" "} + {isRoot ? t("note.posted") : t("note.replied")}: -
+
diff --git a/packages/ui/src/note/content.tsx b/packages/ui/src/note/content.tsx index 735c115f..8cdd3375 100644 --- a/packages/ui/src/note/content.tsx +++ b/packages/ui/src/note/content.tsx @@ -8,104 +8,38 @@ import { canPreview, cn, } from "@lume/utils"; -import getUrls from "get-urls"; -import { nanoid } from "nanoid"; +import { NIP89 } from "./nip89"; +import { useNoteContext } from "./provider"; import { ReactNode, useMemo } from "react"; import reactStringReplace from "react-string-replace"; -import { stripHtml } from "string-strip-html"; -import { Hashtag } from "./mentions/hashtag"; -import { MentionNote } from "./mentions/note"; +import { nanoid } from "nanoid"; import { MentionUser } from "./mentions/user"; -import { NIP89 } from "./nip89"; -import { ImagePreview } from "./preview/image"; -import { LinkPreview } from "./preview/link"; +import { MentionNote } from "./mentions/note"; +import { Hashtag } from "./mentions/hashtag"; import { VideoPreview } from "./preview/video"; -import { useNoteContext } from "./provider"; +import { stripHtml } from "string-strip-html"; +import getUrl from "get-urls"; +import { ImagePreview } from "./preview/image"; export function NoteContent({ className }: { className?: string }) { const event = useNoteContext(); - - const richContent = useMemo(() => { - if (event.kind !== Kind.Text) return event.content; - - let parsedContent: string | ReactNode[] = stripHtml(event.content).result; - let linkPreview: string = undefined; - let images: string[] = []; - let videos: string[] = []; - let audios: string[] = []; - let events: string[] = []; - - const text = parsedContent; + const content = useMemo(() => { + const text = stripHtml(event.content.trim()).result; const words = text.split(/( |\n)/); - const urls = [...getUrls(text)]; + const urls = [...getUrl(text)]; - images = urls.filter((word) => - IMAGES.some((el) => { - const url = new URL(word); - const extension = url.pathname.split(".")[1]; - if (extension === el) return true; - return false; - }), - ); - - videos = urls.filter((word) => - VIDEOS.some((el) => { - const url = new URL(word); - const extension = url.pathname.split(".")[1]; - if (extension === el) return true; - return false; - }), - ); - - audios = urls.filter((word) => - AUDIOS.some((el) => { - const url = new URL(word); - const extension = url.pathname.split(".")[1]; - if (extension === el) return true; - return false; - }), - ); - - events = words.filter((word) => - NOSTR_EVENTS.some((el) => word.startsWith(el)), - ); + // @ts-ignore, kaboom !!! + let parsedContent: ReactNode[] = text; const hashtags = words.filter((word) => word.startsWith("#")); + const events = words.filter((word) => + NOSTR_EVENTS.some((el) => word.startsWith(el)), + ); const mentions = words.filter((word) => NOSTR_MENTIONS.some((el) => word.startsWith(el)), ); try { - if (images.length) { - for (const image of images) { - parsedContent = reactStringReplace( - parsedContent, - image, - (match, i) => , - ); - } - } - - if (videos.length) { - for (const video of videos) { - parsedContent = reactStringReplace( - parsedContent, - video, - (match, i) => , - ); - } - } - - if (audios.length) { - for (const audio of audios) { - parsedContent = reactStringReplace( - parsedContent, - audio, - (match, i) => , - ); - } - } - if (hashtags.length) { for (const hashtag of hashtags) { const regex = new RegExp(`(|^)${hashtag}\\b`, "g"); @@ -137,41 +71,54 @@ export function NoteContent({ className }: { className?: string }) { parsedContent = reactStringReplace( parsedContent, - /(https?:\/\/\S+)/g, + /(https?:\/\/\S+)/gi, (match, i) => { - const url = new URL(match); + try { + const url = new URL(match); + const ext = url.pathname.split(".")[1]; - if (!linkPreview && canPreview(match)) { - linkPreview = match; - return ; + if (IMAGES.includes(ext)) { + return ; + } + + if (VIDEOS.includes(ext)) { + return ; + } + + if (AUDIOS.includes(ext)) { + return ; + } + + return ( + + {match} + + ); + } catch { + return ( + + {match} + + ); } - - return ( - - {url.toString()} - - ); }, ); - parsedContent = reactStringReplace(parsedContent, "\n", () => { - return
; - }); - - if (typeof parsedContent[0] === "string") { - parsedContent[0] = parsedContent[0].trimStart(); - } - return parsedContent; } catch (e) { - console.warn(event.id, `[parser] parse failed: ${e}`); - return parsedContent; + return text; } }, []); @@ -180,9 +127,9 @@ export function NoteContent({ className }: { className?: string }) { } return ( -
-
- {richContent} +
+
+ {content}
); diff --git a/packages/ui/src/note/mentions/note.tsx b/packages/ui/src/note/mentions/note.tsx index f1eca4f5..519e6d0f 100644 --- a/packages/ui/src/note/mentions/note.tsx +++ b/packages/ui/src/note/mentions/note.tsx @@ -6,6 +6,8 @@ import { User } from "../../user"; import { Hashtag } from "./hashtag"; import { MentionUser } from "./user"; import { useArk, useEvent } from "@lume/ark"; +import { LinkIcon } from "@lume/icons"; +import { stripHtml } from "string-strip-html"; export function MentionNote({ eventId, @@ -18,14 +20,15 @@ export function MentionNote({ const { isLoading, isError, data } = useEvent(eventId); const ark = useArk(); - const richContent = useMemo(() => { + const content = useMemo(() => { if (!data) return ""; - let parsedContent: string | ReactNode[] = data.content; - - const text = parsedContent as string; + const text = stripHtml(data.content.trim()).result; const words = text.split(/( |\n)/); + // @ts-ignore, kaboom !!! + let parsedContent: ReactNode[] = text; + const hashtags = words.filter((word) => word.startsWith("#")); const mentions = words.filter((word) => NOSTR_MENTIONS.some((el) => word.startsWith(el)), @@ -75,8 +78,7 @@ export function MentionNote({ return parsedContent; } catch (e) { - console.log(e); - return parsedContent; + return text; } }, [data]); @@ -118,16 +120,17 @@ export function MentionNote({
- {richContent} + {content}
{openable ? (
) : ( diff --git a/packages/ui/src/note/mentions/user.tsx b/packages/ui/src/note/mentions/user.tsx index 72d058f2..7f91a985 100644 --- a/packages/ui/src/note/mentions/user.tsx +++ b/packages/ui/src/note/mentions/user.tsx @@ -1,38 +1,21 @@ -import { useProfile } from "@lume/ark"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import { useTranslation } from "react-i18next"; +import { useArk, useProfile } from "@lume/ark"; +import { displayNpub } from "@lume/utils"; export function MentionUser({ pubkey }: { pubkey: string }) { - const { isLoading, isError, user } = useProfile(pubkey); - const { t } = useTranslation(); + const ark = useArk(); + const { isLoading, isError, profile } = useProfile(pubkey); return ( - - - {isLoading - ? "@anon" - : isError - ? pubkey - : `@${user?.name || user?.display_name || user?.name || "anon"}`} - - - - - {t("note.buttons.viewProfile")} - - - - - - - + ); } diff --git a/packages/ui/src/note/menu.tsx b/packages/ui/src/note/menu.tsx index 9680b2ea..bc0e8d90 100644 --- a/packages/ui/src/note/menu.tsx +++ b/packages/ui/src/note/menu.tsx @@ -34,7 +34,7 @@ export function NoteMenu() { diff --git a/packages/ui/src/note/preview/image.tsx b/packages/ui/src/note/preview/image.tsx index ab21d5c5..10bdfcf0 100644 --- a/packages/ui/src/note/preview/image.tsx +++ b/packages/ui/src/note/preview/image.tsx @@ -35,10 +35,7 @@ export function ImagePreview({ url }: { url: string }) { return ( // biome-ignore lint/a11y/useKeyWithClickEvents: -
+
{url} downloadImage(e)} - className="absolute right-2 top-2 z-10 hidden size-10 items-center justify-center rounded-lg bg-white/10 text-black/70 backdrop-blur-2xl hover:bg-blue-500 hover:text-white group-hover:inline-flex" + className="absolute right-2 top-2 z-20 hidden size-8 items-center justify-center rounded-md bg-white/10 text-white/70 backdrop-blur-2xl hover:bg-blue-500 hover:text-white group-hover:inline-flex" > {downloaded ? ( - + ) : ( - + )}
diff --git a/packages/ui/src/note/preview/video.tsx b/packages/ui/src/note/preview/video.tsx index ae6334cb..faa2a897 100644 --- a/packages/ui/src/note/preview/video.tsx +++ b/packages/ui/src/note/preview/video.tsx @@ -9,7 +9,7 @@ import { export function VideoPreview({ url }: { url: string }) { return ( -
+