diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 6d5b86c6..74d5801b 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -24,6 +24,7 @@ "@tanstack/react-router": "^1.20.0", "i18next": "^23.10.1", "i18next-resources-to-backend": "^1.2.0", + "nanoid": "^5.0.6", "nostr-tools": "^2.3.1", "react": "^18.2.0", "react-currency-input-field": "^3.8.0", diff --git a/apps/desktop2/src/components/col.tsx b/apps/desktop2/src/components/col.tsx index 54ced59d..664f946d 100644 --- a/apps/desktop2/src/components/col.tsx +++ b/apps/desktop2/src/components/col.tsx @@ -15,7 +15,7 @@ export function Col({ const window = useMemo(() => getCurrent(), []); const container = useRef(null); - const [webview, setWebview] = useState(""); + const [webview, setWebview] = useState(null); const createWebview = async () => { const rect = container.current.getBoundingClientRect(); @@ -36,6 +36,7 @@ export function Col({ }; const closeWebview = async () => { + if (!webview) return; await invoke("close_column", { label: webview, }); @@ -67,7 +68,7 @@ export function Col({ return () => { closeWebview(); }; - }, [window]); + }, []); return
; } diff --git a/apps/desktop2/src/routes/$account.home.tsx b/apps/desktop2/src/routes/$account.home.tsx index 3a302c99..fc1fb245 100644 --- a/apps/desktop2/src/routes/$account.home.tsx +++ b/apps/desktop2/src/routes/$account.home.tsx @@ -15,7 +15,7 @@ export const Route = createFileRoute("/$account/home")({ const COLS: LumeColumn[] = [ { id: 1, name: "Newsfeed", content: "/newsfeed" }, - { id: 2, name: "Lume Store", content: "/store/official" }, + { id: 9999, name: "Lume Store", content: "/store/official" }, ]; function Screen() { @@ -43,34 +43,36 @@ function Screen() { }); }; - const add = async (column: LumeColumn) => { - setColumns((prev) => [...prev, column]); - vlistRef?.current.scrollToIndex(columns.length); + const add = (column: LumeColumn) => { + const col = columns.find((item) => item.id === column.id); + if (!col) { + setColumns((prev) => [...prev, column]); + } }; - const remove = async (id: number) => { + const remove = (id: number) => { setColumns((prev) => prev.filter((t) => t.id !== id)); }; useEffect(() => { - async function listenUpdateColumn() { + const listenColumnEvent = async () => { const mainWindow = getCurrent(); - unlisten.current = await mainWindow.listen( - "columns", - (data) => { - if (data.payload.type === "add") add(data.payload.column); - if (data.payload.type === "remove") remove(data.payload.id); - }, - ); - } + if (!unlisten) { + unlisten.current = await mainWindow.listen( + "columns", + (data) => { + if (data.payload.type === "add") add(data.payload.column); + if (data.payload.type === "remove") remove(data.payload.id); + }, + ); + } + }; // listen for column changes - listenUpdateColumn(); + listenColumnEvent(); // clean up - return () => { - if (unlisten.current) unlisten.current(); - }; + return () => unlisten.current?.(); }, []); return ( diff --git a/apps/desktop2/src/routes/$account.tsx b/apps/desktop2/src/routes/$account.tsx index 624f1c71..391a26c9 100644 --- a/apps/desktop2/src/routes/$account.tsx +++ b/apps/desktop2/src/routes/$account.tsx @@ -1,4 +1,4 @@ -import { ComposeFilledIcon, HorizontalDotsIcon } from "@lume/icons"; +import { ComposeFilledIcon, HorizontalDotsIcon, PlusIcon } from "@lume/icons"; import { Outlet, createFileRoute } from "@tanstack/react-router"; import { cn } from "@lume/utils"; import { Accounts } from "@/components/accounts"; @@ -27,7 +27,7 @@ function App() { type="button" className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600" > - +
diff --git a/apps/desktop2/src/routes/antenas.lazy.tsx b/apps/desktop2/src/routes/antenas.lazy.tsx new file mode 100644 index 00000000..abcc27a6 --- /dev/null +++ b/apps/desktop2/src/routes/antenas.lazy.tsx @@ -0,0 +1,85 @@ +import { RepostNote } from "@/components/repost"; +import { Suggest } from "@/components/suggest"; +import { TextNote } from "@/components/text"; +import { useEvents } from "@lume/ark"; +import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; +import { Event, Kind } from "@lume/types"; +import { Column } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; +import { Virtualizer } from "virtua"; + +export const Route = createLazyFileRoute("/antenas")({ + component: Screen, +}); + +export function Screen() { + // @ts-ignore, just work!!! + const { name, account } = Route.useSearch(); + const { t } = useTranslation(); + const { + data, + hasNextPage, + isLoading, + isRefetching, + isFetchingNextPage, + fetchNextPage, + } = useEvents("local", account); + + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( + + + + {isLoading || isRefetching ? ( +
+ +
+ ) : !data.length ? ( +
+
+ +
+

{t("emptyFeedTitle")}

+

{t("emptyFeedSubtitle")}

+
+
+ +
+ ) : ( + + {data.map((item) => renderItem(item))} + + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/foryou.lazy.tsx b/apps/desktop2/src/routes/foryou.lazy.tsx new file mode 100644 index 00000000..1ba6360c --- /dev/null +++ b/apps/desktop2/src/routes/foryou.lazy.tsx @@ -0,0 +1,85 @@ +import { RepostNote } from "@/components/repost"; +import { Suggest } from "@/components/suggest"; +import { TextNote } from "@/components/text"; +import { useEvents } from "@lume/ark"; +import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; +import { Event, Kind } from "@lume/types"; +import { Column } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; +import { Virtualizer } from "virtua"; + +export const Route = createLazyFileRoute("/foryou")({ + component: Screen, +}); + +export function Screen() { + // @ts-ignore, just work!!! + const { name, account } = Route.useSearch(); + const { t } = useTranslation(); + const { + data, + hasNextPage, + isLoading, + isRefetching, + isFetchingNextPage, + fetchNextPage, + } = useEvents("local", account); + + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( + + + + {isLoading || isRefetching ? ( +
+ +
+ ) : !data.length ? ( +
+
+ +
+

{t("emptyFeedTitle")}

+

{t("emptyFeedSubtitle")}

+
+
+ +
+ ) : ( + + {data.map((item) => renderItem(item))} + + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/global.lazy.tsx b/apps/desktop2/src/routes/global.lazy.tsx new file mode 100644 index 00000000..96eb3ab8 --- /dev/null +++ b/apps/desktop2/src/routes/global.lazy.tsx @@ -0,0 +1,85 @@ +import { RepostNote } from "@/components/repost"; +import { Suggest } from "@/components/suggest"; +import { TextNote } from "@/components/text"; +import { useEvents } from "@lume/ark"; +import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; +import { Event, Kind } from "@lume/types"; +import { Column } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; +import { Virtualizer } from "virtua"; + +export const Route = createLazyFileRoute("/global")({ + component: Screen, +}); + +export function Screen() { + // @ts-ignore, just work!!! + const { name, account } = Route.useSearch(); + const { t } = useTranslation(); + const { + data, + hasNextPage, + isLoading, + isRefetching, + isFetchingNextPage, + fetchNextPage, + } = useEvents("global", account); + + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( + + + + {isLoading || isRefetching ? ( +
+ +
+ ) : !data.length ? ( +
+
+ +
+

{t("emptyFeedTitle")}

+

{t("emptyFeedSubtitle")}

+
+
+ +
+ ) : ( + + {data.map((item) => renderItem(item))} + + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/group.lazy.tsx b/apps/desktop2/src/routes/group.lazy.tsx new file mode 100644 index 00000000..f3edabdb --- /dev/null +++ b/apps/desktop2/src/routes/group.lazy.tsx @@ -0,0 +1,85 @@ +import { RepostNote } from "@/components/repost"; +import { Suggest } from "@/components/suggest"; +import { TextNote } from "@/components/text"; +import { useEvents } from "@lume/ark"; +import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; +import { Event, Kind } from "@lume/types"; +import { Column } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; +import { Virtualizer } from "virtua"; + +export const Route = createLazyFileRoute("/group")({ + component: Screen, +}); + +export function Screen() { + // @ts-ignore, just work!!! + const { name, account } = Route.useSearch(); + const { t } = useTranslation(); + const { + data, + hasNextPage, + isLoading, + isRefetching, + isFetchingNextPage, + fetchNextPage, + } = useEvents("local", account); + + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( + + + + {isLoading || isRefetching ? ( +
+ +
+ ) : !data.length ? ( +
+
+ +
+

{t("emptyFeedTitle")}

+

{t("emptyFeedSubtitle")}

+
+
+ +
+ ) : ( + + {data.map((item) => renderItem(item))} + + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/store.community.tsx b/apps/desktop2/src/routes/store.community.tsx index ab8e488e..30c2b145 100644 --- a/apps/desktop2/src/routes/store.community.tsx +++ b/apps/desktop2/src/routes/store.community.tsx @@ -6,7 +6,7 @@ export const Route = createFileRoute("/store/community")({ function Screen() { return ( -
+

Coming Soon

); diff --git a/apps/desktop2/src/routes/store.official.tsx b/apps/desktop2/src/routes/store.official.tsx index e43857a3..793f794e 100644 --- a/apps/desktop2/src/routes/store.official.tsx +++ b/apps/desktop2/src/routes/store.official.tsx @@ -1,55 +1,104 @@ +import { LumeColumn } from "@lume/types"; import { createFileRoute } from "@tanstack/react-router"; +import { getCurrent } from "@tauri-apps/api/window"; export const Route = createFileRoute("/store/official")({ component: Screen, + loader: () => { + const columns: LumeColumn[] = [ + { + id: 10000, + name: "For you", + content: "/foryou", + logo: "", + cover: "", + author: "Lume", + description: "Keep up to date with content based on your interests.", + }, + { + id: 10001, + name: "Group Feeds", + content: "/group", + logo: "", + cover: "", + author: "Lume", + description: "Collective of people you're interested in.", + }, + { + id: 10002, + name: "Antenas", + content: "/antenas", + logo: "", + cover: "", + author: "Lume", + description: "Keep track to specific content.", + }, + { + id: 10003, + name: "Trending", + content: "/trending", + logo: "", + cover: "", + author: "Lume", + description: "What is trending on Nostr?.", + }, + { + id: 10004, + name: "Global", + content: "/global", + logo: "", + cover: "", + author: "Lume", + description: "All events from connected relays.", + }, + { + id: 10005, + name: "Waifu", + content: "/waifu", + logo: "", + cover: "", + author: "Lume", + description: "Show a random waifu image to boost your morale.", + }, + ]; + return columns; + }, }); function Screen() { - /* - const add = async (column: LumeColumn) => { + const data = Route.useLoaderData(); + + const install = async (column: LumeColumn) => { const mainWindow = getCurrent(); await mainWindow.emit("columns", { type: "add", column }); }; - */ return ( -
-
-
-
-
-

Group Feeds

-

- Collective of people you're interested in. -

+
+ {data.map((column) => ( +
+
+
+
+

{column.name}

+

+ {column.description} +

+
+
-
-
-
-
-
-
-

Antenas

-

- Keep track to specific content. -

-
- -
-
-
+ ))}
); } diff --git a/apps/desktop2/src/routes/store.tsx b/apps/desktop2/src/routes/store.tsx index f83c7b66..9c2dcbb3 100644 --- a/apps/desktop2/src/routes/store.tsx +++ b/apps/desktop2/src/routes/store.tsx @@ -1,4 +1,4 @@ -import { GroupFeedsIcon, LaurelIcon } from "@lume/icons"; +import { GlobalIcon, LaurelIcon } from "@lume/icons"; import { Column } from "@lume/ui"; import { cn } from "@lume/utils"; import { Link } from "@tanstack/react-router"; @@ -38,7 +38,7 @@ function Screen() { : "opacity-50", )} > - + Community
)} diff --git a/apps/desktop2/src/routes/trending.lazy.tsx b/apps/desktop2/src/routes/trending.lazy.tsx new file mode 100644 index 00000000..b0ba05e5 --- /dev/null +++ b/apps/desktop2/src/routes/trending.lazy.tsx @@ -0,0 +1,85 @@ +import { RepostNote } from "@/components/repost"; +import { Suggest } from "@/components/suggest"; +import { TextNote } from "@/components/text"; +import { useEvents } from "@lume/ark"; +import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; +import { Event, Kind } from "@lume/types"; +import { Column } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; +import { Virtualizer } from "virtua"; + +export const Route = createLazyFileRoute("/trending")({ + component: Screen, +}); + +export function Screen() { + // @ts-ignore, just work!!! + const { name, account } = Route.useSearch(); + const { t } = useTranslation(); + const { + data, + hasNextPage, + isLoading, + isRefetching, + isFetchingNextPage, + fetchNextPage, + } = useEvents("local", account); + + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( + + + + {isLoading || isRefetching ? ( +
+ +
+ ) : !data.length ? ( +
+
+ +
+

{t("emptyFeedTitle")}

+

{t("emptyFeedSubtitle")}

+
+
+ +
+ ) : ( + + {data.map((item) => renderItem(item))} + + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/waifu.lazy.tsx b/apps/desktop2/src/routes/waifu.lazy.tsx new file mode 100644 index 00000000..52686c4a --- /dev/null +++ b/apps/desktop2/src/routes/waifu.lazy.tsx @@ -0,0 +1,18 @@ +import { Column } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; + +export const Route = createLazyFileRoute("/waifu")({ + component: Screen, +}); + +function Screen() { + // @ts-ignore, just work!!! + const { name } = Route.useSearch(); + + return ( + + + waifu + + ); +} diff --git a/packages/icons/src/global.tsx b/packages/icons/src/global.tsx index d224cb9f..4428cb73 100644 --- a/packages/icons/src/global.tsx +++ b/packages/icons/src/global.tsx @@ -2,12 +2,11 @@ export function GlobalIcon(props: JSX.IntrinsicElements["svg"]) { return ( - ); diff --git a/packages/icons/src/plus.tsx b/packages/icons/src/plus.tsx index f54d371b..c5db0751 100644 --- a/packages/icons/src/plus.tsx +++ b/packages/icons/src/plus.tsx @@ -4,8 +4,8 @@ export function PlusIcon(props: JSX.IntrinsicElements["svg"]) { ); diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 9c91d000..90eb9f74 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -82,11 +82,12 @@ export interface RichContent { export interface LumeColumn { id: number; - content: string; name: string; + content: URL | string; description?: string; author?: string; - logo?: string; + logo?: URL | string; + cover?: URL | string; } export interface EventColumns { diff --git a/packages/ui/src/note/mentions/note.tsx b/packages/ui/src/note/mentions/note.tsx index ce521b26..1481b7d4 100644 --- a/packages/ui/src/note/mentions/note.tsx +++ b/packages/ui/src/note/mentions/note.tsx @@ -52,7 +52,7 @@ export function MentionNote({
-
+
{data.content}
{openable ? ( diff --git a/packages/utils/index.ts b/packages/utils/index.ts index fbf46d79..148d2745 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -5,7 +5,9 @@ export * from "./src/editor"; export * from "./src/nip01"; export * from "./src/nip94"; export * from "./src/notification"; -export * from "./src/hooks/useNetworkStatus"; -export * from "./src/hooks/useOpenGraph"; export * from "./src/cn"; export * from "./src/image"; + +// Hooks +export * from "./src/hooks/useNetworkStatus"; +export * from "./src/hooks/useOpenGraph"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ac11f7f..46e437e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: i18next-resources-to-backend: specifier: ^1.2.0 version: 1.2.0 + nanoid: + specifier: ^5.0.6 + version: 5.0.6 nostr-tools: specifier: ^2.3.1 version: 2.3.1(typescript@5.4.2) diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 4770662e..2c85ba42 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -13,7 +13,7 @@ pub fn create_column( ) -> Result { match app_handle.get_window("main") { Some(main_window) => match app_handle.get_webview(label) { - Some(_) => Err("Webview is exist".into()), + Some(_) => Ok(label.into()), None => { let path = PathBuf::from(url); let webview_url = WebviewUrl::App(path); @@ -27,7 +27,7 @@ pub fn create_column( LogicalSize::new(width, height), ) { Ok(webview) => Ok(webview.label().into()), - Err(_) => Err("Something is wrong".into()), + Err(_) => Err("Create webview failed".into()), } } }, @@ -36,7 +36,7 @@ pub fn create_column( } #[tauri::command] -pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result { +pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result { match app_handle.get_webview(label) { Some(webview) => { if let Ok(_) = webview.close() { @@ -45,7 +45,7 @@ pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result Err("Webview not found".into()), + None => Ok(true), } }