From 05b52564e074c5bee1c06a4cf99bafeeef89b949 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 18 Mar 2024 10:50:08 +0700 Subject: [PATCH] feat: column manager --- apps/desktop2/package.json | 1 + .../desktop2/src/components/col.tsx | 34 ++++----- apps/desktop2/src/components/toolbar.tsx | 39 ++++++++++ apps/desktop2/src/routes/$account.home.tsx | 73 ++++++++++++++++--- apps/desktop2/src/routes/$account.tsx | 12 ++- apps/desktop2/src/routes/default.lazy.tsx | 16 ++++ .../newsfeed.tsx => routes/newsfeed.lazy.tsx} | 53 ++++---------- packages/ark/src/index.ts | 1 + packages/icons/src/arrowLeft.tsx | 23 +++--- packages/ui/src/column/content.tsx | 16 ++++ packages/ui/src/column/header.tsx | 22 ++++++ packages/ui/src/column/index.ts | 9 +++ packages/ui/src/column/root.tsx | 23 ++++++ packages/ui/src/index.ts | 2 +- pnpm-lock.yaml | 28 +++++++ src-tauri/capabilities/main.json | 2 + src-tauri/gen/schemas/capabilities.json | 2 +- 17 files changed, 269 insertions(+), 87 deletions(-) rename packages/ui/src/column.tsx => apps/desktop2/src/components/col.tsx (63%) create mode 100644 apps/desktop2/src/components/toolbar.tsx create mode 100644 apps/desktop2/src/routes/default.lazy.tsx rename apps/desktop2/src/{components/newsfeed.tsx => routes/newsfeed.lazy.tsx} (65%) create mode 100644 packages/ui/src/column/content.tsx create mode 100644 packages/ui/src/column/header.tsx create mode 100644 packages/ui/src/column/index.ts create mode 100644 packages/ui/src/column/root.tsx diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index f00083fd..6d5b86c6 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -32,6 +32,7 @@ "slate": "^0.102.0", "slate-react": "^0.102.0", "sonner": "^1.4.3", + "use-debounce": "^10.0.0", "virtua": "^0.29.0" }, "devDependencies": { diff --git a/packages/ui/src/column.tsx b/apps/desktop2/src/components/col.tsx similarity index 63% rename from packages/ui/src/column.tsx rename to apps/desktop2/src/components/col.tsx index 13dc993d..cf3bc9e4 100644 --- a/packages/ui/src/column.tsx +++ b/apps/desktop2/src/components/col.tsx @@ -9,20 +9,23 @@ import { LumeColumn } from "@lume/types"; import { useDebouncedCallback } from "use-debounce"; import { type UnlistenFn } from "@tauri-apps/api/event"; -export function Column({ +export function Col({ column, + account, isScroll, }: { column: LumeColumn; + account: string; isScroll: boolean; }) { const mainWindow = useMemo(() => getCurrent(), []); const childWindow = useRef(null); - const divRef = useRef(null); + const container = useRef(null); const initialRect = useRef(null); const unlisten = useRef(null); const handleResize = useDebouncedCallback(() => { - const newRect = divRef.current.getBoundingClientRect(); + if (!childWindow.current) return; + const newRect = container.current.getBoundingClientRect(); if (initialRect.current.height !== newRect.height) { childWindow.current.setSize( new LogicalSize(newRect.width, newRect.height), @@ -37,8 +40,9 @@ export function Column({ }, []); useEffect(() => { + if (!childWindow.current) return; if (isScroll) { - const newRect = divRef.current.getBoundingClientRect(); + const newRect = container.current.getBoundingClientRect(); childWindow.current.setPosition( new LogicalPosition(newRect.x, newRect.y), ); @@ -47,16 +51,17 @@ export function Column({ useEffect(() => { if (!mainWindow) return; - if (!divRef.current) return; + if (!container.current) return; if (childWindow.current) return; - const rect = divRef.current.getBoundingClientRect(); - const name = column.name.toLowerCase().replace(/\W/g, ""); + const rect = container.current.getBoundingClientRect(); + const name = `column-${column.name.toLowerCase().replace(/\W/g, "")}`; + const url = column.name + `?account=${account}&name=${column.name}`; // create new webview initialRect.current = rect; childWindow.current = new Webview(mainWindow, name, { - url: column.content, + url, x: rect.x, y: rect.y, width: rect.width, @@ -70,18 +75,9 @@ export function Column({ return () => { if (unlisten.current) unlisten.current(); + if (childWindow.current) childWindow.current.close(); }; }, []); - return ( -
-
-
-
{column.name}
-
-
-
-
-
- ); + return
; } diff --git a/apps/desktop2/src/components/toolbar.tsx b/apps/desktop2/src/components/toolbar.tsx new file mode 100644 index 00000000..e0f8aba2 --- /dev/null +++ b/apps/desktop2/src/components/toolbar.tsx @@ -0,0 +1,39 @@ +import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; + +export function Toolbar({ + moveLeft, + moveRight, +}: { + moveLeft: () => void; + moveRight: () => void; +}) { + const [domReady, setDomReady] = useState(false); + + useEffect(() => { + setDomReady(true); + }, []); + + return domReady + ? createPortal( +
+ + +
, + document.getElementById("toolbar"), + ) + : null; +} diff --git a/apps/desktop2/src/routes/$account.home.tsx b/apps/desktop2/src/routes/$account.home.tsx index fde8e612..a41207c8 100644 --- a/apps/desktop2/src/routes/$account.home.tsx +++ b/apps/desktop2/src/routes/$account.home.tsx @@ -1,17 +1,17 @@ +import { Col } from "@/components/col"; +import { Toolbar } from "@/components/toolbar"; import { LoaderIcon } from "@lume/icons"; -import { Column } from "@lume/ui"; import { createFileRoute } from "@tanstack/react-router"; -import { useState } from "react"; +import { useRef, useState } from "react"; +import { VList, VListHandle } from "virtua"; export const Route = createFileRoute("/$account/home")({ component: Screen, pendingComponent: Pending, loader: async () => { const columns = [ - { name: "Tauri v2", content: "https://beta.tauri.app" }, - { name: "Tauri v1", content: "https://tauri.app" }, - { name: "Lume", content: "https://lume.nu" }, - { name: "Snort", content: "https://snort.social" }, + { name: "Newsfeed", content: "/columns/newsfeed" }, + { name: "Default", content: "/columns/default" }, ]; return columns; }, @@ -19,22 +19,71 @@ export const Route = createFileRoute("/$account/home")({ function Screen() { const data = Route.useLoaderData(); + const search = Route.useSearch(); + const vlistRef = useRef(null); + + const [selectedIndex, setSelectedIndex] = useState(-1); const [isScroll, setIsScroll] = useState(false); + const moveLeft = () => { + const prevIndex = Math.max(selectedIndex - 1, 0); + setSelectedIndex(prevIndex); + vlistRef.current.scrollToIndex(prevIndex, { + align: "start", + }); + }; + + const moveRight = () => { + const nextIndex = Math.min(selectedIndex + 1, data.length - 1); + setSelectedIndex(nextIndex); + vlistRef.current.scrollToIndex(nextIndex, { + align: "end", + }); + }; + return ( -
-
setIsScroll((state) => !state)} - className="flex h-full w-full flex-nowrap gap-3 overflow-x-auto px-3 pb-3 pt-1.5 focus:outline-none" +
+ { + if (!vlistRef.current) return; + switch (e.code) { + case "ArrowUp": + case "ArrowLeft": { + e.preventDefault(); + moveLeft(); + break; + } + case "ArrowDown": + case "ArrowRight": { + e.preventDefault(); + moveRight(); + break; + } + } + }} + onScroll={() => { + setIsScroll(true); + }} + onScrollEnd={() => { + setIsScroll(false); + }} + className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none" > {data.map((column, index) => ( - ))} -
+ +
); } diff --git a/apps/desktop2/src/routes/$account.tsx b/apps/desktop2/src/routes/$account.tsx index e221e91a..60a9a78e 100644 --- a/apps/desktop2/src/routes/$account.tsx +++ b/apps/desktop2/src/routes/$account.tsx @@ -1,4 +1,9 @@ -import { ComposeFilledIcon, PlusIcon } from "@lume/icons"; +import { + ArrowLeftIcon, + ArrowRightIcon, + ComposeFilledIcon, + PlusIcon, +} from "@lume/icons"; import { Outlet, createFileRoute } from "@tanstack/react-router"; import { cn } from "@lume/utils"; import { Accounts } from "@/components/accounts"; @@ -10,7 +15,7 @@ export const Route = createFileRoute("/$account")({ function App() { const ark = useArk(); - const context = Route.useRouteContext(); + const { platform } = Route.useRouteContext(); return (
@@ -18,7 +23,7 @@ function App() { data-tauri-drag-region className={cn( "flex h-11 shrink-0 items-center justify-between pr-2", - context.platform === "macos" ? "ml-2 pl-20" : "pl-4", + platform === "macos" ? "ml-2 pl-20" : "pl-4", )} >
@@ -40,6 +45,7 @@ function App() { New post +
diff --git a/apps/desktop2/src/routes/default.lazy.tsx b/apps/desktop2/src/routes/default.lazy.tsx new file mode 100644 index 00000000..24a785a9 --- /dev/null +++ b/apps/desktop2/src/routes/default.lazy.tsx @@ -0,0 +1,16 @@ +import { Column } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; + +export const Route = createLazyFileRoute("/default")({ + component: Screen, +}); + +function Screen() { + return ( + + +

TODO

+
+
+ ); +} diff --git a/apps/desktop2/src/components/newsfeed.tsx b/apps/desktop2/src/routes/newsfeed.lazy.tsx similarity index 65% rename from apps/desktop2/src/components/newsfeed.tsx rename to apps/desktop2/src/routes/newsfeed.lazy.tsx index d4d7768e..f9fcdb54 100644 --- a/apps/desktop2/src/components/newsfeed.tsx +++ b/apps/desktop2/src/routes/newsfeed.lazy.tsx @@ -1,19 +1,22 @@ import { RepostNote } from "@/components/repost"; import { Suggest } from "@/components/suggest"; import { TextNote } from "@/components/text"; -import { useArk } from "@lume/ark"; +import { useEvents } from "@lume/ark"; import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; import { Event, Kind } from "@lume/types"; import { Column } from "@lume/ui"; -import { FETCH_LIMIT } from "@lume/utils"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { Link, useParams } from "@tanstack/react-router"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; import { Virtualizer } from "virtua"; -export function Newsfeed() { - const ark = useArk(); +export const Route = createLazyFileRoute("/newsfeed")({ + component: Screen, +}); + +export function Screen() { // @ts-ignore, just work!!! - const { account } = useParams({ strict: false }); + const { name, account } = Route.useSearch(); + const { t } = useTranslation(); const { data, hasNextPage, @@ -21,26 +24,7 @@ export function Newsfeed() { isRefetching, isFetchingNextPage, fetchNextPage, - } = useInfiniteQuery({ - queryKey: ["local_newsfeed", account], - initialPageParam: 0, - queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_events( - "local", - FETCH_LIMIT, - pageParam, - true, - ); - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage?.at(-1); - if (!lastEvent) return; - return lastEvent.created_at - 1; - }, - select: (data) => data?.pages.flatMap((page) => page), - refetchOnWindowFocus: false, - }); + } = useEvents("local", account); const renderItem = (event: Event) => { if (!event) return; @@ -54,7 +38,7 @@ export function Newsfeed() { return ( - + {isLoading || isRefetching ? (
@@ -64,15 +48,10 @@ export function Newsfeed() {
-

- Empty newsfeed. Or you view the{" "} - - Global Newsfeed - -

+
+

{t("emptyFeedTitle")}

+

{t("emptyFeedSubtitle")}

+
diff --git a/packages/ark/src/index.ts b/packages/ark/src/index.ts index ca80124b..3dc9604d 100644 --- a/packages/ark/src/index.ts +++ b/packages/ark/src/index.ts @@ -2,4 +2,5 @@ export * from "./ark"; export * from "./context"; export * from "./hooks/useArk"; export * from "./hooks/useEvent"; +export * from "./hooks/useEvents"; export * from "./hooks/useProfile"; diff --git a/packages/icons/src/arrowLeft.tsx b/packages/icons/src/arrowLeft.tsx index ceb952d9..d81b773d 100644 --- a/packages/icons/src/arrowLeft.tsx +++ b/packages/icons/src/arrowLeft.tsx @@ -1,18 +1,13 @@ -export function ArrowLeftIcon(props: JSX.IntrinsicElements['svg']) { +export function ArrowLeftIcon(props: JSX.IntrinsicElements["svg"]) { return ( - - + + ); } diff --git a/packages/ui/src/column/content.tsx b/packages/ui/src/column/content.tsx new file mode 100644 index 00000000..3c079945 --- /dev/null +++ b/packages/ui/src/column/content.tsx @@ -0,0 +1,16 @@ +import { cn } from "@lume/utils"; +import { ReactNode } from "react"; + +export function ColumnContent({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/packages/ui/src/column/header.tsx b/packages/ui/src/column/header.tsx new file mode 100644 index 00000000..30d6e8eb --- /dev/null +++ b/packages/ui/src/column/header.tsx @@ -0,0 +1,22 @@ +import { cn } from "@lume/utils"; + +export function ColumnHeader({ + name, + className, +}: { + name: string; + className?: string; +}) { + return ( +
+
+
{name}
+
+
+ ); +} diff --git a/packages/ui/src/column/index.ts b/packages/ui/src/column/index.ts new file mode 100644 index 00000000..abd5b32a --- /dev/null +++ b/packages/ui/src/column/index.ts @@ -0,0 +1,9 @@ +import { ColumnContent } from "./content"; +import { ColumnHeader } from "./header"; +import { ColumnRoot } from "./root"; + +export const Column = { + Root: ColumnRoot, + Header: ColumnHeader, + Content: ColumnContent, +}; diff --git a/packages/ui/src/column/root.tsx b/packages/ui/src/column/root.tsx new file mode 100644 index 00000000..e9a01b9c --- /dev/null +++ b/packages/ui/src/column/root.tsx @@ -0,0 +1,23 @@ +import { cn } from "@lume/utils"; +import { ReactNode } from "react"; + +export function ColumnRoot({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 236dcf3d..566e7dcb 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,7 +1,7 @@ export * from "./user"; export * from "./note"; +export * from "./column"; // UI -export * from "./column"; export * from "./container"; export * from "./box"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72bc634a..8ac11f7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: sonner: specifier: ^1.4.3 version: 1.4.3(react-dom@18.2.0)(react@18.2.0) + use-debounce: + specifier: ^10.0.0 + version: 10.0.0(react@18.2.0) virtua: specifier: ^0.29.0 version: 0.29.0(react-dom@18.2.0)(react@18.2.0) @@ -310,6 +313,31 @@ importers: specifier: ^5.4.2 version: 5.4.2 + packages/column-newsfeed: + dependencies: + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@types/react': + specifier: ^18.2.64 + version: 18.2.66 + '@types/react-dom': + specifier: ^18.2.21 + version: 18.2.22 + '@vitejs/plugin-react-swc': + specifier: ^3.5.0 + version: 3.6.0(vite@5.1.6) + typescript: + specifier: ^5.2.2 + version: 5.4.2 + vite: + specifier: ^5.1.6 + version: 5.1.6 + packages/icons: dependencies: react: diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index e21d1551..9a3e5be1 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -31,6 +31,7 @@ "updater:default", "window:allow-start-dragging", "window:allow-create", + "window:allow-close", "store:allow-get", "clipboard-manager:allow-write", "clipboard-manager:allow-read", @@ -38,6 +39,7 @@ "webview:allow-create-webview", "webview:allow-set-webview-size", "webview:allow-set-webview-position", + "webview:allow-webview-close", "dialog:allow-open", "fs:allow-read-file", "shell:allow-open", diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 8cb6bccf..dc0d3678 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","nwc","zap-*","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","window:allow-create","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file +{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","nwc","zap-*","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","window:allow-create","window:allow-close","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file