feat: improve column carousel

This commit is contained in:
2024-10-15 09:59:26 +07:00
parent 62bd689031
commit e158f2e4d7
11 changed files with 182 additions and 163 deletions

View File

@@ -454,25 +454,9 @@ async createColumn(column: Column) : Promise<Result<string, string>> {
else return { status: "error", error: e as any };
}
},
async closeColumn(label: string) : Promise<Result<boolean, string>> {
async updateColumn(label: string, width: number, height: number, x: number, y: number) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("close_column", { label }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async repositionColumn(label: string, x: number, y: number) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("reposition_column", { label, x, y }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async resizeColumn(label: string, width: number, height: number) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("resize_column", { label, width, height }) };
return { status: "ok", data: await TAURI_INVOKE("update_column", { label, width, height, x, y }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -486,6 +470,14 @@ async reloadColumn(label: string) : Promise<Result<boolean, string>> {
else return { status: "error", error: e as any };
}
},
async closeColumn(label: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("close_column", { label }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async openWindow(window: Window) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) };

View File

@@ -1,107 +1,89 @@
import { commands } from "@/commands.gen";
import { appColumns } from "@/commons";
import { useRect } from "@/system";
import type { LumeColumn } from "@/types";
import { CaretDown, Check } from "@phosphor-icons/react";
import { useParams } from "@tanstack/react-router";
import { useStore } from "@tanstack/react-store";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { Spinner } from "./spinner";
import { useCallback, useEffect, useMemo, useState } from "react";
type WindowEvent = {
scroll: boolean;
resize: boolean;
};
export const Column = memo(function Column({ column }: { column: LumeColumn }) {
export function Column({ column }: { column: LumeColumn }) {
const params = useParams({ strict: false });
const container = useRef<HTMLDivElement>(null);
const webviewLabel = `column-${params.account}_${column.label}`;
const webviewLabel = useMemo(
() => `column-${params.account}_${column.label}`,
[params.account, column.label],
);
const [isCreated, setIsCreated] = useState(false);
const repositionWebview = useCallback(async () => {
if (!container.current) return;
const newRect = container.current.getBoundingClientRect();
await invoke("reposition_column", {
label: webviewLabel,
x: newRect.x,
y: newRect.y,
});
}, []);
const resizeWebview = useCallback(async () => {
if (!container.current) return;
const newRect = container.current.getBoundingClientRect();
await invoke("resize_column", {
label: webviewLabel,
width: newRect.width,
height: newRect.height,
});
}, []);
const [rect, ref] = useRect();
const [error, setError] = useState<string>(null);
useEffect(() => {
if (!isCreated) return;
(async () => {
if (rect) {
const res = await commands.updateColumn(
webviewLabel,
rect.width,
rect.height,
rect.x,
rect.y,
);
const unlisten = listen<WindowEvent>("child_webview", (data) => {
if (data.payload.scroll) repositionWebview();
if (data.payload.resize) repositionWebview().then(() => resizeWebview());
});
return () => {
unlisten.then((f) => f());
};
}, [isCreated]);
if (res.status === "ok") {
console.log("webview is updated: ", webviewLabel);
} else {
console.log("webview error: ", res.error);
}
}
})();
}, [rect]);
useEffect(() => {
if (!container.current) return;
const isCreated = window.sessionStorage.getItem(webviewLabel);
const rect = container.current.getBoundingClientRect();
const url = `${column.url}?account=${params.account}&label=${column.label}&name=${column.name}`;
if (!isCreated) {
const initialRect = ref.current.getBoundingClientRect();
const prop = {
label: webviewLabel,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
url,
};
commands
.createColumn({
label: webviewLabel,
x: initialRect.x,
y: initialRect.y,
width: initialRect.width,
height: initialRect.height,
url: `${column.url}?account=${params.account}&label=${column.label}&name=${column.name}`,
})
.then((res) => {
if (res.status === "ok") {
console.log("webview is created: ", webviewLabel);
window.sessionStorage.setItem(webviewLabel, "");
} else {
setError(res.error);
}
});
// create new webview
invoke("create_column", { column: prop }).then(() => {
console.log("created: ", webviewLabel);
setIsCreated(true);
});
// close webview when unmounted
return () => {
invoke("close_column", { label: webviewLabel }).then(() => {
console.log("closed: ", webviewLabel);
});
};
return () => {
commands.closeColumn(webviewLabel).then((res) => {
if (res.status === "ok") {
console.log("webview is closed: ", webviewLabel);
} else {
console.log("webview error: ", res.error);
}
});
};
}
}, [params.account]);
return (
<div className="h-full w-[440px] shrink-0 border-r border-black/5 dark:border-white/5">
<div className="flex flex-col gap-px size-full">
<Header label={column.label} />
<div ref={container} className="flex-1 size-full">
{!isCreated ? (
<div className="size-full flex items-center justify-center">
<Spinner />
</div>
) : null}
</div>
<div ref={ref} className="flex-1 size-full" />
</div>
</div>
);
});
}
function Header({ label }: { label: string }) {
const [title, setTitle] = useState("");

View File

@@ -32,7 +32,6 @@ import { Route as AccountSettingsRelayImport } from './routes/$account/_settings
import { Route as AccountSettingsProfileImport } from './routes/$account/_settings/profile'
import { Route as AccountSettingsGeneralImport } from './routes/$account/_settings/general'
import { Route as AccountSettingsBitcoinConnectImport } from './routes/$account/_settings/bitcoin-connect'
import { Route as AccountAppHomeImport } from './routes/$account/_app/home'
import { Route as ColumnsLayoutInterestsIdImport } from './routes/columns/_layout/interests.$id'
import { Route as ColumnsLayoutGroupsIdImport } from './routes/columns/_layout/groups.$id'
import { Route as ColumnsLayoutCreateNewsfeedUsersImport } from './routes/columns/_layout/create-newsfeed.users'
@@ -63,6 +62,7 @@ const ColumnsLayoutNotificationLazyImport = createFileRoute(
const ColumnsLayoutLaunchpadLazyImport = createFileRoute(
'/columns/_layout/launchpad',
)()
const AccountAppHomeLazyImport = createFileRoute('/$account/_app/home')()
const ColumnsLayoutUsersIdLazyImport = createFileRoute(
'/columns/_layout/users/$id',
)()
@@ -208,6 +208,13 @@ const ColumnsLayoutLaunchpadLazyRoute = ColumnsLayoutLaunchpadLazyImport.update(
import('./routes/columns/_layout/launchpad.lazy').then((d) => d.Route),
)
const AccountAppHomeLazyRoute = AccountAppHomeLazyImport.update({
path: '/home',
getParentRoute: () => AccountAppRoute,
} as any).lazy(() =>
import('./routes/$account/_app.home.lazy').then((d) => d.Route),
)
const ColumnsLayoutStoriesRoute = ColumnsLayoutStoriesImport.update({
path: '/stories',
getParentRoute: () => ColumnsLayoutRoute,
@@ -271,13 +278,6 @@ const AccountSettingsBitcoinConnectRoute =
),
)
const AccountAppHomeRoute = AccountAppHomeImport.update({
path: '/home',
getParentRoute: () => AccountAppRoute,
} as any).lazy(() =>
import('./routes/$account/_app/home.lazy').then((d) => d.Route),
)
const ColumnsLayoutUsersIdLazyRoute = ColumnsLayoutUsersIdLazyImport.update({
path: '/users/$id',
getParentRoute: () => ColumnsLayoutRoute,
@@ -457,13 +457,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof EditorIndexImport
parentRoute: typeof rootRoute
}
'/$account/_app/home': {
id: '/$account/_app/home'
path: '/home'
fullPath: '/$account/home'
preLoaderRoute: typeof AccountAppHomeImport
parentRoute: typeof AccountAppImport
}
'/$account/_settings/bitcoin-connect': {
id: '/$account/_settings/bitcoin-connect'
path: '/bitcoin-connect'
@@ -527,6 +520,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ColumnsLayoutStoriesImport
parentRoute: typeof ColumnsLayoutImport
}
'/$account/_app/home': {
id: '/$account/_app/home'
path: '/home'
fullPath: '/$account/home'
preLoaderRoute: typeof AccountAppHomeLazyImport
parentRoute: typeof AccountAppImport
}
'/columns/_layout/launchpad': {
id: '/columns/_layout/launchpad'
path: '/launchpad'
@@ -617,11 +617,11 @@ declare module '@tanstack/react-router' {
// Create and export the route tree
interface AccountAppRouteChildren {
AccountAppHomeRoute: typeof AccountAppHomeRoute
AccountAppHomeLazyRoute: typeof AccountAppHomeLazyRoute
}
const AccountAppRouteChildren: AccountAppRouteChildren = {
AccountAppHomeRoute: AccountAppHomeRoute,
AccountAppHomeLazyRoute: AccountAppHomeLazyRoute,
}
const AccountAppRouteWithChildren = AccountAppRoute._addFileChildren(
@@ -745,7 +745,6 @@ export interface FileRoutesByFullPath {
'/auth/import': typeof AuthImportLazyRoute
'/auth/new': typeof AuthNewLazyRoute
'/editor': typeof EditorIndexRoute
'/$account/home': typeof AccountAppHomeRoute
'/$account/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute
'/$account/general': typeof AccountSettingsGeneralRoute
'/$account/profile': typeof AccountSettingsProfileRoute
@@ -755,6 +754,7 @@ export interface FileRoutesByFullPath {
'/columns/global': typeof ColumnsLayoutGlobalRoute
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute
'/columns/stories': typeof ColumnsLayoutStoriesRoute
'/$account/home': typeof AccountAppHomeLazyRoute
'/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
@@ -785,7 +785,6 @@ export interface FileRoutesByTo {
'/auth/import': typeof AuthImportLazyRoute
'/auth/new': typeof AuthNewLazyRoute
'/editor': typeof EditorIndexRoute
'/$account/home': typeof AccountAppHomeRoute
'/$account/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute
'/$account/general': typeof AccountSettingsGeneralRoute
'/$account/profile': typeof AccountSettingsProfileRoute
@@ -795,6 +794,7 @@ export interface FileRoutesByTo {
'/columns/global': typeof ColumnsLayoutGlobalRoute
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute
'/columns/stories': typeof ColumnsLayoutStoriesRoute
'/$account/home': typeof AccountAppHomeLazyRoute
'/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
@@ -829,7 +829,6 @@ export interface FileRoutesById {
'/auth/import': typeof AuthImportLazyRoute
'/auth/new': typeof AuthNewLazyRoute
'/editor/': typeof EditorIndexRoute
'/$account/_app/home': typeof AccountAppHomeRoute
'/$account/_settings/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute
'/$account/_settings/general': typeof AccountSettingsGeneralRoute
'/$account/_settings/profile': typeof AccountSettingsProfileRoute
@@ -839,6 +838,7 @@ export interface FileRoutesById {
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
'/columns/_layout/newsfeed': typeof ColumnsLayoutNewsfeedRoute
'/columns/_layout/stories': typeof ColumnsLayoutStoriesRoute
'/$account/_app/home': typeof AccountAppHomeLazyRoute
'/columns/_layout/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
'/columns/_layout/notification': typeof ColumnsLayoutNotificationLazyRoute
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
@@ -871,7 +871,6 @@ export interface FileRouteTypes {
| '/auth/import'
| '/auth/new'
| '/editor'
| '/$account/home'
| '/$account/bitcoin-connect'
| '/$account/general'
| '/$account/profile'
@@ -881,6 +880,7 @@ export interface FileRouteTypes {
| '/columns/global'
| '/columns/newsfeed'
| '/columns/stories'
| '/$account/home'
| '/columns/launchpad'
| '/columns/notification'
| '/columns/onboarding'
@@ -910,7 +910,6 @@ export interface FileRouteTypes {
| '/auth/import'
| '/auth/new'
| '/editor'
| '/$account/home'
| '/$account/bitcoin-connect'
| '/$account/general'
| '/$account/profile'
@@ -920,6 +919,7 @@ export interface FileRouteTypes {
| '/columns/global'
| '/columns/newsfeed'
| '/columns/stories'
| '/$account/home'
| '/columns/launchpad'
| '/columns/notification'
| '/columns/onboarding'
@@ -952,7 +952,6 @@ export interface FileRouteTypes {
| '/auth/import'
| '/auth/new'
| '/editor/'
| '/$account/_app/home'
| '/$account/_settings/bitcoin-connect'
| '/$account/_settings/general'
| '/$account/_settings/profile'
@@ -962,6 +961,7 @@ export interface FileRouteTypes {
| '/columns/_layout/global'
| '/columns/_layout/newsfeed'
| '/columns/_layout/stories'
| '/$account/_app/home'
| '/columns/_layout/launchpad'
| '/columns/_layout/notification'
| '/columns/_layout/onboarding'
@@ -1131,10 +1131,6 @@ export const routeTree = rootRoute
"/editor/": {
"filePath": "editor/index.tsx"
},
"/$account/_app/home": {
"filePath": "$account/_app/home.tsx",
"parent": "/$account/_app"
},
"/$account/_settings/bitcoin-connect": {
"filePath": "$account/_settings/bitcoin-connect.tsx",
"parent": "/$account/_settings"
@@ -1175,6 +1171,10 @@ export const routeTree = rootRoute
"filePath": "columns/_layout/stories.tsx",
"parent": "/columns/_layout"
},
"/$account/_app/home": {
"filePath": "$account/_app.home.lazy.tsx",
"parent": "/$account/_app"
},
"/columns/_layout/launchpad": {
"filePath": "columns/_layout/launchpad.lazy.tsx",
"parent": "/columns/_layout"

View File

@@ -45,16 +45,16 @@ function Screen() {
}, [emblaApi]);
const emitScrollEvent = useCallback(() => {
getCurrentWindow().emit("child_webview", { scroll: true });
}, []);
const emitResizeEvent = useCallback(() => {
getCurrentWindow().emit("child_webview", { resize: true, direction: "x" });
getCurrentWindow().emit("column_scroll", {});
}, []);
const add = useDebouncedCallback((column: LumeColumn) => {
column.label = `${column.label}-${nanoid()}`; // update col label
appColumns.setState((prev) => [column, ...prev]);
if (emblaApi) {
emblaApi.scrollTo(0, true);
}
}, 150);
const remove = useDebouncedCallback((label: string) => {
@@ -112,16 +112,14 @@ function Screen() {
useEffect(() => {
if (emblaApi) {
emblaApi.on("scroll", emitScrollEvent);
emblaApi.on("resize", emitResizeEvent);
emblaApi.on("slidesChanged", emitScrollEvent);
}
return () => {
emblaApi?.off("scroll", emitScrollEvent);
emblaApi?.off("resize", emitResizeEvent);
emblaApi?.off("slidesChanged", emitScrollEvent);
};
}, [emblaApi, emitScrollEvent, emitResizeEvent]);
}, [emblaApi, emitScrollEvent]);
// Listen for keyboard event
useEffect(() => {

View File

@@ -1,3 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/$account/_app/home")();

View File

@@ -1,4 +1,5 @@
export * from "./event";
export * from "./window";
export * from "./hooks/useEvent";
export * from "./hooks/useProfile";
export * from "./useEvent";
export * from "./useProfile";
export * from "./useRect";

View File

@@ -2,7 +2,7 @@ import { type Result, type RichEvent, commands } from "@/commands.gen";
import type { NostrEvent } from "@/types";
import { useQuery } from "@tanstack/react-query";
import { nip19 } from "nostr-tools";
import { LumeEvent } from "../event";
import { LumeEvent } from "./event";
export function useEvent(id: string, repost?: string) {
const { isLoading, isError, error, data } = useQuery({

53
src/system/useRect.ts Normal file
View File

@@ -0,0 +1,53 @@
import { listen } from "@tauri-apps/api/event";
import { useEffect, useRef, useState } from "react";
type MutableRefObject<T> = {
current: T;
};
const useEffectInEvent = <K extends keyof WindowEventMap>(
event: K,
set: () => void,
useCapture?: boolean,
) => {
useEffect(() => {
if (set) {
set();
window.addEventListener(event, set, useCapture);
return () => window.removeEventListener(event, set, useCapture);
}
}, []);
};
const useTauriInEvent = (set: () => void) => {
useEffect(() => {
if (set) {
const unlisten = listen("column_scroll", () => {
set();
});
return () => {
unlisten.then((f) => f());
};
}
}, []);
};
export const useRect = <T extends HTMLDivElement | null>(): [
DOMRect | undefined,
MutableRefObject<T | null>,
] => {
const ref = useRef<T>(null);
const [rect, setRect] = useState<DOMRect>();
const set = (): void => {
setRect(ref.current?.getBoundingClientRect());
};
useTauriInEvent(set);
useEffectInEvent("resize", set);
useEffectInEvent("scroll", set, true);
return [rect, ref];
};