feat: improve
This commit is contained in:
@@ -19,15 +19,15 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@tanstack/query-sync-storage-persister": "^5.28.13",
|
||||
"@tanstack/react-query": "^5.28.14",
|
||||
"@tanstack/react-query-persist-client": "^5.28.14",
|
||||
"@tanstack/react-router": "^1.26.7",
|
||||
"i18next": "^23.10.1",
|
||||
"@tanstack/query-sync-storage-persister": "^5.29.0",
|
||||
"@tanstack/react-query": "^5.29.0",
|
||||
"@tanstack/react-query-persist-client": "^5.29.0",
|
||||
"@tanstack/react-router": "^1.26.18",
|
||||
"i18next": "^23.11.1",
|
||||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"minidenticons": "^4.2.1",
|
||||
"nanoid": "^5.0.6",
|
||||
"nostr-tools": "^2.3.2",
|
||||
"nanoid": "^5.0.7",
|
||||
"nostr-tools": "^2.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-currency-input-field": "^3.8.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -38,21 +38,21 @@
|
||||
"slate-react": "^0.102.0",
|
||||
"sonner": "^1.4.41",
|
||||
"use-debounce": "^10.0.0",
|
||||
"virtua": "^0.29.1"
|
||||
"virtua": "^0.29.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tailwindcss": "workspace:^",
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@tanstack/router-devtools": "^1.26.7",
|
||||
"@tanstack/router-vite-plugin": "^1.26.8",
|
||||
"@types/react": "^18.2.74",
|
||||
"@tanstack/router-devtools": "^1.26.18",
|
||||
"@tanstack/router-vite-plugin": "^1.26.16",
|
||||
"@types/react": "^18.2.75",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
"typescript": "^5.4.4",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-top-level-await": "^1.4.1",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
|
||||
@@ -6,7 +6,6 @@ import { I18nextProvider } from "react-i18next";
|
||||
import "./app.css";
|
||||
import i18n from "./locale";
|
||||
import { Toaster } from "sonner";
|
||||
import { locale, platform } from "@tauri-apps/plugin-os";
|
||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||
import { routeTree } from "./router.gen"; // auto generated file
|
||||
@@ -27,18 +26,11 @@ const persister = createSyncStoragePersister({
|
||||
});
|
||||
|
||||
const ark = new Ark();
|
||||
const platformName = await platform();
|
||||
const osLocale = await locale();
|
||||
|
||||
// Set up a Router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
platform: platformName,
|
||||
locale: osLocale,
|
||||
settings: null,
|
||||
accounts: null,
|
||||
interests: null,
|
||||
ark,
|
||||
queryClient,
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ export function AvatarUploader({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className={cn("", className)}
|
||||
className={cn("size-4", className)}
|
||||
>
|
||||
{loading ? <LoaderIcon className="size-4 animate-spin" /> : children}
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { LumeColumn } from "@lume/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
|
||||
export function Col({
|
||||
column,
|
||||
@@ -13,43 +13,18 @@ export function Col({
|
||||
account: string;
|
||||
isScroll: boolean;
|
||||
}) {
|
||||
const window = useMemo(() => getCurrent(), []);
|
||||
const webview = useRef<string>(null);
|
||||
const webview = useRef<string | undefined>(undefined);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
|
||||
const createWebview = async () => {
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
const label = `column-${column.label}`;
|
||||
const url =
|
||||
column.content +
|
||||
`?account=${account}&label=${column.label}&name=${column.name}`;
|
||||
|
||||
// create new webview
|
||||
webview.current = await invoke("create_column", {
|
||||
label,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
const closeWebview = async () => {
|
||||
const close = await invoke("close_column", {
|
||||
label: webview.current,
|
||||
});
|
||||
if (close) webview.current = null;
|
||||
};
|
||||
|
||||
const repositionWebview = async () => {
|
||||
if (!webview.current) return;
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("reposition_column", {
|
||||
label: webview.current,
|
||||
x: newRect.x,
|
||||
y: newRect.y,
|
||||
});
|
||||
if (webview.current && webview.current.length > 1) {
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("reposition_column", {
|
||||
label: webview.current,
|
||||
x: newRect.x,
|
||||
y: newRect.y,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -59,27 +34,50 @@ export function Col({
|
||||
}, [isScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window) return;
|
||||
if (!container.current) return;
|
||||
if (webview.current) return;
|
||||
(async () => {
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
const windowLabel = `column-${column.label}`;
|
||||
const url =
|
||||
column.content +
|
||||
`?account=${account}&label=${column.label}&name=${column.name}`;
|
||||
|
||||
// create webview for current column
|
||||
createWebview();
|
||||
// create new webview
|
||||
webview.current = await invoke("create_column", {
|
||||
label: windowLabel,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
url,
|
||||
});
|
||||
})();
|
||||
|
||||
// close webview when unmounted
|
||||
return () => {
|
||||
if (webview.current) closeWebview();
|
||||
if (webview.current && webview.current.length > 1) {
|
||||
invoke("close_column", {
|
||||
label: webview.current,
|
||||
}).then(() => {
|
||||
webview.current = undefined;
|
||||
});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={container}
|
||||
className="h-full w-[440px] shrink-0 p-2 flex items-center justify-center"
|
||||
>
|
||||
<button type="button" disabled>
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</button>
|
||||
<div ref={container} className="h-full w-[440px] shrink-0 p-2">
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-full flex items-center justify-center",
|
||||
!webview?.current?.length
|
||||
? "rounded-xl flex-col bg-black/5 dark:bg-white/5 backdrop-blur-lg"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<button type="button" className="size-5" disabled>
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Toolbar } from "@/components/toolbar";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { EventColumns, LumeColumn } from "@lume/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { VList, VListHandle } from "virtua";
|
||||
|
||||
export const Route = createFileRoute("/$account/home")({
|
||||
@@ -53,40 +54,45 @@ function Screen() {
|
||||
});
|
||||
};
|
||||
|
||||
const add = (column: LumeColumn) => {
|
||||
setColumns((state) => [...state, column]);
|
||||
};
|
||||
const add = useDebouncedCallback((column: LumeColumn) => {
|
||||
column["label"] = column.label + "-" + nanoid();
|
||||
|
||||
const remove = (label: string) => {
|
||||
setColumns((state) => [...state, column]);
|
||||
setSelectedIndex(columns.length + 1);
|
||||
|
||||
// scroll to the last column
|
||||
vlistRef.current.scrollToIndex(columns.length + 1, {
|
||||
align: "end",
|
||||
});
|
||||
}, 150);
|
||||
|
||||
const remove = useDebouncedCallback((label: string) => {
|
||||
setColumns((state) => state.filter((t) => t.label !== label));
|
||||
};
|
||||
setSelectedIndex(columns.length - 1);
|
||||
|
||||
// scroll to the first column
|
||||
vlistRef.current.scrollToIndex(0, {
|
||||
align: "start",
|
||||
});
|
||||
}, 150);
|
||||
|
||||
useEffect(() => {
|
||||
ark.set_columns(columns);
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: UnlistenFn = undefined;
|
||||
let unlisten: Awaited<ReturnType<typeof listen>> | undefined = undefined;
|
||||
|
||||
const listenColumnEvent = async () => {
|
||||
const mainWindow = getCurrent();
|
||||
if (!unlisten) {
|
||||
unlisten = await mainWindow.listen<EventColumns>("columns", (data) => {
|
||||
if (data.payload.type === "add") add(data.payload.column);
|
||||
if (data.payload.type === "remove") remove(data.payload.label);
|
||||
});
|
||||
}
|
||||
};
|
||||
(async () => {
|
||||
if (unlisten) return;
|
||||
unlisten = await listen<EventColumns>("columns", (data) => {
|
||||
if (data.payload.type === "add") add(data.payload.column);
|
||||
if (data.payload.type === "remove") remove(data.payload.label);
|
||||
});
|
||||
})();
|
||||
|
||||
// listen for column changes
|
||||
listenColumnEvent();
|
||||
|
||||
// clean up
|
||||
return () => {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = undefined;
|
||||
}
|
||||
if (unlisten) unlisten();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -122,9 +128,9 @@ function Screen() {
|
||||
}}
|
||||
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
||||
>
|
||||
{columns.map((column) => (
|
||||
{columns.map((column, index) => (
|
||||
<Col
|
||||
key={column.label}
|
||||
key={column.label + index}
|
||||
column={column}
|
||||
account={account}
|
||||
isScroll={isScroll}
|
||||
@@ -139,7 +145,7 @@ function Screen() {
|
||||
function Pending() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<button type="button" disabled>
|
||||
<button type="button" className="size-5" disabled>
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,14 @@ import { ComposeFilledIcon, PlusIcon } from "@lume/icons";
|
||||
import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { cn } from "@lume/utils";
|
||||
import { Accounts } from "@/components/accounts";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
|
||||
export const Route = createFileRoute("/$account")({
|
||||
component: App,
|
||||
beforeLoad: async () => {
|
||||
const platformName = await platform();
|
||||
return { platform: platformName };
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import {
|
||||
Outlet,
|
||||
ScrollRestoration,
|
||||
createRootRouteWithContext,
|
||||
} from "@tanstack/react-router";
|
||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||
import { type Ark } from "@lume/ark";
|
||||
import { type QueryClient } from "@tanstack/react-query";
|
||||
import { type Platform } from "@tauri-apps/plugin-os";
|
||||
@@ -12,20 +8,15 @@ import { Account, Interests, Settings } from "@lume/types";
|
||||
interface RouterContext {
|
||||
ark: Ark;
|
||||
queryClient: QueryClient;
|
||||
platform: Platform;
|
||||
locale: string;
|
||||
settings: Settings;
|
||||
interests: Interests;
|
||||
accounts: Account[];
|
||||
platform?: Platform;
|
||||
locale?: string;
|
||||
settings?: Settings;
|
||||
interests?: Interests;
|
||||
accounts?: Account[];
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: () => (
|
||||
<>
|
||||
<ScrollRestoration />
|
||||
<Outlet />
|
||||
</>
|
||||
),
|
||||
component: () => <Outlet />,
|
||||
pendingComponent: Pending,
|
||||
wrapInSuspense: true,
|
||||
});
|
||||
@@ -33,7 +24,9 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
function Pending() {
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
<button type="button" className="size-5" disabled>
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { CheckIcon } from "@lume/icons";
|
||||
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { LaurelIcon } from "@lume/icons";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Switch from "@radix-ui/react-switch";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Settings } from "@lume/types";
|
||||
import { AppRouteSearch, Settings } from "@lume/types";
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createLazyFileRoute("/auth/settings")({
|
||||
export const Route = createFileRoute("/auth/settings")({
|
||||
validateSearch: (search: Record<string, string>): AppRouteSearch => {
|
||||
return {
|
||||
account: search.account,
|
||||
};
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
@@ -25,6 +30,7 @@ function Screen() {
|
||||
notification: false,
|
||||
enhancedPrivacy: false,
|
||||
autoUpdate: false,
|
||||
zap: false,
|
||||
});
|
||||
|
||||
const toggleNofitication = async () => {
|
||||
@@ -49,6 +55,13 @@ function Screen() {
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleZap = () => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
zap: !settings.zap,
|
||||
}));
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
const eventId = await ark.set_settings(settings);
|
||||
@@ -64,7 +77,6 @@ function Screen() {
|
||||
async function loadSettings() {
|
||||
const permissionGranted = await isPermissionGranted(); // get notification permission
|
||||
const settings = await ark.get_settings();
|
||||
|
||||
setSettings({ ...settings, notification: permissionGranted });
|
||||
}
|
||||
|
||||
@@ -75,7 +87,7 @@ function Screen() {
|
||||
<div className="mx-auto flex h-full w-full flex-col items-center justify-center gap-6 px-5 xl:max-w-xl">
|
||||
<div className="flex flex-col items-center gap-5 text-center">
|
||||
<div className="flex size-20 items-center justify-center rounded-full bg-teal-100 text-teal-500">
|
||||
<CheckIcon className="size-6" />
|
||||
<LaurelIcon className="size-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">
|
||||
@@ -135,6 +147,22 @@ function Screen() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
|
||||
<Switch.Root
|
||||
checked={settings.zap}
|
||||
onClick={() => toggleZap()}
|
||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
||||
>
|
||||
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||
</Switch.Root>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold">Zap</h3>
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
Show the Zap button in each note and user's profile screen, use
|
||||
for send Bitcoin tip to other users.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-50 px-5 py-4 dark:bg-neutral-950">
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
There are many more settings you can configure from the 'Settings'
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CheckCircleIcon } from "@lume/icons";
|
||||
import { ColumnRouteSearch } from "@lume/types";
|
||||
import { Column, User } from "@lume/ui";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -23,9 +23,10 @@ export const Route = createFileRoute("/create-group")({
|
||||
|
||||
function Screen() {
|
||||
const contacts = Route.useLoaderData();
|
||||
const router = useRouter();
|
||||
|
||||
const { ark } = Route.useRouteContext();
|
||||
const { label, name } = Route.useSearch();
|
||||
const { label, name, redirect } = Route.useSearch();
|
||||
|
||||
const [title, setTitle] = useState<string>("Just a new group");
|
||||
const [users, setUsers] = useState<Array<string>>([]);
|
||||
@@ -40,7 +41,7 @@ function Screen() {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (isDone) return history.back();
|
||||
if (isDone) return router.history.push(redirect);
|
||||
|
||||
const groups = await ark.set_nstore(
|
||||
`lume_group_${label}`,
|
||||
|
||||
@@ -24,8 +24,10 @@ export const Route = createFileRoute("/foryou")({
|
||||
if (!interests) {
|
||||
throw redirect({
|
||||
to: "/interests",
|
||||
replace: false,
|
||||
search,
|
||||
search: {
|
||||
...search,
|
||||
redirect: "/foryou",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,10 @@ export const Route = createFileRoute("/group")({
|
||||
if (!groups) {
|
||||
throw redirect({
|
||||
to: "/create-group",
|
||||
replace: false,
|
||||
search,
|
||||
search: {
|
||||
...search,
|
||||
redirect: "/group",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ColumnRouteSearch } from "@lume/types";
|
||||
import { Column } from "@lume/ui";
|
||||
import { TOPICS, cn } from "@lume/utils";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -19,12 +19,14 @@ export const Route = createFileRoute("/interests")({
|
||||
|
||||
function Screen() {
|
||||
const { t } = useTranslation();
|
||||
const { label, name } = Route.useSearch();
|
||||
const { label, name, redirect } = Route.useSearch();
|
||||
const { ark } = Route.useRouteContext();
|
||||
|
||||
const [hashtags, setHashtags] = useState<string[]>([]);
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const toggleHashtag = (item: string) => {
|
||||
const arr = hashtags.includes(item)
|
||||
? hashtags.filter((i) => i !== item)
|
||||
@@ -40,7 +42,7 @@ function Screen() {
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (isDone) {
|
||||
return history.back();
|
||||
return router.history.push(redirect);
|
||||
}
|
||||
|
||||
const eventId = await ark.set_interest(undefined, undefined, hashtags);
|
||||
|
||||
@@ -38,23 +38,25 @@ function Screen() {
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-px flex-1 bg-white/20" />
|
||||
<span className="text-white">Or</span>
|
||||
<div className="text-white/70">{t("login.or")}</div>
|
||||
<div className="h-px flex-1 bg-white/20" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
to="/auth/remote"
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
|
||||
className="group inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
|
||||
>
|
||||
<RemoteIcon className="size-5" />
|
||||
Continue with Nostr Connect
|
||||
<RemoteIcon className="size-5 text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-400 dark:group-hover:text-neutral-600" />
|
||||
Nostr Connect
|
||||
<div className="size-5" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/auth/privkey"
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
|
||||
className="group inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
|
||||
>
|
||||
<KeyIcon className="size-5" />
|
||||
Continue with Private Key
|
||||
<KeyIcon className="size-5 text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-400 dark:group-hover:text-neutral-600" />
|
||||
Private Key
|
||||
<div className="size-5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { RepostNote } from "@/components/repost";
|
||||
import { Suggest } from "@/components/suggest";
|
||||
import { TextNote } from "@/components/text";
|
||||
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||
import {
|
||||
LoaderIcon,
|
||||
ArrowRightCircleIcon,
|
||||
InfoIcon,
|
||||
RepostIcon,
|
||||
} from "@lume/icons";
|
||||
import { ColumnRouteSearch, Event, Kind } from "@lume/types";
|
||||
import { Column } from "@lume/ui";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { Column, Note, User } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createFileRoute("/newsfeed")({
|
||||
component: Screen,
|
||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||
return {
|
||||
account: search.account,
|
||||
@@ -22,10 +25,9 @@ export const Route = createFileRoute("/newsfeed")({
|
||||
const ark = context.ark;
|
||||
const settings = await ark.get_settings();
|
||||
|
||||
return {
|
||||
settings,
|
||||
};
|
||||
return { settings };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
@@ -112,3 +114,145 @@ export function Screen() {
|
||||
</Column.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function TextNote({ event, className }: { event: Event; className?: string }) {
|
||||
const { settings } = Route.useRouteContext();
|
||||
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root
|
||||
className={cn(
|
||||
"flex flex-col gap-2 border-b border-neutral-100 px-3 py-5 dark:border-neutral-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Note.User />
|
||||
<div className="flex gap-3">
|
||||
<div className="size-11 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<Note.Content className="mb-2" />
|
||||
<Note.Thread />
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="-ml-1 inline-flex items-center gap-4">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
{settings.zap ? <Note.Zap /> : null}
|
||||
</div>
|
||||
<Note.Menu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function RepostNote({
|
||||
event,
|
||||
className,
|
||||
}: {
|
||||
event: Event;
|
||||
className?: string;
|
||||
}) {
|
||||
const { ark, settings } = Route.useRouteContext();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: repostEvent,
|
||||
} = useQuery({
|
||||
queryKey: ["repost", event.id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
if (event.content.length > 50) {
|
||||
const embed: Event = JSON.parse(event.content);
|
||||
return embed;
|
||||
}
|
||||
const id = event.tags.find((el) => el[0] === "e")[1];
|
||||
return await ark.get_event(id);
|
||||
} catch {
|
||||
throw new Error("Failed to get repost event");
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="w-full px-3 pb-3">Loading...</div>;
|
||||
}
|
||||
|
||||
if (isError || !repostEvent) {
|
||||
return (
|
||||
<Note.Root
|
||||
className={cn(
|
||||
"flex flex-col gap-2 border-b border-neutral-100 px-3 py-5 dark:border-neutral-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<User.Provider pubkey={event.pubkey}>
|
||||
<User.Root className="flex h-14 gap-2 px-3">
|
||||
<div className="inline-flex w-10 shrink-0 items-center justify-center">
|
||||
<RepostIcon className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<User.Avatar className="size-6 shrink-0 rounded object-cover" />
|
||||
<div className="inline-flex items-baseline gap-1">
|
||||
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
|
||||
<span className="text-blue-500">{t("note.reposted")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="mb-3 select-text px-3">
|
||||
<div className="flex flex-col items-start justify-start rounded-lg bg-red-100 px-3 py-3 dark:bg-red-900">
|
||||
<p className="text-red-500">Failed to get event</p>
|
||||
</div>
|
||||
</div>
|
||||
</Note.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Note.Root
|
||||
className={cn(
|
||||
"flex flex-col gap-2 border-b border-neutral-100 px-3 py-5 dark:border-neutral-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<User.Provider pubkey={event.pubkey}>
|
||||
<User.Root className="flex gap-3">
|
||||
<div className="inline-flex w-11 shrink-0 items-center justify-center">
|
||||
<RepostIcon className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
|
||||
<div className="inline-flex items-baseline gap-1">
|
||||
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
|
||||
<span className="text-blue-500">{t("note.reposted")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<Note.Provider event={repostEvent}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Note.User />
|
||||
<div className="flex gap-3">
|
||||
<div className="size-11 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<Note.Content />
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="-ml-1 inline-flex items-center gap-4">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
{settings.zap ? <Note.Zap /> : null}
|
||||
</div>
|
||||
<Note.Menu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Note.Provider>
|
||||
</Note.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Link } from "@tanstack/react-router";
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/store")({
|
||||
component: Screen,
|
||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||
return {
|
||||
account: search.account,
|
||||
@@ -14,6 +13,7 @@ export const Route = createFileRoute("/store")({
|
||||
name: search.name,
|
||||
};
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
|
||||
Reference in New Issue
Block a user