From cdf29f8a54b5de29a850c65d33805545386105ef Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 15 Feb 2024 12:47:15 +0700 Subject: [PATCH] wip: update --- apps/desktop2/src/routes/app/home.lazy.tsx | 29 +- packages/ark/src/ark.ts | 12 + packages/ui/src/index.ts | 4 + packages/ui/src/note/buttons/reply.tsx | 48 ++- packages/ui/src/note/child.tsx | 7 +- packages/ui/src/note/content.tsx | 413 ++++++++++----------- packages/ui/src/note/mentions/note.tsx | 13 +- packages/ui/src/note/mentions/user.tsx | 7 +- packages/ui/src/note/menu.tsx | 9 +- packages/ui/src/note/primitives/text.tsx | 59 +-- packages/ui/src/note/thread.tsx | 7 +- packages/ui/src/note/user.tsx | 25 +- packages/ui/src/user/nip05.tsx | 71 ++-- src-tauri/src/nostr/keys.rs | 12 + 14 files changed, 369 insertions(+), 347 deletions(-) diff --git a/apps/desktop2/src/routes/app/home.lazy.tsx b/apps/desktop2/src/routes/app/home.lazy.tsx index 75dd53ae..eb2ce487 100644 --- a/apps/desktop2/src/routes/app/home.lazy.tsx +++ b/apps/desktop2/src/routes/app/home.lazy.tsx @@ -1,12 +1,12 @@ import { useArk } from "@lume/ark"; import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons"; import { Event, Kind } from "@lume/types"; -import { EmptyFeed } from "@lume/ui"; +import { EmptyFeed, TextNote } from "@lume/ui"; import { FETCH_LIMIT } from "@lume/utils"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; import { useEffect, useMemo, useRef } from "react"; -import { CacheSnapshot, VList, VListHandle } from "virtua"; +import { CacheSnapshot, Virtualizer, VListHandle } from "virtua"; export const Route = createLazyFileRoute("/app/home")({ component: Home, @@ -34,21 +34,18 @@ function Home() { getNextPageParam: (lastPage) => { const lastEvent = lastPage.at(-1); if (!lastEvent) return; - return lastEvent.created_at - 1; + return lastEvent.created_at; }, select: (data) => data?.pages.flatMap((page) => page), refetchOnWindowFocus: false, - refetchOnMount: false, }); const renderItem = (event: Event) => { switch (event.kind) { case Kind.Text: - return

{event.content}

; - case Kind.Repost: - return

{event.content}

; + return ; default: - return

{event.content}

; + return ; } }; @@ -70,25 +67,27 @@ function Home() { return (
-
- +
+
{isLoading ? ( -
+
) : !data.length ? ( -
+ ) : ( - data.map((item) => renderItem(item)) + + {data.map((item) => renderItem(item))} + )}
{hasNextPage ? ( @@ -109,7 +108,7 @@ function Home() { ) : null}
- +
); diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 266efbe6..b95839bb 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -190,4 +190,16 @@ export class Ark { console.error(String(e)); } } + + public async verify_nip05(pubkey: string, nip05: string) { + try { + const cmd: boolean = await invoke("verify_nip05", { + key: pubkey, + nip05, + }); + return cmd; + } catch { + return false; + } + } } diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 5a137cf9..54e1bf39 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -3,6 +3,10 @@ export * from "./user"; export * from "./note"; export * from "./column"; +// Note Primities +export * from "./note/primitives/text"; +export * from "./note/primitives/repost"; + // Deprecated export * from "./routes/event"; export * from "./routes/user"; diff --git a/packages/ui/src/note/buttons/reply.tsx b/packages/ui/src/note/buttons/reply.tsx index 1371e4ac..38fa7750 100644 --- a/packages/ui/src/note/buttons/reply.tsx +++ b/packages/ui/src/note/buttons/reply.tsx @@ -1,34 +1,30 @@ import { ReplyIcon } from "@lume/icons"; import * as Tooltip from "@radix-ui/react-tooltip"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { useNoteContext } from "../provider"; export function NoteReply() { - const event = useNoteContext(); - const navigate = useNavigate(); + const event = useNoteContext(); + const { t } = useTranslation(); - const { t } = useTranslation(); - - return ( - - - - - - - - {t("note.menu.viewThread")} - - - - - - ); + return ( + + + + + + + + {t("note.menu.viewThread")} + + + + + + ); } diff --git a/packages/ui/src/note/child.tsx b/packages/ui/src/note/child.tsx index 6882f4da..50bcc462 100644 --- a/packages/ui/src/note/child.tsx +++ b/packages/ui/src/note/child.tsx @@ -2,7 +2,6 @@ import { NOSTR_MENTIONS } from "@lume/utils"; import { nanoid } from "nanoid"; import { ReactNode, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; import reactStringReplace from "react-string-replace"; import { User } from "../user"; import { Hashtag } from "./mentions/hashtag"; @@ -61,15 +60,15 @@ export function NoteChild({ (match, i) => { const url = new URL(match); return ( - {url.toString()} - + ); }, ); diff --git a/packages/ui/src/note/content.tsx b/packages/ui/src/note/content.tsx index 0a70129c..67798091 100644 --- a/packages/ui/src/note/content.tsx +++ b/packages/ui/src/note/content.tsx @@ -1,20 +1,19 @@ import { useStorage } from "@lume/storage"; import { Kind } from "@lume/types"; import { - AUDIOS, - IMAGES, - NOSTR_EVENTS, - NOSTR_MENTIONS, - VIDEOS, - canPreview, - cn, - regionNames, + AUDIOS, + IMAGES, + NOSTR_EVENTS, + NOSTR_MENTIONS, + VIDEOS, + canPreview, + cn, + regionNames, } from "@lume/utils"; import { fetch } from "@tauri-apps/plugin-http"; import getUrls from "get-urls"; import { nanoid } from "nanoid"; import { ReactNode, useMemo, useState } from "react"; -import { Link } from "react-router-dom"; import reactStringReplace from "react-string-replace"; import { toast } from "sonner"; import { stripHtml } from "string-strip-html"; @@ -27,230 +26,226 @@ import { LinkPreview } from "./preview/link"; import { VideoPreview } from "./preview/video"; import { useNoteContext } from "./provider"; -export function NoteContent({ - className, -}: { - className?: string; -}) { - const storage = useStorage(); - const event = useNoteContext(); +export function NoteContent({ className }: { className?: string }) { + const storage = useStorage(); + const event = useNoteContext(); - const [content, setContent] = useState(event.content); - const [translate, setTranslate] = useState({ - translatable: false, - translated: false, - }); + const [content, setContent] = useState(event.content); + const [translate, setTranslate] = useState({ + translatable: false, + translated: false, + }); - const richContent = useMemo(() => { - if (event.kind !== Kind.Text) return content; + const richContent = useMemo(() => { + if (event.kind !== Kind.Text) return content; - let parsedContent: string | ReactNode[] = stripHtml( - content.replace(/\n{2,}\s*/g, "\n"), - ).result; - let linkPreview: string = undefined; - let images: string[] = []; - let videos: string[] = []; - let audios: string[] = []; - let events: string[] = []; + let parsedContent: string | ReactNode[] = stripHtml( + content.replace(/\n{2,}\s*/g, "\n"), + ).result; + let linkPreview: string = undefined; + let images: string[] = []; + let videos: string[] = []; + let audios: string[] = []; + let events: string[] = []; - const text = parsedContent; - const words = text.split(/( |\n)/); - const urls = [...getUrls(text)]; + const text = parsedContent; + const words = text.split(/( |\n)/); + const urls = [...getUrls(text)]; - if (storage.settings.media && !storage.settings.lowPower) { - 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; - }), - ); - } + if (storage.settings.media && !storage.settings.lowPower) { + 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)), - ); + events = words.filter((word) => + NOSTR_EVENTS.some((el) => word.startsWith(el)), + ); - const hashtags = words.filter((word) => word.startsWith("#")); - const mentions = words.filter((word) => - NOSTR_MENTIONS.some((el) => word.startsWith(el)), - ); + const hashtags = words.filter((word) => word.startsWith("#")); + 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) => , - ); - } - } + 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 (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 (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"); - parsedContent = reactStringReplace(parsedContent, regex, () => { - if (storage.settings.hashtag) - return ; - return null; - }); - } - } + if (hashtags.length) { + for (const hashtag of hashtags) { + const regex = new RegExp(`(|^)${hashtag}\\b`, "g"); + parsedContent = reactStringReplace(parsedContent, regex, () => { + if (storage.settings.hashtag) + return ; + return null; + }); + } + } - if (events.length) { - for (const event of events) { - parsedContent = reactStringReplace( - parsedContent, - event, - (match, i) => , - ); - } - } + if (events.length) { + for (const event of events) { + parsedContent = reactStringReplace( + parsedContent, + event, + (match, i) => , + ); + } + } - if (mentions.length) { - for (const mention of mentions) { - parsedContent = reactStringReplace( - parsedContent, - mention, - (match, i) => , - ); - } - } + if (mentions.length) { + for (const mention of mentions) { + parsedContent = reactStringReplace( + parsedContent, + mention, + (match, i) => , + ); + } + } - parsedContent = reactStringReplace( - parsedContent, - /(https?:\/\/\S+)/g, - (match, i) => { - const url = new URL(match); + parsedContent = reactStringReplace( + parsedContent, + /(https?:\/\/\S+)/g, + (match, i) => { + const url = new URL(match); - if (!linkPreview && canPreview(match)) { - linkPreview = match; - return ; - } + if (!linkPreview && canPreview(match)) { + linkPreview = match; + return ; + } - return ( - - {url.toString()} - - ); - }, - ); + return ( + + {url.toString()} + + ); + }, + ); - parsedContent = reactStringReplace(parsedContent, "\n", () => { - return
; - }); + parsedContent = reactStringReplace(parsedContent, "\n", () => { + return
; + }); - if (typeof parsedContent[0] === "string") { - parsedContent[0] = parsedContent[0].trimStart(); - } + if (typeof parsedContent[0] === "string") { + parsedContent[0] = parsedContent[0].trimStart(); + } - return parsedContent; - } catch (e) { - console.warn(event.id, `[parser] parse failed: ${e}`); - return parsedContent; - } - }, [content]); + return parsedContent; + } catch (e) { + console.warn(event.id, `[parser] parse failed: ${e}`); + return parsedContent; + } + }, [content]); - const translateContent = async () => { - try { - if (!translate.translatable) return; + const translateContent = async () => { + try { + if (!translate.translatable) return; - const res = await fetch("https://translate.nostr.wine/translate", { - method: "POST", - body: JSON.stringify({ - q: event.content, - target: storage.locale.slice(0, 2), - api_key: storage.settings.translateApiKey, - }), - headers: { "Content-Type": "application/json" }, - }); + const res = await fetch("https://translate.nostr.wine/translate", { + method: "POST", + body: JSON.stringify({ + q: event.content, + target: storage.locale.slice(0, 2), + api_key: storage.settings.translateApiKey, + }), + headers: { "Content-Type": "application/json" }, + }); - if (!res.ok) - toast.error( - "Cannot connect to translate service, please try again later.", - ); + if (!res.ok) + toast.error( + "Cannot connect to translate service, please try again later.", + ); - const data = await res.json(); + const data = await res.json(); - setContent(data.translatedText); - setTranslate((state) => ({ ...state, translated: true })); - } catch (e) { - console.error("translate api: ", String(e)); - } - }; + setContent(data.translatedText); + setTranslate((state) => ({ ...state, translated: true })); + } catch (e) { + console.error("translate api: ", String(e)); + } + }; - if (event.kind !== Kind.Text) { - return ; - } + if (event.kind !== Kind.Text) { + return ; + } - return ( -
-
- {richContent} -
- {storage.settings.translation && translate.translatable ? ( - translate.translated ? ( - - ) : ( - - ) - ) : null} -
- ); + return ( +
+
+ {richContent} +
+ {storage.settings.translation && translate.translatable ? ( + translate.translated ? ( + + ) : ( + + ) + ) : null} +
+ ); } diff --git a/packages/ui/src/note/mentions/note.tsx b/packages/ui/src/note/mentions/note.tsx index 637bdbcf..dabf035f 100644 --- a/packages/ui/src/note/mentions/note.tsx +++ b/packages/ui/src/note/mentions/note.tsx @@ -2,7 +2,6 @@ import { PinIcon } from "@lume/icons"; import { NOSTR_MENTIONS } from "@lume/utils"; import { ReactNode, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; import reactStringReplace from "react-string-replace"; import { User } from "../../user"; import { Hashtag } from "./hashtag"; @@ -64,15 +63,15 @@ export function MentionNote({ (match, i) => { const url = new URL(match); return ( - {url.toString()} - + ); }, ); @@ -126,12 +125,12 @@ export function MentionNote({
{openable ? (
- {t("note.showMore")} - + - {t("note.menu.viewAuthor")} - +