final design (#184)

* feat: redesign

* feat: update other columns to new design

* chore: small fixes

* fix: better manage external webview

* feat: redesign note

* feat: update ui

* chore: update

* chore: update

* chore: polish ui

* chore: update auth ui

* feat: finalize note design

* chore: small fixes

* feat: add window management in rust

* chore: format

* feat: update ui for event screen

* feat: update event screen

* feat: final
This commit is contained in:
雨宮蓮
2024-05-03 15:15:48 +07:00
committed by GitHub
parent 61d1f095d4
commit a4aef25adb
250 changed files with 9360 additions and 9235 deletions

View File

@@ -20,21 +20,21 @@
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.31.0", "@tanstack/query-sync-storage-persister": "^5.32.0",
"@tanstack/react-query": "^5.31.0", "@tanstack/react-query": "^5.32.0",
"@tanstack/react-query-persist-client": "^5.31.0", "@tanstack/react-query-persist-client": "^5.32.0",
"@tanstack/react-router": "^1.29.2", "@tanstack/react-router": "1.29.2",
"i18next": "^23.11.2", "i18next": "^23.11.3",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"minidenticons": "^4.2.1", "minidenticons": "^4.2.1",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"nostr-tools": "^2.5.0", "nostr-tools": "^2.5.1",
"react": "^18.2.0", "react": "^18.3.1",
"react-currency-input-field": "^3.8.0", "react-currency-input-field": "^3.8.0",
"react-dom": "^18.2.0", "react-dom": "^18.3.1",
"react-hook-form": "^7.51.3", "react-hook-form": "^7.51.3",
"react-hotkeys-hook": "^4.5.0", "react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.1",
"slate": "^0.102.0", "slate": "^0.102.0",
"slate-react": "^0.102.0", "slate-react": "^0.102.0",
"sonner": "^1.4.41", "sonner": "^1.4.41",
@@ -45,10 +45,10 @@
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.29.2", "@tanstack/router-devtools": "^1.31.3",
"@tanstack/router-vite-plugin": "^1.30.0", "@tanstack/router-vite-plugin": "^1.30.0",
"@types/react": "^18.2.79", "@types/react": "^18.3.1",
"@types/react-dom": "^18.2.25", "@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.6.0", "@vitejs/plugin-react-swc": "^3.6.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"postcss": "^8.4.38", "postcss": "^8.4.38",

View File

@@ -1,19 +1,21 @@
import { Ark } from "@lume/ark";
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { QueryClient } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { platform } from "@tauri-apps/plugin-os";
import React, { StrictMode } from "react"; import React, { StrictMode } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import { Toaster } from "sonner";
import "./app.css"; import "./app.css";
import i18n from "./locale"; import i18n from "./locale";
import { Toaster } from "sonner";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { routeTree } from "./router.gen"; // auto generated file import { routeTree } from "./router.gen"; // auto generated file
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
import { Ark } from "@lume/ark";
const ark = new Ark(); const ark = new Ark();
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const platformName = await platform();
const persister = createSyncStoragePersister({ const persister = createSyncStoragePersister({
storage: window.localStorage, storage: window.localStorage,
@@ -25,6 +27,7 @@ const router = createRouter({
context: { context: {
ark, ark,
queryClient, queryClient,
platform: platformName,
}, },
}); });

View File

@@ -1,69 +0,0 @@
import { Account } from "@lume/types";
import { User } from "@lume/ui";
import {
useNavigate,
useParams,
useRouteContext,
} from "@tanstack/react-router";
import { useEffect, useState } from "react";
export function Accounts() {
const { ark } = useRouteContext({ strict: false });
const params = useParams({ strict: false });
const [accounts, setAccounts] = useState<Account[]>(null);
useEffect(() => {
async function getAllAccounts() {
const data = await ark.get_all_accounts();
if (data) setAccounts(data);
}
getAllAccounts();
}, []);
return (
<div data-tauri-drag-region className="flex items-center gap-3">
{accounts
? accounts.map((account) =>
// @ts-ignore, useless
account.npub === params.account ? (
<Active key={account.npub} pubkey={account.npub} />
) : (
<Inactive key={account.npub} pubkey={account.npub} />
),
)
: null}
</div>
);
}
function Inactive({ pubkey }: { pubkey: string }) {
const { ark } = useRouteContext({ strict: false });
const navigate = useNavigate();
const changeAccount = async (npub: string) => {
const select = await ark.load_selected_account(npub);
if (select) navigate({ to: "/$account/home", params: { account: npub } });
};
return (
<button type="button" onClick={() => changeAccount(pubkey)}>
<User.Provider pubkey={pubkey}>
<User.Root className="rounded-full">
<User.Avatar className="aspect-square h-auto w-8 rounded-full object-cover" />
</User.Root>
</User.Provider>
</button>
);
}
function Active({ pubkey }: { pubkey: string }) {
return (
<User.Provider pubkey={pubkey}>
<User.Root className="rounded-full ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950">
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
</User.Root>
</User.Provider>
);
}

View File

@@ -1,7 +1,12 @@
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { Dispatch, ReactNode, SetStateAction, useState } from "react"; import {
type Dispatch,
type ReactNode,
type SetStateAction,
useState,
} from "react";
import { toast } from "sonner"; import { toast } from "sonner";
export function AvatarUploader({ export function AvatarUploader({

View File

@@ -1,6 +1,9 @@
import { useEffect, useRef, useState } from "react"; import { CancelIcon, CheckIcon } from "@lume/icons";
import type { LumeColumn } from "@lume/types"; import type { LumeColumn } from "@lume/types";
import { cn } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useEffect, useRef, useState } from "react";
export function Col({ export function Col({
column, column,
@@ -52,9 +55,7 @@ export function Col({
const rect = container.current.getBoundingClientRect(); const rect = container.current.getBoundingClientRect();
const windowLabel = `column-${column.label}`; const windowLabel = `column-${column.label}`;
const url = const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
column.content +
`?account=${account}&label=${column.label}&name=${column.name}`;
// create new webview // create new webview
const label: string = await invoke("create_column", { const label: string = await invoke("create_column", {
@@ -79,5 +80,81 @@ export function Col({
}; };
}, [webview]); }, [webview]);
return <div ref={container} className="h-full w-[440px] shrink-0 p-2" />; return (
<div className="h-full w-[440px] shrink-0 p-2">
<div
className={cn(
"flex flex-col w-full h-full rounded-xl",
column.label !== "open"
? "bg-black/5 dark:bg-white/5 backdrop-blur-sm"
: "",
)}
>
{column.label !== "open" ? (
<Header label={column.label} name={column.name} />
) : null}
<div ref={container} className="flex-1 w-full h-full" />
</div>
</div>
);
}
function Header({ label, name }: { label: string; name: string }) {
const [title, setTitle] = useState(name);
const [isChanged, setIsChanged] = useState(false);
const saveNewTitle = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "set_title", label, title });
// update search params
// @ts-ignore, hahaha
search.name = title;
// reset state
setIsChanged(false);
};
const close = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "remove", label });
};
useEffect(() => {
if (title.length !== name.length) setIsChanged(true);
}, [title]);
return (
<div className="h-9 w-full flex items-center justify-between shrink-0 px-1">
<div className="size-7" />
<div className="shrink-0 h-9 flex items-center justify-center">
<div className="relative flex gap-2 items-center">
<div
contentEditable
suppressContentEditableWarning={true}
onBlur={(e) => setTitle(e.currentTarget.textContent)}
className="text-sm font-medium focus:outline-none"
>
{name}
</div>
{isChanged ? (
<button
type="button"
onClick={() => saveNewTitle()}
className="text-teal-500 hover:text-teal-600"
>
<CheckIcon className="size-4" />
</button>
) : null}
</div>
</div>
<button
type="button"
onClick={() => close()}
className="size-7 inline-flex hover:bg-black/10 rounded-lg dark:hover:bg-white/10 items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
>
<CancelIcon className="size-4" />
</button>
</div>
);
} }

View File

@@ -0,0 +1,55 @@
import { ThreadIcon } from "@lume/icons";
import type { Event } from "@lume/types";
import { Note } from "@lume/ui";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
export function Conversation({
event,
className,
}: {
event: Event;
className?: string;
}) {
const { ark } = useRouteContext({ strict: false });
const thread = ark.parse_event_thread({
content: event.content,
tags: event.tags,
});
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div className="flex flex-col gap-3">
{thread?.rootEventId ? (
<Note.Child eventId={thread?.rootEventId} isRoot />
) : null}
<div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<ThreadIcon className="size-4" />
Thread
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
{thread?.replyEventId ? (
<Note.Child eventId={thread?.replyEventId} />
) : null}
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -0,0 +1,47 @@
import { QuoteIcon } from "@lume/icons";
import type { Event } from "@lume/types";
import { Note } from "@lume/ui";
import { cn } from "@lume/utils";
export function Quote({
event,
className,
}: {
event: Event;
className?: string;
}) {
const quoteEventId = event.tags.find(
(tag) => tag[0] === "q" || tag[3] === "mention",
)?.[1];
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div className="flex flex-col gap-3">
<Note.Child eventId={quoteEventId} isRoot />
<div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<QuoteIcon className="size-4" />
Quote
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" quote={false} clean />
</div>
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -1,9 +1,7 @@
import { RepostIcon } from "@lume/icons"; import type { Event } from "@lume/types";
import { Event } from "@lume/types"; import { Note, Spinner, User } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { Note, Spinner, User } from "@lume/ui";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
export function RepostNote({ export function RepostNote({
@@ -13,8 +11,7 @@ export function RepostNote({
event: Event; event: Event;
className?: string; className?: string;
}) { }) {
const { ark, settings } = useRouteContext({ strict: false }); const { ark } = useRouteContext({ strict: false });
const { t } = useTranslation();
const { const {
isLoading, isLoading,
isError, isError,
@@ -27,8 +24,11 @@ export function RepostNote({
const embed: Event = JSON.parse(event.content); const embed: Event = JSON.parse(event.content);
return embed; return embed;
} }
const id = event.tags.find((el) => el[0] === "e")?.[1]; const id = event.tags.find((el) => el[0] === "e")?.[1];
if (id) return await ark.get_event(id); const repostEvent = await ark.get_event(id);
return repostEvent;
} catch (e) { } catch (e) {
throw new Error(e); throw new Error(e);
} }
@@ -40,50 +40,42 @@ export function RepostNote({
return ( return (
<Note.Root <Note.Root
className={cn( className={cn(
"flex flex-col gap-2 border-b border-neutral-100 px-3 py-5 dark:border-neutral-900", "bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl mb-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className, className,
)} )}
> >
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<User.Root className="flex gap-3"> <User.Root className="flex items-center gap-2 px-3 py-3 border-b border-neutral-100 dark:border-neutral-800/50 rounded-t-xl">
<div className="inline-flex w-11 shrink-0 items-center justify-center"> <div className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
<RepostIcon className="h-5 w-5 text-blue-500" /> Reposted by
</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> </div>
<User.Avatar className="size-6 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
{isLoading ? ( {isLoading ? (
<div className="flex h-20 items-center justify-center gap-2">
<Spinner /> <Spinner />
) : isError ? ( Loading event...
<div className="w-full h-16 flex items-center px-3 border border-neutral-100 dark:border-neutral-900"> </div>
<p>Event not found</p> ) : isError || !repostEvent ? (
<div className="flex h-20 items-center justify-center">
Event not found within your current relay set
</div> </div>
) : ( ) : (
<Note.Provider event={repostEvent}> <Note.Provider event={repostEvent}>
<div className="flex flex-col gap-2"> <Note.Root>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User /> <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 />
<Note.Pin />
{settings.zap ? <Note.Zap /> : null}
</div>
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.Content className="px-3" />
<div className="mt-3 flex items-center gap-4 h-14 px-3">
<Note.Open />
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div> </div>
</div> </Note.Root>
</div>
</Note.Provider> </Note.Provider>
)} )}
</Note.Root> </Note.Root>

View File

@@ -1,7 +1,6 @@
import { Event } from "@lume/types"; import type { Event } from "@lume/types";
import { Note } from "@lume/ui"; import { Note } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
export function TextNote({ export function TextNote({
event, event,
@@ -10,31 +9,24 @@ export function TextNote({
event: Event; event: Event;
className?: string; className?: string;
}) { }) {
const { settings } = useRouteContext({ strict: false });
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root
className={cn( className={cn(
"flex flex-col gap-2 border-b border-neutral-100 px-3 py-5 dark:border-neutral-900", "bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className, className,
)} )}
> >
<div className="px-3 h-14 flex items-center justify-between">
<Note.User /> <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 /> <Note.Menu />
</div> </div>
</div> <Note.Content className="px-3" />
<div className="mt-3 flex items-center gap-4 h-14 px-3">
<Note.Open />
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div> </div>
</Note.Root> </Note.Root>
</Note.Provider> </Note.Provider>

View File

@@ -1,4 +1,4 @@
import { ReactNode } from "@tanstack/react-router"; import type { ReactNode } from "@tanstack/react-router";
import { useLayoutEffect, useState } from "react"; import { useLayoutEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";

View File

@@ -2,7 +2,6 @@ import { Col } from "@/components/col";
import { Toolbar } from "@/components/toolbar"; import { Toolbar } from "@/components/toolbar";
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons"; import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import type { EventColumns, LumeColumn } from "@lume/types"; import type { EventColumns, LumeColumn } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { resolveResource } from "@tauri-apps/api/path"; import { resolveResource } from "@tauri-apps/api/path";
@@ -14,8 +13,6 @@ import { useDebouncedCallback } from "use-debounce";
import { VList, type VListHandle } from "virtua"; import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({ export const Route = createFileRoute("/$account/home")({
component: Screen,
pendingComponent: Pending,
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const resourcePath = await resolveResource("resources/system_columns.json"); const resourcePath = await resolveResource("resources/system_columns.json");
@@ -28,6 +25,7 @@ export const Route = createFileRoute("/$account/home")({
storedColumns: !userColumns.length ? systemColumns : userColumns, storedColumns: !userColumns.length ? systemColumns : userColumns,
}; };
}, },
component: Screen,
}); });
function Screen() { function Screen() {
@@ -71,20 +69,24 @@ function Screen() {
]; ];
setColumns(newCols); setColumns(newCols);
setSelectedIndex(cols.length - 1); setSelectedIndex(newCols.length);
setIsScroll(true);
// scroll to the newest column // scroll to the newest column
vlistRef.current.scrollToIndex(cols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "end", align: "end",
}); });
}, 150); }, 150);
const remove = useDebouncedCallback((label: string) => { const remove = useDebouncedCallback((label: string) => {
setColumns((state) => state.filter((t) => t.label !== label)); const newCols = columns.filter((t) => t.label !== label);
setSelectedIndex(columns.length - 1);
setColumns(newCols);
setSelectedIndex(newCols.length);
setIsScroll(true);
// scroll to the first column // scroll to the first column
vlistRef.current.scrollToIndex(0, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "start", align: "start",
}); });
}, 150); }, 150);
@@ -165,14 +167,14 @@ function Screen() {
<button <button
type="button" type="button"
onClick={() => goLeft()} onClick={() => goLeft()}
className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-neutral-200 dark:text-neutral-200 dark:hover:bg-neutral-800" className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
> >
<ArrowLeftIcon className="size-5" /> <ArrowLeftIcon className="size-5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => goRight()} onClick={() => goRight()}
className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-neutral-200 dark:text-neutral-200 dark:hover:bg-neutral-800" className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
</button> </button>
@@ -181,13 +183,3 @@ function Screen() {
</div> </div>
); );
} }
function Pending() {
return (
<div className="flex h-full w-full items-center justify-center">
<button type="button" className="size-5" disabled>
<Spinner className="size-5" />
</button>
</div>
);
}

View File

@@ -1,19 +1,16 @@
import { ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons"; import { ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router"; import type { Account } from "@lume/types";
import { User } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { Accounts } from "@/components/accounts"; import { Outlet, createFileRoute } from "@tanstack/react-router";
import { platform } from "@tauri-apps/plugin-os"; import { useEffect, useState } from "react";
export const Route = createFileRoute("/$account")({ export const Route = createFileRoute("/$account")({
component: App, component: Screen,
beforeLoad: async () => {
const platformName = await platform();
return { platform: platformName };
},
}); });
function App() { function Screen() {
const navigate = useNavigate(); const navigate = Route.useNavigate();
const { ark, platform } = Route.useRouteContext(); const { ark, platform } = Route.useRouteContext();
return ( return (
@@ -30,16 +27,16 @@ function App() {
<button <button
type="button" type="button"
onClick={() => navigate({ to: "/landing" })} onClick={() => navigate({ to: "/landing" })}
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" className="inline-flex size-8 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
> >
<PlusIcon className="size-5" /> <PlusIcon className="size-5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => ark.open_search()} onClick={() => ark.open_search()}
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" className="inline-flex size-8 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
> >
<SearchIcon className="size-5" /> <SearchIcon className="size-4" />
</button> </button>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -60,3 +57,57 @@ function App() {
</div> </div>
); );
} }
export function Accounts() {
const navigate = Route.useNavigate();
const { ark } = Route.useRouteContext();
const { account } = Route.useParams();
const [accounts, setAccounts] = useState<Account[]>([]);
const changeAccount = async (npub: string) => {
if (npub === account) return;
const select = await ark.load_selected_account(npub);
if (select)
return navigate({ to: "/$account/home", params: { account: npub } });
};
useEffect(() => {
async function getAllAccounts() {
const data = await ark.get_all_accounts();
if (data) setAccounts(data);
}
getAllAccounts();
}, []);
return (
<div data-tauri-drag-region className="flex items-center gap-3">
{accounts.map((user) => (
<button
key={user.npub}
type="button"
onClick={() => changeAccount(user.npub)}
>
<User.Provider pubkey={user.npub}>
<User.Root
className={cn(
"rounded-full",
user.npub === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "",
)}
>
<User.Avatar
className={cn(
"aspect-square h-auto rounded-full object-cover",
user.npub === account ? "w-7" : "w-8",
)}
/>
</User.Root>
</User.Provider>
</button>
))}
</div>
);
}

View File

@@ -1,16 +1,10 @@
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; import type { Ark } from "@lume/ark";
import { type Ark } from "@lume/ark"; import type { Account, Interests, Metadata, Settings } from "@lume/types";
import { type QueryClient } from "@tanstack/react-query";
import { type Platform } from "@tauri-apps/plugin-os";
import type {
Account,
Contact,
Interests,
Metadata,
Settings,
} from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { type Descendant } from "slate"; import type { QueryClient } from "@tanstack/react-query";
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import type { Platform } from "@tauri-apps/plugin-os";
import type { Descendant } from "slate";
type EditorElement = { type EditorElement = {
type: string; type: string;
@@ -19,16 +13,20 @@ type EditorElement = {
}; };
interface RouterContext { interface RouterContext {
// System
ark: Ark; ark: Ark;
queryClient: QueryClient; queryClient: QueryClient;
// App info
platform?: Platform; platform?: Platform;
locale?: string; locale?: string;
// Settings
settings?: Settings; settings?: Settings;
interests?: Interests; interests?: Interests;
// Profile
accounts?: Account[]; accounts?: Account[];
initialValue?: EditorElement[];
profile?: Metadata; profile?: Metadata;
contacts?: Contact[]; // Editor
initialValue?: EditorElement[];
} }
export const Route = createRootRouteWithContext<RouterContext>()({ export const Route = createRootRouteWithContext<RouterContext>()({
@@ -40,9 +38,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
function Pending() { function Pending() {
return ( return (
<div className="flex h-screen w-screen flex-col items-center justify-center"> <div className="flex h-screen w-screen flex-col items-center justify-center">
<button type="button" className="size-5" disabled>
<Spinner className="size-5" /> <Spinner className="size-5" />
</button>
</div> </div>
); );
} }

View File

@@ -1,119 +0,0 @@
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { ColumnRouteSearch, Event, Kind } from "@lume/types";
import { Column, Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/antenas")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
export function Screen() {
const { label, name, account } = Route.useSearch();
const { ark } = Route.useRouteContext();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [name, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return (
<Column.Root>
<Column.Header label={label} name={name} />
<Column.Content>
{isLoading ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<Spinner className="size-5" />
</div>
) : !data.length ? (
<Empty />
) : (
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
</Virtualizer>
)}
<div className="flex h-20 items-center justify-center">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</Column.Content>
</Column.Root>
);
}
function Empty() {
return (
<div className="flex flex-col py-10 gap-10">
<div className="text-center flex flex-col items-center justify-center">
<div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8">
<div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" />
</div>
<p className="text-lg font-medium">Your newsfeed is empty</p>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Here are few suggestions to get started.
</p>
</div>
<div className="flex flex-col px-3 gap-2">
<Link
to="/trending/notes"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
>
<ArrowRightIcon className="size-5" />
Show trending notes
</Link>
<Link
to="/trending/users"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
>
<ArrowRightIcon className="size-5" />
Discover trending users
</Link>
</div>
</div>
);
}

View File

@@ -1,13 +1,13 @@
import { CheckIcon } from "@lume/icons";
import type { AppRouteSearch } from "@lume/types";
import { displayNsec } from "@lume/utils"; import { displayNsec } from "@lume/utils";
import * as Checkbox from "@radix-ui/react-checkbox";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import * as Checkbox from "@radix-ui/react-checkbox";
import { CheckIcon } from "@lume/icons";
import { AppRouteSearch } from "@lume/types";
export const Route = createFileRoute("/auth/new/backup")({ export const Route = createFileRoute("/auth/new/backup")({
validateSearch: (search: Record<string, string>): AppRouteSearch => { validateSearch: (search: Record<string, string>): AppRouteSearch => {
@@ -34,13 +34,13 @@ function Screen() {
if (key) { if (key) {
if (!confirm.c1 || !confirm.c2 || !confirm.c3) { if (!confirm.c1 || !confirm.c2 || !confirm.c3) {
return toast.warning("You need to confirm before continue"); return toast.warning("You need to confirm before continue");
} else { }
return navigate({ return navigate({
to: "/auth/settings", to: "/auth/settings",
search: { account }, search: { account },
}); });
} }
}
const encrypted: string = await invoke("get_encrypted_key", { const encrypted: string = await invoke("get_encrypted_key", {
npub: account, npub: account,
@@ -82,7 +82,7 @@ function Screen() {
type="password" type="password"
value={passphase} value={passphase}
onChange={(e) => setPassphase(e.target.value)} onChange={(e) => setPassphase(e.target.value)}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900" className="w-full h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
</div> </div>
@@ -98,12 +98,12 @@ function Screen() {
type="text" type="text"
value={displayNsec(key, 36)} value={displayNsec(key, 36)}
readOnly readOnly
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900" className="w-full h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
<button <button
type="button" type="button"
onClick={copyKey} onClick={() => copyKey()}
className="inline-flex h-11 w-24 items-center justify-center rounded-lg bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700" className="inline-flex h-11 w-24 items-center justify-center rounded-lg bg-neutral-200 hover:bg-neutral-300 dark:bg-white/20 dark:hover:bg-white/30"
> >
{copied ? "Copied" : "Copy"} {copied ? "Copied" : "Copy"}
</button> </button>
@@ -118,7 +118,7 @@ function Screen() {
onCheckedChange={() => onCheckedChange={() =>
setConfirm((state) => ({ ...state, c1: !state.c1 })) setConfirm((state) => ({ ...state, c1: !state.c1 }))
} }
className="flex size-6 appearance-none items-center justify-center rounded-md bg-neutral-100 outline-none dark:bg-neutral-900" className="flex size-6 appearance-none items-center justify-center rounded-md bg-neutral-100 outline-none dark:bg-white/10 dark:hover:bg-white/20"
id="confirm1" id="confirm1"
> >
<Checkbox.Indicator className="text-blue-500"> <Checkbox.Indicator className="text-blue-500">
@@ -138,7 +138,7 @@ function Screen() {
onCheckedChange={() => onCheckedChange={() =>
setConfirm((state) => ({ ...state, c2: !state.c2 })) setConfirm((state) => ({ ...state, c2: !state.c2 }))
} }
className="flex size-6 appearance-none items-center justify-center rounded-md bg-neutral-100 outline-none dark:bg-neutral-900" className="flex size-6 appearance-none items-center justify-center rounded-md bg-neutral-100 outline-none dark:bg-white/10 dark:hover:bg-white/20"
id="confirm2" id="confirm2"
> >
<Checkbox.Indicator className="text-blue-500"> <Checkbox.Indicator className="text-blue-500">
@@ -158,7 +158,7 @@ function Screen() {
onCheckedChange={() => onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 })) setConfirm((state) => ({ ...state, c3: !state.c3 }))
} }
className="flex size-6 appearance-none items-center justify-center rounded-md bg-neutral-100 outline-none dark:bg-neutral-900" className="flex size-6 appearance-none items-center justify-center rounded-md bg-neutral-100 outline-none dark:bg-white/10 dark:hover:bg-white/20"
id="confirm3" id="confirm3"
> >
<Checkbox.Indicator className="text-blue-500"> <Checkbox.Indicator className="text-blue-500">
@@ -179,7 +179,7 @@ function Screen() {
<div> <div>
<button <button
type="button" type="button"
onClick={submit} onClick={() => submit()}
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50" className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
> >
{t("global.continue")} {t("global.continue")}

View File

@@ -1,6 +1,6 @@
import { AvatarUploader } from "@/components/avatarUploader"; import { AvatarUploader } from "@/components/avatarUploader";
import { PlusIcon } from "@lume/icons"; import { PlusIcon } from "@lume/icons";
import { Metadata } from "@lume/types"; import type { Metadata } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
@@ -74,7 +74,7 @@ function Screen() {
) : null} ) : null}
<AvatarUploader <AvatarUploader
setPicture={setPicture} setPicture={setPicture}
className="absolute inset-0 z-20 flex h-full w-full items-center justify-center rounded-full bg-black/10 text-white hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" className="absolute inset-0 z-20 flex h-full w-full items-center justify-center rounded-full dark:text-black bg-black/10 text-white hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
> >
<PlusIcon className="size-8" /> <PlusIcon className="size-8" />
</AvatarUploader> </AvatarUploader>
@@ -93,7 +93,7 @@ function Screen() {
{...register("display_name", { required: true, minLength: 1 })} {...register("display_name", { required: true, minLength: 1 })}
placeholder="e.g. Alice in Nostrland" placeholder="e.g. Alice in Nostrland"
spellCheck={false} spellCheck={false}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@@ -105,7 +105,7 @@ function Screen() {
{...register("name")} {...register("name")}
placeholder="e.g. alice" placeholder="e.g. alice"
spellCheck={false} spellCheck={false}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@@ -116,7 +116,7 @@ function Screen() {
{...register("about")} {...register("about")}
placeholder="e.g. Artist, anime-lover, and k-pop fan" placeholder="e.g. Artist, anime-lover, and k-pop fan"
spellCheck={false} spellCheck={false}
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@@ -128,7 +128,7 @@ function Screen() {
{...register("website")} {...register("website")}
placeholder="e.g. https://alice.me" placeholder="e.g. https://alice.me"
spellCheck={false} spellCheck={false}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<button <button

View File

@@ -58,7 +58,7 @@ function Screen() {
placeholder="nsec or ncryptsec..." placeholder="nsec or ncryptsec..."
value={key} value={key}
onChange={(e) => setKey(e.target.value)} onChange={(e) => setKey(e.target.value)}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@@ -73,12 +73,12 @@ function Screen() {
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<button <button
type="button" type="button"
onClick={submit} onClick={() => submit()}
disabled={loading} disabled={loading}
className="mt-3 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50" className="mt-3 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
> >

View File

@@ -57,12 +57,12 @@ function Screen() {
placeholder="bunker://..." placeholder="bunker://..."
value={uri} value={uri}
onChange={(e) => setUri(e.target.value)} onChange={(e) => setUri(e.target.value)}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<button <button
type="button" type="button"
onClick={submit} onClick={() => submit()}
disabled={loading} disabled={loading}
className="mt-3 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50" className="mt-3 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
> >

View File

@@ -1,15 +1,15 @@
import { LaurelIcon } from "@lume/icons"; import { LaurelIcon } from "@lume/icons";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import type { AppRouteSearch, Settings } from "@lume/types";
import { useTranslation } from "react-i18next"; import { Spinner } from "@lume/ui";
import * as Switch from "@radix-ui/react-switch"; import * as Switch from "@radix-ui/react-switch";
import { useState } from "react"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { AppRouteSearch, Settings } from "@lume/types";
import { import {
isPermissionGranted, isPermissionGranted,
requestPermission, requestPermission,
} from "@tauri-apps/plugin-notification"; } from "@tauri-apps/plugin-notification";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { Spinner } from "@lume/ui";
export const Route = createFileRoute("/auth/settings")({ export const Route = createFileRoute("/auth/settings")({
validateSearch: (search: Record<string, string>): AppRouteSearch => { validateSearch: (search: Record<string, string>): AppRouteSearch => {
@@ -97,7 +97,7 @@ function Screen() {
return ( return (
<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="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 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"> <div className="flex size-20 items-center justify-center rounded-full bg-teal-100 dark:bg-teal-950 text-teal-500">
<LaurelIcon className="size-8" /> <LaurelIcon className="size-8" />
</div> </div>
<div> <div>
@@ -111,14 +111,7 @@ function Screen() {
</div> </div>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900"> <div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<Switch.Root
checked={newSettings.notification}
onClick={() => toggleNofitication()}
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"> <div className="flex-1">
<h3 className="font-semibold">Push Notification</h3> <h3 className="font-semibold">Push Notification</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300"> <p className="text-sm text-neutral-700 dark:text-neutral-300">
@@ -126,15 +119,15 @@ function Screen() {
notifications from Lume. notifications from Lume.
</p> </p>
</div> </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 <Switch.Root
checked={newSettings.enhancedPrivacy} checked={newSettings.notification}
onClick={() => toggleEnhancedPrivacy()} onClick={() => toggleNofitication()}
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" 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-white/20"
> >
<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.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> </Switch.Root>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold">Enhanced Privacy</h3> <h3 className="font-semibold">Enhanced Privacy</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300"> <p className="text-sm text-neutral-700 dark:text-neutral-300">
@@ -142,30 +135,30 @@ function Screen() {
preview as plain text. preview as plain text.
</p> </p>
</div> </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 <Switch.Root
checked={newSettings.autoUpdate} checked={newSettings.enhancedPrivacy}
onClick={() => toggleAutoUpdate()} onClick={() => toggleEnhancedPrivacy()}
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" 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-white/20"
> >
<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.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> </Switch.Root>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold">Auto Update</h3> <h3 className="font-semibold">Auto Update</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300"> <p className="text-sm text-neutral-700 dark:text-neutral-300">
Automatically download and install new version. Automatically download and install new version.
</p> </p>
</div> </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 <Switch.Root
checked={newSettings.zap} checked={newSettings.autoUpdate}
onClick={() => toggleZap()} onClick={() => toggleAutoUpdate()}
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" 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-white/20"
> >
<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.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> </Switch.Root>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold">Zap</h3> <h3 className="font-semibold">Zap</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300"> <p className="text-sm text-neutral-700 dark:text-neutral-300">
@@ -173,15 +166,15 @@ function Screen() {
for send Bitcoin tip to other users. for send Bitcoin tip to other users.
</p> </p>
</div> </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 <Switch.Root
checked={newSettings.nsfw} checked={newSettings.zap}
onClick={() => toggleNsfw()} 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" 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-white/20"
> >
<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.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> </Switch.Root>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold">Filter sensitive content</h3> <h3 className="font-semibold">Filter sensitive content</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300"> <p className="text-sm text-neutral-700 dark:text-neutral-300">
@@ -189,11 +182,18 @@ function Screen() {
Warning tag, it's may include NSFW content. Warning tag, it's may include NSFW content.
</p> </p>
</div> </div>
<Switch.Root
checked={newSettings.nsfw}
onClick={() => toggleNsfw()}
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-white/20"
>
<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> </div>
</div> </div>
<button <button
type="button" type="button"
onClick={submit} onClick={() => submit()}
disabled={loading} disabled={loading}
className="mb-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50" className="mb-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
> >

View File

@@ -1,6 +1,6 @@
import { CheckCircleIcon } from "@lume/icons"; import { CheckCircleIcon } from "@lume/icons";
import { ColumnRouteSearch } from "@lume/types"; import type { ColumnRouteSearch } from "@lume/types";
import { Column, Spinner, User } from "@lume/ui"; import { Spinner, User } from "@lume/ui";
import { createFileRoute, useRouter } from "@tanstack/react-router"; import { createFileRoute, useRouter } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -26,7 +26,7 @@ function Screen() {
const router = useRouter(); const router = useRouter();
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const { label, name, redirect } = Route.useSearch(); const { label, redirect } = Route.useSearch();
const [title, setTitle] = useState<string>("Just a new group"); const [title, setTitle] = useState<string>("Just a new group");
const [users, setUsers] = useState<Array<string>>([]); const [users, setUsers] = useState<Array<string>>([]);
@@ -65,9 +65,7 @@ function Screen() {
}; };
return ( return (
<Column.Root> <div className="h-full overflow-y-auto scrollbar-none">
<Column.Header label={label} name={name} />
<Column.Content>
<div className="flex flex-col gap-5 p-3"> <div className="flex flex-col gap-5 p-3">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="name" className="font-medium"> <label htmlFor="name" className="font-medium">
@@ -78,13 +76,13 @@ function Screen() {
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Nostrichs..." placeholder="Nostrichs..."
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="h-10 rounded-lg bg-transparent border border-neutral-300 dark:border-neutral-700 px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between">
<span className="font-medium">Pick user</span> <span className="font-medium">Pick user</span>
<span className="text-xs text-neutral-600 dark:text-neutral-400">{`${users.length} / ∞`}</span> <span className="text-neutral-600 dark:text-neutral-400">{`${users.length} / ∞`}</span>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{contacts.map((item: string) => ( {contacts.map((item: string) => (
@@ -92,14 +90,14 @@ function Screen() {
key={item} key={item}
type="button" type="button"
onClick={() => toggleUser(item)} onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-xl bg-neutral-50 dark:bg-neutral-950 hover:bg-neutral-100 dark:hover:bg-neutral-900" className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-black/10 dark:bg-white/10 hover:bg-black/20 dark:hover:bg-white/20"
> >
<User.Provider pubkey={item}> <User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5"> <User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-10 rounded-full object-cover" /> <User.Avatar className="size-10 rounded-full object-cover" />
<div className="flex flex-col items-start"> <div className="flex items-center gap-1">
<User.Name className="font-medium" /> <User.Name className="font-medium" />
<User.NIP05 className="text-neutral-700 dark:text-neutral-300" /> <User.NIP05 />
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>
@@ -112,16 +110,17 @@ function Screen() {
</div> </div>
</div> </div>
<div className="fixed z-10 flex items-center justify-center w-full bottom-6"> <div className="fixed z-10 flex items-center justify-center w-full bottom-6">
{users.length >= 1 ? (
<button <button
type="button" type="button"
onClick={submit} onClick={() => submit()}
disabled={users.length < 1} disabled={users.length < 1}
className="inline-flex items-center justify-center px-4 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-26 h-9 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed" className="inline-flex items-center justify-center px-4 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-32 h-10 hover:bg-blue-600 focus:outline-none"
> >
{isDone ? "Back" : loading ? <Spinner /> : "Update"} {isDone ? "Back" : loading ? <Spinner /> : "Update"}
</button> </button>
) : null}
</div>
</div> </div>
</Column.Content>
</Column.Root>
); );
} }

View File

@@ -1,13 +1,13 @@
import { AddMediaIcon } from "@lume/icons"; import { AddMediaIcon } from "@lume/icons";
import { Spinner } from "@lume/ui";
import { cn, insertImage, isImagePath } from "@lume/utils"; import { cn, insertImage, isImagePath } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useRouteContext } from "@tanstack/react-router";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSlateStatic } from "slate-react"; import { useSlateStatic } from "slate-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { getCurrent } from "@tauri-apps/api/window";
import { UnlistenFn } from "@tauri-apps/api/event";
import { useRouteContext } from "@tanstack/react-router";
import { Spinner } from "@lume/ui";
import * as Tooltip from "@radix-ui/react-tooltip";
export function MediaButton({ className }: { className?: string }) { export function MediaButton({ className }: { className?: string }) {
const { ark } = useRouteContext({ strict: false }); const { ark } = useRouteContext({ strict: false });

View File

@@ -1,7 +1,7 @@
import { NsfwIcon } from "@lume/icons"; import { NsfwIcon } from "@lume/icons";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
export function NsfwToggle({ export function NsfwToggle({
nsfw, nsfw,

View File

@@ -1,4 +1,6 @@
import { ComposeFilledIcon, TrashIcon } from "@lume/icons"; import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
import { Spinner, User } from "@lume/ui";
import { MentionNote } from "@lume/ui/src/note/mentions/note";
import { import {
Portal, Portal,
cn, cn,
@@ -9,12 +11,11 @@ import {
sendNativeNotification, sendNativeNotification,
} from "@lume/utils"; } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { nip19 } from "nostr-tools";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MediaButton } from "./-components/media";
import { MentionNote } from "@lume/ui/src/note/mentions/note";
import { import {
Descendant, type Descendant,
Editor, Editor,
Node, Node,
Range, Range,
@@ -22,18 +23,15 @@ import {
createEditor, createEditor,
} from "slate"; } from "slate";
import { import {
ReactEditor,
useSlateStatic,
useSelected,
useFocused,
withReact,
Slate,
Editable, Editable,
ReactEditor,
Slate,
useFocused,
useSelected,
useSlateStatic,
withReact,
} from "slate-react"; } from "slate-react";
import { Contact } from "@lume/types"; import { MediaButton } from "./-components/media";
import { Spinner, User } from "@lume/ui";
import { nip19 } from "nostr-tools";
import { invoke } from "@tauri-apps/api/core";
import { NsfwToggle } from "./-components/nsfw"; import { NsfwToggle } from "./-components/nsfw";
type EditorSearch = { type EditorSearch = {
@@ -49,10 +47,7 @@ export const Route = createFileRoute("/editor/")({
}; };
}, },
beforeLoad: async ({ search }) => { beforeLoad: async ({ search }) => {
const contacts: Contact[] = await invoke("get_contact_metadata");
return { return {
contacts,
initialValue: search.quote initialValue: search.quote
? [ ? [
{ {
@@ -97,11 +92,12 @@ function Screen() {
withMentions(withNostrEvent(withImages(withReact(createEditor())))), withMentions(withNostrEvent(withImages(withReact(createEditor())))),
); );
const filters = contacts const filters =
contacts
?.filter((c) => ?.filter((c) =>
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()), c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
) )
?.slice(0, 5); ?.slice(0, 5) ?? [];
const reset = () => { const reset = () => {
// @ts-expect-error, backlog // @ts-expect-error, backlog
@@ -209,7 +205,7 @@ function Screen() {
<MediaButton className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" /> <MediaButton className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
<button <button
type="button" type="button"
onClick={publish} onClick={() => publish()}
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600" className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
> >
{loading ? ( {loading ? (

View File

@@ -1,71 +0,0 @@
import { useEvent } from "@lume/ark";
import { Box, Container, Note, Spinner, User } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { ReplyList } from "./-components/replyList";
import { WindowVirtualizer } from "virtua";
import { type Event } from "@lume/types";
export const Route = createLazyFileRoute("/events/$eventId")({
component: Event,
});
function Event() {
const { eventId } = Route.useParams();
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner className="size-5 animate-spin" />
</div>
);
}
if (isError) {
<div className="flex h-full w-full items-center justify-center">
<p>Not found.</p>
</div>;
}
return (
<Container withDrag>
<Box className="px-3 pt-3 scrollbar-none">
<WindowVirtualizer>
<MainNote data={data} />
{data ? <ReplyList eventId={eventId} /> : null}
</WindowVirtualizer>
</Box>
</Container>
);
}
function MainNote({ data }: { data: Event }) {
return (
<Note.Provider event={data}>
<Note.Root className="flex flex-col pb-3">
<User.Provider pubkey={data.pubkey}>
<User.Root className="mb-3 flex flex-1 items-center gap-3">
<User.Avatar className="size-11 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<div className="flex flex-1 flex-col">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<User.Time time={data.created_at} />
<span>·</span>
<User.NIP05 />
</div>
</div>
</User.Root>
</User.Provider>
<Note.Thread className="mb-2" />
<Note.Content className="min-w-0" />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Repost />
<Note.Zap />
</div>
<Note.Menu />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -0,0 +1,71 @@
import { useEvent } from "@lume/ark";
import type { Event } from "@lume/types";
import { Box, Container, Note, Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua";
import { ReplyList } from "./-components/replyList";
export const Route = createFileRoute("/events/$eventId")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
return { settings };
},
component: Screen,
});
function Screen() {
const { eventId } = Route.useParams();
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner className="size-5" />
</div>
);
}
if (isError) {
<div className="flex h-full w-full items-center justify-center">
<p>Not found.</p>
</div>;
}
return (
<Container withDrag>
<Box className="scrollbar-none">
<WindowVirtualizer>
<MainNote data={data} />
{data ? (
<ReplyList eventId={eventId} />
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner className="size-5" />
</div>
)}
</WindowVirtualizer>
</Box>
</Container>
);
}
function MainNote({ data }: { data: Event }) {
return (
<Note.Provider event={data}>
<Note.Root>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="mt-4 h-11 gap-2 flex items-center justify-end px-3">
<Note.Reply large />
<Note.Repost large />
<Note.Zap large />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -1,37 +1,26 @@
import { EventWithReplies } from "@lume/types"; import type { EventWithReplies } from "@lume/types";
import { cn } from "@lume/utils";
import { Note, User } from "@lume/ui"; import { Note, User } from "@lume/ui";
import { cn } from "@lume/utils";
import { SubReply } from "./subReply"; import { SubReply } from "./subReply";
export function Reply({ event }: { event: EventWithReplies }) { export function Reply({ event }: { event: EventWithReplies }) {
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root className="border-t border-neutral-100 pt-3 dark:border-neutral-900"> <Note.Root className="border-t border-neutral-100 dark:border-neutral-900">
<User.Provider pubkey={event.pubkey}> <div className="px-3 h-14 flex items-center justify-between">
<User.Root className="mb-2 flex items-center justify-between"> <Note.User />
<div className="inline-flex gap-2"> <Note.Menu />
<User.Avatar className="size-6 rounded-full" />
<div className="inline-flex items-center gap-2">
<User.Name className="font-semibold" />
<User.NIP05 className="text-base lowercase text-neutral-600 dark:text-neutral-400" />
</div> </div>
</div> <Note.ContentLarge className="px-3" />
<User.Time time={event.created_at} /> <div className="mt-3 flex items-center gap-4 px-3 h-14">
</User.Root>
</User.Provider>
<Note.Content />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Reply /> <Note.Reply />
<Note.Repost /> <Note.Repost />
<Note.Zap /> <Note.Zap />
</div> </div>
<Note.Menu />
</div>
<div <div
className={cn( className={cn(
event.replies?.length > 0 event.replies?.length > 0
? "my-3 mt-6 flex flex-col gap-3 divide-y divide-neutral-100 border-l-2 border-neutral-100 pl-6 dark:divide-neutral-900 dark:border-neutral-900" ? "py-2 pl-3 flex flex-col gap-3 divide-y divide-neutral-100 bg-neutral-50 dark:bg-white/5 border-l-2 border-blue-500 dark:divide-neutral-900"
: "", : "",
)} )}
> >

View File

@@ -1,10 +1,10 @@
import type { EventWithReplies } from "@lume/types";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { EventWithReplies } from "@lume/types";
import { Reply } from "./reply"; import { Reply } from "./reply";
import { useRouteContext } from "@tanstack/react-router";
import { Spinner } from "@lume/ui";
export function ReplyList({ export function ReplyList({
eventId, eventId,
@@ -26,13 +26,16 @@ export function ReplyList({
}, [eventId]); }, [eventId]);
return ( return (
<div className={cn("flex flex-col gap-3", className)}> <div className={cn("flex flex-col", className)}>
<div className="h-11 flex px-3 items-center text-sm font-semibold text-neutral-700 dark:text-neutral-300 border-t border-neutral-100 dark:border-neutral-900">
Replies ({data?.length ?? 0})
</div>
{!data ? ( {!data ? (
<div className="mt-4 flex h-16 items-center justify-center p-3"> <div className="flex h-16 items-center justify-center p-3">
<Spinner className="size-5" /> <Spinner className="size-5" />
</div> </div>
) : data.length === 0 ? ( ) : data.length === 0 ? (
<div className="mt-4 flex w-full items-center justify-center"> <div className="flex w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-2 py-6"> <div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3> <h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400"> <p className="leading-none text-neutral-600 dark:text-neutral-400">

View File

@@ -1,31 +1,20 @@
import { Event } from "@lume/types"; import type { Event } from "@lume/types";
import { Note, User } from "@lume/ui"; import { Note } from "@lume/ui";
export function SubReply({ event }: { event: Event; rootEventId?: string }) { export function SubReply({ event }: { event: Event; rootEventId?: string }) {
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root className="pt-3"> <Note.Root>
<User.Provider pubkey={event.pubkey}> <div className="px-3 h-14 flex items-center justify-between">
<User.Root className="mb-2 flex items-center justify-between"> <Note.User />
<div className="inline-flex gap-2"> <Note.Menu />
<User.Avatar className="size-6 rounded-full" />
<div className="inline-flex items-center gap-2">
<User.Name className="font-semibold" />
<User.NIP05 className="text-base lowercase text-neutral-600 dark:text-neutral-400" />
</div> </div>
</div> <Note.ContentLarge className="px-3" />
<User.Time time={event.created_at} /> <div className="mt-3 flex items-center gap-4 px-3">
</User.Root>
</User.Provider>
<Note.Content />
<div className="mt-4 flex items-center justify-between">
<div className="-ml-1 inline-flex items-center gap-4">
<Note.Reply /> <Note.Reply />
<Note.Repost /> <Note.Repost />
<Note.Zap /> <Note.Zap />
</div> </div>
<Note.Menu />
</div>
</Note.Root> </Note.Root>
</Note.Provider> </Note.Provider>
); );

View File

@@ -1,8 +1,8 @@
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { ColumnRouteSearch, Event, Kind } from "@lume/types"; import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { Column, Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute, redirect } from "@tanstack/react-router"; import { Link, createFileRoute, redirect } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
@@ -39,10 +39,16 @@ export const Route = createFileRoute("/foryou")({
}); });
export function Screen() { export function Screen() {
const { label, name, account } = Route.useSearch(); const { name, account } = Route.useSearch();
const { ark, interests } = Route.useRouteContext(); const { ark, interests } = Route.useRouteContext();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = const {
useInfiniteQuery({ data,
isLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [name, account], queryKey: [name, account],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
@@ -72,14 +78,19 @@ export function Screen() {
}; };
return ( return (
<Column.Root> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
<Column.Header label={label} name={name} /> {isFetching && !isLoading && !isFetchingNextPage ? (
<Column.Content> <div className="w-full h-11 flex items-center justify-center">
{isLoading ? ( <div className="flex items-center justify-center gap-2">
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button type="button" className="size-5" disabled>
<Spinner className="size-5" /> <Spinner className="size-5" />
</button> <span className="text-sm font-medium">Fetching new notes...</span>
</div>
</div>
) : null}
{isLoading ? (
<div className="flex h-16 w-full items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<Empty /> <Empty />
@@ -89,12 +100,12 @@ export function Screen() {
</Virtualizer> </Virtualizer>
)} )}
{data?.length && hasNextPage ? ( {data?.length && hasNextPage ? (
<div className="flex h-20 items-center justify-center"> <div>
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage} disabled={isFetchingNextPage || isLoading}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 px-3 font-medium hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
@@ -107,8 +118,7 @@ export function Screen() {
</button> </button>
</div> </div>
) : null} ) : null}
</Column.Content> </div>
</Column.Root>
); );
} }

View File

@@ -1,8 +1,10 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { ColumnRouteSearch, Event, Kind } from "@lume/types"; import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { Column, Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router"; import { Link, createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
@@ -25,10 +27,16 @@ export const Route = createFileRoute("/global")({
}); });
export function Screen() { export function Screen() {
const { label, name, account } = Route.useSearch(); const { account } = Route.useSearch();
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = const {
useInfiniteQuery({ data,
isLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: ["global", account], queryKey: ["global", account],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
@@ -48,20 +56,39 @@ export function Screen() {
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: default: {
return <TextNote key={event.id} event={event} />; const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />;
}
if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
} }
}; };
return ( return (
<Column.Root> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
<Column.Header label={label} name={name} /> {isFetching && !isLoading && !isFetchingNextPage ? (
<Column.Content> <div className="w-full h-11 flex items-center justify-center">
{isLoading ? ( <div className="flex items-center justify-center gap-2">
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button type="button" className="size-5" disabled>
<Spinner className="size-5" /> <Spinner className="size-5" />
</button> <span className="text-sm font-medium">Fetching new notes...</span>
</div>
</div>
) : null}
{isLoading ? (
<div className="flex h-16 w-full items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<Empty /> <Empty />
@@ -71,12 +98,12 @@ export function Screen() {
</Virtualizer> </Virtualizer>
)} )}
{data?.length && hasNextPage ? ( {data?.length && hasNextPage ? (
<div className="flex h-20 items-center justify-center"> <div>
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
@@ -89,8 +116,7 @@ export function Screen() {
</button> </button>
</div> </div>
) : null} ) : null}
</Column.Content> </div>
</Column.Root>
); );
} }

View File

@@ -1,8 +1,8 @@
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { ColumnRouteSearch, Event, Kind } from "@lume/types"; import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { Column, Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute, redirect } from "@tanstack/react-router"; import { Link, createFileRoute, redirect } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
@@ -41,10 +41,16 @@ export const Route = createFileRoute("/group")({
}); });
export function Screen() { export function Screen() {
const { label, name, account } = Route.useSearch(); const { name, account } = Route.useSearch();
const { ark, groups } = Route.useRouteContext(); const { ark, groups } = Route.useRouteContext();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = const {
useInfiniteQuery({ data,
isLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [name, account], queryKey: [name, account],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
@@ -55,7 +61,8 @@ export function Screen() {
const lastEvent = lastPage?.at(-1); const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null; return lastEvent ? lastEvent.created_at - 1 : null;
}, },
select: (data) => data?.pages.flatMap((page) => page), select: (data) =>
data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)),
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
@@ -70,12 +77,19 @@ export function Screen() {
}; };
return ( return (
<Column.Root> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
<Column.Header label={label} name={name} /> {isFetching && !isLoading && !isFetchingNextPage ? (
<Column.Content> <div className="w-full h-11 flex items-center justify-center">
{isLoading ? ( <div className="flex items-center justify-center gap-2">
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<Spinner className="size-5" /> <Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span>
</div>
</div>
) : null}
{isLoading ? (
<div className="flex h-16 w-full items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<Empty /> <Empty />
@@ -84,13 +98,13 @@ export function Screen() {
{data.map((item) => renderItem(item))} {data.map((item) => renderItem(item))}
</Virtualizer> </Virtualizer>
)} )}
<div className="flex h-20 items-center justify-center"> {data?.length && hasNextPage ? (
{hasNextPage ? ( <div>
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} disabled={isFetchingNextPage || isLoading}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 px-3 font-medium hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
@@ -101,10 +115,9 @@ export function Screen() {
</> </>
)} )}
</button> </button>
</div>
) : null} ) : null}
</div> </div>
</Column.Content>
</Column.Root>
); );
} }

View File

@@ -1,7 +1,7 @@
import { PlusIcon } from "@lume/icons"; import { PlusIcon } from "@lume/icons";
import { Spinner, User } from "@lume/ui"; import { Spinner, User } from "@lume/ui";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
@@ -17,7 +17,7 @@ export const Route = createFileRoute("/")({
replace: true, replace: true,
}); });
// Only 1 account, skip account selection screen // Only 1 account, skip account selection screen
case 1: case 1: {
const account = accounts[0].npub; const account = accounts[0].npub;
const loadedAccount = await ark.load_selected_account(account); const loadedAccount = await ark.load_selected_account(account);
@@ -28,6 +28,9 @@ export const Route = createFileRoute("/")({
replace: true, replace: true,
}); });
} }
break;
}
// Account selection // Account selection
default: default:
return { accounts }; return { accounts };
@@ -37,7 +40,7 @@ export const Route = createFileRoute("/")({
}); });
function Screen() { function Screen() {
const navigate = useNavigate(); const navigate = Route.useNavigate();
const context = Route.useRouteContext(); const context = Route.useRouteContext();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -115,6 +118,7 @@ function Screen() {
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw" href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
target="_blank" target="_blank"
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20" className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20"
rel="noreferrer"
> >
Design by NoGood Design by NoGood
</a> </a>

View File

@@ -1,5 +1,4 @@
import { ColumnRouteSearch } from "@lume/types"; import type { ColumnRouteSearch } from "@lume/types";
import { Column } from "@lume/ui";
import { TOPICS, cn } from "@lume/utils"; import { TOPICS, cn } from "@lume/utils";
import { createFileRoute, useRouter } from "@tanstack/react-router"; import { createFileRoute, useRouter } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
@@ -56,10 +55,8 @@ function Screen() {
}; };
return ( return (
<Column.Root> <div className="h-full flex flex-col px-2">
<Column.Header label={label} name={name} /> <div className="shrink-0 flex h-16 items-center justify-between">
<Column.Content>
<div className="sticky left-0 top-0 flex h-16 w-full items-center justify-between border-b border-neutral-100 bg-white px-3 dark:border-neutral-900 dark:bg-black">
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
<h3 className="font-semibold">Interests</h3> <h3 className="font-semibold">Interests</h3>
<p className="text-sm leading-tight text-neutral-700 dark:text-neutral-300"> <p className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
@@ -74,11 +71,13 @@ function Screen() {
{isDone ? t("global.back") : t("global.update")} {isDone ? t("global.back") : t("global.update")}
</button> </button>
</div> </div>
<div className="flex w-full flex-col p-3"> <div className="flex-1 flex flex-col gap-3 pb-2 scrollbar-none overflow-y-auto">
<div className="flex flex-col gap-8">
{TOPICS.map((topic) => ( {TOPICS.map((topic) => (
<div key={topic.title} className="flex flex-col gap-4"> <div
<div className="flex w-full items-center justify-between"> key={topic.title}
className="flex flex-col gap-4 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
>
<div className="px-3 flex w-full items-center justify-between h-14 shrink-0 border-b border-neutral-100 dark:border-neutral-900">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<img <img
src={topic.icon} src={topic.icon}
@@ -95,7 +94,7 @@ function Screen() {
{t("interests.followAll")} {t("interests.followAll")}
</button> </button>
</div> </div>
<div className="flex flex-wrap items-center gap-3"> <div className="px-3 pb-3 flex flex-wrap items-center gap-3">
{topic.content.map((hashtag) => ( {topic.content.map((hashtag) => (
<button <button
key={hashtag} key={hashtag}
@@ -116,7 +115,5 @@ function Screen() {
))} ))}
</div> </div>
</div> </div>
</Column.Content>
</Column.Root>
); );
} }

View File

@@ -20,8 +20,8 @@ function Screen() {
<div className="mx-auto flex w-full max-w-4xl flex-col items-center gap-10"> <div className="mx-auto flex w-full max-w-4xl flex-col items-center gap-10">
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
<img <img
src={`/heading-en.png`} src="/heading-en.png"
srcSet={`/heading-en@2x.png 2x`} srcSet="/heading-en@2x.png 2x"
alt="lume" alt="lume"
className="xl:w-2/3" className="xl:w-2/3"
/> />
@@ -61,7 +61,7 @@ function Screen() {
</div> </div>
</div> </div>
</div> </div>
<div className="flex h-11 items-center justify-center"></div> <div className="flex h-11 items-center justify-center" />
</div> </div>
<div className="absolute z-10 h-full w-full bg-black/5 backdrop-blur-sm" /> <div className="absolute z-10 h-full w-full bg-black/5 backdrop-blur-sm" />
<div className="absolute inset-0 h-full w-full"> <div className="absolute inset-0 h-full w-full">
@@ -75,6 +75,7 @@ function Screen() {
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw" href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
target="_blank" target="_blank"
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white backdrop-blur-md dark:bg-black/20" className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white backdrop-blur-md dark:bg-black/20"
rel="noreferrer"
> >
Design by NoGood Design by NoGood
</a> </a>

View File

@@ -1,8 +1,10 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { ColumnRouteSearch, Event, Kind } from "@lume/types"; import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { Column, Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router"; import { Link, createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
@@ -25,7 +27,7 @@ export const Route = createFileRoute("/newsfeed")({
}); });
export function Screen() { export function Screen() {
const { label, name, account } = Route.useSearch(); const { label, account } = Route.useSearch();
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const { const {
data, data,
@@ -54,17 +56,29 @@ export function Screen() {
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: default: {
return <TextNote key={event.id} event={event} />; const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />;
}
if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
} }
}; };
return ( return (
<Column.Root> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
<Column.Header label={label} name={name} />
<Column.Content>
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<div className="w-full h-16 flex items-center justify-center border-b border-neutral-100 dark:border-neutral-900"> <div className="w-full h-11 flex items-center justify-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Spinner className="size-5" /> <Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span> <span className="text-sm font-medium">Fetching new notes...</span>
@@ -84,12 +98,12 @@ export function Screen() {
</Virtualizer> </Virtualizer>
)} )}
{data?.length && hasNextPage ? ( {data?.length && hasNextPage ? (
<div className="flex h-20 items-center justify-center"> <div>
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
@@ -102,8 +116,7 @@ export function Screen() {
</button> </button>
</div> </div>
) : null} ) : null}
</Column.Content> </div>
</Column.Root>
); );
} }
@@ -125,26 +138,26 @@ function Empty() {
<Link <Link
to="/global" to="/global"
search={search} search={search}
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center justify-between bg-black/10 hover:bg-black/20 text-sm font-medium dark:bg-white/10 dark:hover:bg-white/20 gap-2 rounded-lg px-3"
> >
<ArrowRightIcon className="size-5" />
Show global newsfeed Show global newsfeed
<ArrowRightIcon className="size-5" />
</Link> </Link>
<Link <Link
to="/trending/notes" to="/trending/notes"
search={search} search={search}
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center justify-between bg-black/10 hover:bg-black/20 text-sm font-medium dark:bg-white/10 dark:hover:bg-white/20 gap-2 rounded-lg px-3"
> >
<ArrowRightIcon className="size-5" />
Show trending notes Show trending notes
<ArrowRightIcon className="size-5" />
</Link> </Link>
<Link <Link
to="/trending/users" to="/trending/users"
search={search} search={search}
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center justify-between bg-black/10 hover:bg-black/20 text-sm font-medium dark:bg-white/10 dark:hover:bg-white/20 gap-2 rounded-lg px-3"
> >
<ArrowRightIcon className="size-5" />
Discover trending users Discover trending users
<ArrowRightIcon className="size-5" />
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,5 @@
import { PlusIcon } from "@lume/icons"; import { PlusIcon } from "@lume/icons";
import { LumeColumn } from "@lume/types"; import type { LumeColumn } from "@lume/types";
import { Column } from "@lume/ui";
import { createLazyRoute } from "@tanstack/react-router"; import { createLazyRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/window"; import { getCurrent } from "@tauri-apps/api/window";
@@ -15,8 +14,7 @@ function Screen() {
}; };
return ( return (
<Column.Root shadow={false} background={false}> <div className="relative flex h-full w-full items-center justify-center">
<Column.Content className="relative flex h-full w-full items-center justify-center">
<div className="group absolute left-0 top-0 z-10 h-full w-12"> <div className="group absolute left-0 top-0 z-10 h-full w-12">
<button <button
type="button" type="button"
@@ -45,7 +43,6 @@ function Screen() {
> >
<PlusIcon className="size-8" /> <PlusIcon className="size-8" />
</button> </button>
</Column.Content> </div>
</Column.Root>
); );
} }

View File

@@ -1,8 +1,8 @@
import { SearchIcon } from "@lume/icons"; import { SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types"; import { type Event, Kind } from "@lume/types";
import { Note, Spinner, User } from "@lume/ui"; import { Note, Spinner, User } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
export const Route = createFileRoute("/search")({ export const Route = createFileRoute("/search")({
@@ -41,8 +41,12 @@ function Screen() {
> >
<div <div
data-tauri-drag-region data-tauri-drag-region
className="h-24 shrink-0 flex items-end border-neutral-300 border dark:border-neutral-700" className="relative h-24 shrink-0 flex items-end border-neutral-300 border-b dark:border-neutral-700"
> >
<div
data-tauri-drag-region
className="absolute top-0 left-0 w-full h-4"
/>
<input <input
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
@@ -50,7 +54,7 @@ function Screen() {
if (e.key === "Enter") searchEvents(); if (e.key === "Enter") searchEvents();
}} }}
placeholder="Search anything..." placeholder="Search anything..."
className="w-full h-20 pt-10 px-6 text-lg bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600" className="z-10 w-full h-20 pt-10 px-6 text-lg bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600"
/> />
</div> </div>
<div className="flex-1 p-3 overflow-y-auto scrollbar-none"> <div className="flex-1 p-3 overflow-y-auto scrollbar-none">
@@ -70,9 +74,9 @@ function Screen() {
</div> </div>
<div className="flex-1 flex flex-col gap-3"> <div className="flex-1 flex flex-col gap-3">
{events {events
.filter((ev) => ev.kind === Kind["Metadata"]) .filter((ev) => ev.kind === Kind.Metadata)
.map((event) => ( .map((event) => (
<SearchUser event={event} /> <SearchUser key={event.id} event={event} />
))} ))}
</div> </div>
</div> </div>
@@ -82,9 +86,9 @@ function Screen() {
</div> </div>
<div className="flex-1 flex flex-col gap-3"> <div className="flex-1 flex flex-col gap-3">
{events {events
.filter((ev) => ev.kind === Kind["Text"]) .filter((ev) => ev.kind === Kind.Text)
.map((event) => ( .map((event) => (
<SearchNote event={event} /> <SearchNote key={event.id} event={event} />
))} ))}
</div> </div>
</div> </div>
@@ -132,7 +136,8 @@ function SearchNote({ event }: { event: Event }) {
return ( return (
<div <div
key={event.id} key={event.id}
onClick={() => ark.open_thread(event.id)} onClick={() => ark.open_event(event)}
onKeyDown={() => ark.open_event(event)}
className="p-3 bg-white rounded-lg dark:bg-black" className="p-3 bg-white rounded-lg dark:bg-black"
> >
<Note.Provider event={event}> <Note.Provider event={event}>

View File

@@ -1,4 +1,4 @@
import { SettingsIcon, UserIcon, ZapIcon, SecureIcon } from "@lume/icons"; import { SecureIcon, SettingsIcon, UserIcon, ZapIcon } from "@lume/icons";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router";

View File

@@ -1,4 +1,4 @@
import { type Account } from "@lume/types"; import type { Account } from "@lume/types";
import { User } from "@lume/ui"; import { User } from "@lume/ui";
import { displayNsec } from "@lume/utils"; import { displayNsec } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
@@ -13,7 +13,7 @@ export const Route = createFileRoute("/settings/backup")({
const ark = context.ark; const ark = context.ark;
const npubs = await ark.get_all_accounts(); const npubs = await ark.get_all_accounts();
let accounts: Account[] = []; const accounts: Account[] = [];
for (const account of npubs) { for (const account of npubs) {
const nsec: string = await invoke("get_stored_nsec", { const nsec: string = await invoke("get_stored_nsec", {
@@ -33,14 +33,14 @@ function Screen() {
<div className="mx-auto w-full max-w-xl"> <div className="mx-auto w-full max-w-xl">
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700"> <div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
{accounts.map((account) => ( {accounts.map((account) => (
<Account account={account} /> <NostrAccount key={account.npub} account={account} />
))} ))}
</div> </div>
</div> </div>
); );
} }
function Account({ account }: { account: Account }) { function NostrAccount({ account }: { account: Account }) {
const [key, setKey] = useState(account.nsec); const [key, setKey] = useState(account.nsec);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [passphase, setPassphase] = useState(""); const [passphase, setPassphase] = useState("");
@@ -69,7 +69,7 @@ function Account({ account }: { account: Account }) {
<User.Avatar className="size-8 rounded-full object-cover" /> <User.Avatar className="size-8 rounded-full object-cover" />
<div className="flex flex-col"> <div className="flex flex-col">
<User.Name className="text-sm leading-tight" /> <User.Name className="text-sm leading-tight" />
<User.NIP05 className="text-sm leading-tight text-neutral-700 dark:text-neutral-300" /> <User.NIP05 />
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>
@@ -91,7 +91,7 @@ function Account({ account }: { account: Account }) {
/> />
<button <button
type="button" type="button"
onClick={copyKey} onClick={() => copyKey()}
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700" className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
> >
{copied ? "Copied" : "Copy"} {copied ? "Copied" : "Copy"}
@@ -115,7 +115,7 @@ function Account({ account }: { account: Account }) {
/> />
<button <button
type="button" type="button"
onClick={encrypt} onClick={() => encrypt()}
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700" className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
> >
Update Update

View File

@@ -1,11 +1,11 @@
import { Settings } from "@lume/types"; import type { Settings } from "@lume/types";
import * as Switch from "@radix-ui/react-switch";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { import {
isPermissionGranted, isPermissionGranted,
requestPermission, requestPermission,
} from "@tauri-apps/plugin-notification"; } from "@tauri-apps/plugin-notification";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import * as Switch from "@radix-ui/react-switch";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
export const Route = createFileRoute("/settings/general")({ export const Route = createFileRoute("/settings/general")({

View File

@@ -1,6 +1,6 @@
import { AvatarUploader } from "@/components/avatarUploader"; import { AvatarUploader } from "@/components/avatarUploader";
import { PlusIcon } from "@lume/icons"; import { PlusIcon } from "@lume/icons";
import { Metadata } from "@lume/types"; import type { Metadata } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";

View File

@@ -55,7 +55,7 @@ function Connection() {
/> />
<button <button
type="button" type="button"
onClick={connect} onClick={() => connect()}
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700" className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
> >
Connect Connect

View File

@@ -1,4 +1,4 @@
import { LumeColumn } from "@lume/types"; import type { LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path"; import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window"; import { getCurrent } from "@tauri-apps/api/window";

View File

@@ -1,6 +1,5 @@
import { GlobalIcon, LaurelIcon } from "@lume/icons"; import { GlobalIcon, LaurelIcon } from "@lume/icons";
import { ColumnRouteSearch } from "@lume/types"; import type { ColumnRouteSearch } from "@lume/types";
import { Column } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router";
@@ -17,20 +16,16 @@ export const Route = createFileRoute("/store")({
}); });
function Screen() { function Screen() {
const { label, name } = Route.useSearch();
return ( return (
<Column.Root> <div className="flex flex-col h-full">
<Column.Header label={label} name={name}> <div className="px-3 my-2">
<div className="inline-flex h-full w-full items-center gap-1"> <div className="p-1 shrink-0 inline-flex w-full rounded-lg items-center gap-1 bg-black/10 dark:bg-white/10">
<Link to="/store/official"> <Link to="/store/official" className="flex-1">
{({ isActive }) => ( {({ isActive }) => (
<div <div
className={cn( className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium", "inline-flex h-9 w-full items-center justify-center gap-1.5 rounded-md text-sm font-medium leading-tight",
isActive isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)} )}
> >
<LaurelIcon className="size-4" /> <LaurelIcon className="size-4" />
@@ -38,14 +33,12 @@ function Screen() {
</div> </div>
)} )}
</Link> </Link>
<Link to="/store/community"> <Link to="/store/community" className="flex-1">
{({ isActive }) => ( {({ isActive }) => (
<div <div
className={cn( className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium", "inline-flex h-9 w-full items-center justify-center gap-1.5 rounded-md text-sm font-medium leading-tight",
isActive isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)} )}
> >
<GlobalIcon className="size-4" /> <GlobalIcon className="size-4" />
@@ -54,10 +47,10 @@ function Screen() {
)} )}
</Link> </Link>
</div> </div>
</Column.Header> </div>
<Column.Content> <div className="flex-1 overflow-y-auto scrollbar-none">
<Outlet /> <Outlet />
</Column.Content> </div>
</Column.Root> </div>
); );
} }

View File

@@ -1,11 +1,11 @@
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { Event, Kind } from "@lume/types"; import { type Event, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Await, createFileRoute } from "@tanstack/react-router"; import { Await, createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua";
import { defer } from "@tanstack/react-router"; import { defer } from "@tanstack/react-router";
import { Suspense } from "react"; import { Suspense } from "react";
import { Spinner } from "@lume/ui"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/trending/notes")({ export const Route = createFileRoute("/trending/notes")({
loader: async ({ abortController }) => { loader: async ({ abortController }) => {
@@ -29,16 +29,6 @@ export const Route = createFileRoute("/trending/notes")({
export function Screen() { export function Screen() {
const { data } = Route.useLoaderData(); const { data } = Route.useLoaderData();
const renderItem = (event: Event) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return ( return (
<div className="w-full h-full"> <div className="w-full h-full">
<Virtualizer overscan={3}> <Virtualizer overscan={3}>
@@ -57,7 +47,11 @@ export function Screen() {
} }
> >
<Await promise={data}> <Await promise={data}>
{(notes) => notes.map((event) => renderItem(event))} {(notes) =>
notes.map((event) => (
<TextNote key={event.id} event={event} className="mb-3" />
))
}
</Await> </Await>
</Suspense> </Suspense>
</Virtualizer> </Virtualizer>

View File

@@ -1,6 +1,5 @@
import { ArticleIcon, GroupFeedsIcon } from "@lume/icons"; import { ArticleIcon, GroupFeedsIcon } from "@lume/icons";
import { ColumnRouteSearch } from "@lume/types"; import type { ColumnRouteSearch } from "@lume/types";
import { Column } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
@@ -23,20 +22,16 @@ export const Route = createFileRoute("/trending")({
}); });
export function Screen() { export function Screen() {
const { label, name } = Route.useSearch();
return ( return (
<Column.Root> <div className="flex flex-col h-full">
<Column.Header label={label} name={name}> <div className="h-11 shrink-0 inline-flex w-full items-center gap-1 px-3">
<div className="inline-flex h-full w-full items-center gap-1"> <div className="inline-flex h-full w-full items-center gap-1">
<Link to="/trending/notes"> <Link to="/trending/notes">
{({ isActive }) => ( {({ isActive }) => (
<div <div
className={cn( className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium", "inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)} )}
> >
<ArticleIcon className="size-4" /> <ArticleIcon className="size-4" />
@@ -49,9 +44,7 @@ export function Screen() {
<div <div
className={cn( className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium", "inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)} )}
> >
<GroupFeedsIcon className="size-4" /> <GroupFeedsIcon className="size-4" />
@@ -60,10 +53,10 @@ export function Screen() {
)} )}
</Link> </Link>
</div> </div>
</Column.Header> </div>
<Column.Content> <div className="p-2 flex-1 overflow-y-auto w-full h-full scrollbar-none">
<Outlet /> <Outlet />
</Column.Content> </div>
</Column.Root> </div>
); );
} }

View File

@@ -24,7 +24,7 @@ export function Screen() {
const { data } = Route.useLoaderData(); const { data } = Route.useLoaderData();
return ( return (
<div className="w-full h-full px-3"> <div className="w-full h-full">
<Suspense <Suspense
fallback={ fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1"> <div className="flex h-20 w-full flex-col items-center justify-center gap-1">
@@ -44,7 +44,7 @@ export function Screen() {
users.profiles.map((item) => ( users.profiles.map((item) => (
<div <div
key={item.pubkey} key={item.pubkey}
className="h-max w-full overflow-hidden py-5 border-b border-neutral-100 dark:border-neutral-900" className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"
> >
<User.Provider pubkey={item.pubkey}> <User.Provider pubkey={item.pubkey}>
<User.Root> <User.Root>
@@ -54,7 +54,7 @@ export function Screen() {
<User.Avatar className="size-10 shrink-0 rounded-full object-cover" /> <User.Avatar className="size-10 shrink-0 rounded-full object-cover" />
<User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" /> <User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" />
</div> </div>
<User.Button className="inline-flex h-8 w-20 items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" /> <User.Button className="inline-flex h-8 w-20 items-center justify-center rounded-lg bg-black/10 text-sm font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
</div> </div>
<User.About className="mt-1 line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" /> <User.About className="mt-1 line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" />
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Box, Container, User } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua"; import { WindowVirtualizer } from "virtua";
import { Box, Container, User } from "@lume/ui";
import { EventList } from "./-components/eventList"; import { EventList } from "./-components/eventList";
export const Route = createLazyFileRoute("/users/$pubkey")({ export const Route = createLazyFileRoute("/users/$pubkey")({

View File

@@ -1,11 +1,11 @@
import { TextNote } from "@/components/text";
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; import { ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types"; import { type Event, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils"; import { FETCH_LIMIT } from "@lume/utils";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { Spinner } from "@lume/ui";
export function EventList({ id }: { id: string }) { export function EventList({ id }: { id: string }) {
const { ark } = useRouteContext({ strict: false }); const { ark } = useRouteContext({ strict: false });

View File

@@ -1,11 +1,11 @@
import { Balance } from "@/components/balance"; import { Balance } from "@/components/balance";
import { Box, Container, User } from "@lume/ui"; import { Box, Container, User } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { getCurrent } from "@tauri-apps/api/webviewWindow"; import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { toast } from "sonner"; import { useState } from "react";
import CurrencyInput from "react-currency-input-field"; import CurrencyInput from "react-currency-input-field";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
const DEFAULT_VALUES = [69, 100, 200, 500]; const DEFAULT_VALUES = [69, 100, 200, 500];
@@ -79,6 +79,7 @@ function Screen() {
<div className="inline-flex items-center justify-center gap-2"> <div className="inline-flex items-center justify-center gap-2">
{DEFAULT_VALUES.map((value) => ( {DEFAULT_VALUES.map((value) => (
<button <button
key={value}
type="button" type="button"
onClick={() => setAmount(value)} onClick={() => setAmount(value)}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"

View File

@@ -1,8 +1,8 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import tailwind from "@astrojs/tailwind"; import tailwind from "@astrojs/tailwind";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [tailwind()] integrations: [tailwind()],
}); });

View File

@@ -13,14 +13,14 @@
"@astrojs/check": "^0.5.10", "@astrojs/check": "^0.5.10",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@fontsource/geist-mono": "^5.0.3", "@fontsource/geist-mono": "^5.0.3",
"astro": "^4.6.3", "astro": "^4.7.0",
"astro-seo-meta": "^4.1.0", "astro-seo-meta": "^4.1.1",
"astro-seo-schema": "^4.0.0", "astro-seo-schema": "^4.0.2",
"schema-dts": "^1.1.2", "schema-dts": "^1.1.2",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.5" "typescript": "^5.4.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.12" "@tailwindcss/typography": "^0.5.13"
} }
} }

View File

@@ -2,6 +2,9 @@
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json", "$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true
},
"files": {
"ignore": ["apps/desktop2/src/router.gen.ts"]
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,

View File

@@ -13,7 +13,7 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.7.1", "@biomejs/biome": "^1.7.1",
"@tauri-apps/cli": "2.0.0-beta.12", "@tauri-apps/cli": "2.0.0-beta.12",
"turbo": "^1.13.2" "turbo": "^1.13.3"
}, },
"packageManager": "pnpm@8.9.0", "packageManager": "pnpm@8.9.0",
"engines": { "engines": {

View File

@@ -5,13 +5,13 @@
"main": "./src/index.ts", "main": "./src/index.ts",
"dependencies": { "dependencies": {
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.31.0", "@tanstack/react-query": "^5.32.0",
"react": "^18.2.0" "react": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@types/react": "^18.2.79", "@types/react": "^18.3.1",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }

View File

@@ -1,4 +1,3 @@
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import type { import type {
Account, Account,
Contact, Contact,
@@ -10,10 +9,11 @@ import type {
Metadata, Metadata,
Settings, Settings,
} from "@lume/types"; } from "@lume/types";
import { generateContentTags } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs"; import { readFile } from "@tauri-apps/plugin-fs";
import { generateContentTags } from "@lume/utils";
enum NSTORE_KEYS { enum NSTORE_KEYS {
settings = "lume_user_settings", settings = "lume_user_settings",
@@ -86,7 +86,7 @@ export class Ark {
} }
} }
public async save_account(nsec: string, password: string = "") { public async save_account(nsec: string, password = "") {
try { try {
const cmd: string = await invoke("save_key", { const cmd: string = await invoke("save_key", {
nsec, nsec,
@@ -118,6 +118,7 @@ export class Ark {
const event: Event = JSON.parse(cmd); const event: Event = JSON.parse(cmd);
return event; return event;
} catch (e) { } catch (e) {
console.error(id, String(e));
throw new Error(String(e)); throw new Error(String(e));
} }
} }
@@ -163,7 +164,7 @@ export class Ark {
) { ) {
try { try {
let until: string = undefined; let until: string = undefined;
let isGlobal = global ?? false; const isGlobal = global ?? false;
if (asOf && asOf > 0) until = asOf.toString(); if (asOf && asOf > 0) until = asOf.toString();
@@ -178,17 +179,17 @@ export class Ark {
}); });
for (const event of nostrEvents) { for (const event of nostrEvents) {
const tags = event.tags const eventIds = event.tags
.filter((el) => el[0] === "e") .filter((el) => el[3] === "root" || el[3] === "reply")
?.map((item) => item[1]); ?.map((item) => item[1]);
if (tags.length) { if (eventIds.length) {
for (const tag of tags) { for (const id of eventIds) {
if (seenIds.has(tag)) { if (seenIds.has(id)) {
dedupQueue.add(event.id); dedupQueue.add(event.id);
break; break;
} }
seenIds.add(tag); seenIds.add(id);
} }
} }
} }
@@ -380,14 +381,13 @@ export class Ark {
public parse_event_thread({ public parse_event_thread({
content, content,
tags, tags,
}: { content: string; tags: string[][] }) { }: {
content: string;
tags: string[][];
}) {
let rootEventId: string = null; let rootEventId: string = null;
let replyEventId: string = null; let replyEventId: string = null;
// Ignore quote repost
if (content.includes("nostr:note1") || content.includes("nostr:nevent1"))
return null;
// Get all event references from tags, ignore mention // Get all event references from tags, ignore mention
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention"); const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
@@ -420,7 +420,8 @@ export class Ark {
const cmd: Metadata = await invoke("get_profile", { id }); const cmd: Metadata = await invoke("get_profile", { id });
return cmd; return cmd;
} catch { } catch (e) {
console.error(pubkey, String(e));
return null; return null;
} }
} }
@@ -561,7 +562,6 @@ export class Ark {
} }
public async upload(filePath?: string) { public async upload(filePath?: string) {
try {
const allowExts = [ const allowExts = [
"png", "png",
"jpeg", "jpeg",
@@ -589,8 +589,10 @@ export class Ark {
}) })
).path; ).path;
// User cancelled action
if (!selected) return null; if (!selected) return null;
try {
const file = await readFile(selected); const file = await readFile(selected);
const blob = new Blob([file]); const blob = new Blob([file]);
@@ -640,11 +642,15 @@ export class Ark {
public async get_settings() { public async get_settings() {
try { try {
if (this.settings) return this.settings;
const cmd: string = await invoke("get_nstore", { const cmd: string = await invoke("get_nstore", {
key: NSTORE_KEYS.settings, key: NSTORE_KEYS.settings,
}); });
const settings: Settings = cmd ? JSON.parse(cmd) : null; const settings: Settings = cmd ? JSON.parse(cmd) : null;
this.settings = settings; this.settings = settings;
return settings; return settings;
} catch { } catch {
const defaultSettings: Settings = { const defaultSettings: Settings = {
@@ -729,44 +735,69 @@ export class Ark {
} }
} }
public open_thread(id: string) { public async open_event_id(id: string) {
try { try {
const window = new WebviewWindow(`event-${id}`, { const label = `event-${id}`;
const url = `/events/${id}`;
await invoke("open_window", {
label,
title: "Thread", title: "Thread",
url: `/events/${id}`, url,
minWidth: 500,
minHeight: 800,
width: 500, width: 500,
height: 800, height: 800,
titleBarStyle: "overlay",
center: false,
}); });
this.windows.push(window);
} catch (e) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }
} }
public open_profile(pubkey: string) { public async open_event(event: Event) {
try { try {
const window = new WebviewWindow(`user-${pubkey}`, { let root: string = undefined;
let reply: string = undefined;
const eTags = event.tags.filter(
(tag) => tag[0] === "e" || tag[0] === "q",
);
root = eTags.find((el) => el[3] === "root")?.[1];
reply = eTags.find((el) => el[3] === "reply")?.[1];
if (!root) root = eTags[0]?.[1];
if (!reply) reply = eTags[1]?.[1];
const label = `event-${event.id}`;
const url = `/events/${root ?? reply ?? event.id}`;
await invoke("open_window", {
label,
title: "Thread",
url,
width: 500,
height: 800,
});
} catch (e) {
throw new Error(String(e));
}
}
public async open_profile(pubkey: string) {
try {
const label = `user-${pubkey}`;
await invoke("open_window", {
label,
title: "Profile", title: "Profile",
url: `/users/${pubkey}`, url: `/users/${pubkey}`,
minWidth: 500,
minHeight: 800,
width: 500, width: 500,
height: 800, height: 800,
titleBarStyle: "overlay",
}); });
this.windows.push(window);
} catch (e) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }
} }
public open_editor(reply_to?: string, quote: boolean = false) { public async open_editor(reply_to?: string, quote = false) {
try { try {
let url: string; let url: string;
@@ -776,91 +807,75 @@ export class Ark {
url = "/editor"; url = "/editor";
} }
const window = new WebviewWindow(`editor-${reply_to ? reply_to : 0}`, { const label = `editor-${reply_to ? reply_to : 0}`;
await invoke("open_window", {
label,
title: "Editor", title: "Editor",
url, url,
minWidth: 500, width: 500,
minHeight: 400, height: 360,
width: 600,
height: 400,
hiddenTitle: true,
titleBarStyle: "overlay",
}); });
this.windows.push(window);
} catch (e) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }
} }
public open_nwc() { public async open_nwc() {
try { try {
const window = new WebviewWindow("nwc", { const label = "nwc";
await invoke("open_window", {
label,
title: "Nostr Wallet Connect", title: "Nostr Wallet Connect",
url: "/nwc", url: "/nwc",
minWidth: 400,
minHeight: 600,
width: 400, width: 400,
height: 600, height: 600,
hiddenTitle: true,
titleBarStyle: "overlay",
}); });
this.windows.push(window);
} catch (e) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }
} }
public open_zap(id: string, pubkey: string, account: string) { public async open_zap(id: string, pubkey: string, account: string) {
try { try {
const window = new WebviewWindow(`zap-${id}`, { const label = `zap-${id}`;
await invoke("open_window", {
label,
title: "Zap", title: "Zap",
url: `/zap/${id}?pubkey=${pubkey}&account=${account}`, url: `/zap/${id}?pubkey=${pubkey}&account=${account}`,
minWidth: 400,
minHeight: 500,
width: 400, width: 400,
height: 500, height: 500,
titleBarStyle: "overlay",
}); });
this.windows.push(window);
} catch (e) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }
} }
public open_settings() { public async open_settings() {
try { try {
const window = new WebviewWindow("settings", { const label = "settings";
await invoke("open_window", {
label,
title: "Settings", title: "Settings",
url: "/settings", url: "/settings",
minWidth: 600,
minHeight: 500,
width: 800, width: 800,
height: 500, height: 500,
titleBarStyle: "overlay",
}); });
this.windows.push(window);
} catch (e) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }
} }
public open_search() { public async open_search() {
try { try {
const window = new WebviewWindow("search", { const label = "search";
await invoke("open_window", {
label,
title: "Search", title: "Search",
url: "/search", url: "/search",
width: 750, width: 750,
height: 470, height: 470,
minimizable: false,
resizable: false,
titleBarStyle: "overlay",
}); });
this.windows.push(window);
} catch (e) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }

View File

@@ -1,4 +1,4 @@
import { Event } from "@lume/types"; import type { Event } from "@lume/types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
@@ -21,7 +21,7 @@ export function useEvent(id: string) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
staleTime: Infinity, staleTime: Number.POSITIVE_INFINITY,
retry: 2, retry: 2,
}); });

View File

@@ -16,7 +16,7 @@ export function usePreview(url: string) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
staleTime: Infinity, staleTime: Number.POSITIVE_INFINITY,
retry: 2, retry: 2,
}); });

View File

@@ -1,5 +1,5 @@
import type { Metadata } from "@lume/types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Metadata } from "@lume/types";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
export function useProfile(pubkey: string, embed?: string) { export function useProfile(pubkey: string, embed?: string) {
@@ -27,7 +27,7 @@ export function useProfile(pubkey: string, embed?: string) {
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,
staleTime: Infinity, staleTime: Number.POSITIVE_INFINITY,
retry: 2, retry: 2,
}); });

View File

@@ -124,3 +124,4 @@ export * from "./src/quote";
export * from "./src/key"; export * from "./src/key";
export * from "./src/remote"; export * from "./src/remote";
export * from "./src/nsfw"; export * from "./src/nsfw";
export * from "./src/visit";

View File

@@ -4,11 +4,11 @@
"private": true, "private": true,
"main": "./index.ts", "main": "./index.ts",
"dependencies": { "dependencies": {
"react": "^18.2.0" "react": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tsconfig": "workspace:*", "@lume/tsconfig": "workspace:*",
"@types/react": "^18.2.79", "@types/react": "^18.3.1",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function AddWidgetIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function AddWidgetIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,7 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function AdvancedSettingsIcon( export function AdvancedSettingsIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement> props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function AlbyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function AlbyIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
export function AnnouncementIcon(props: JSX.IntrinsicElements['svg']) { export function AnnouncementIcon(props: JSX.IntrinsicElements["svg"]) {
return ( return (
<svg <svg
{...props} {...props}

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ArrowDownIcon( export function ArrowDownIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,7 +1,7 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function ArrowRightCircleIcon( export function ArrowRightCircleIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement> props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ArrowUpIcon( export function ArrowUpIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ArrowUpSquareIcon( export function ArrowUpSquareIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ArticleIcon( export function ArticleIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function BellIcon( export function BellIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function BellFilledIcon( export function BellFilledIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function BoldIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function BoldIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ChatsIcon( export function ChatsIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function CheckIcon( export function CheckIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function CheckCircleIcon( export function CheckCircleIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ChevronDownIcon( export function ChevronDownIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,7 +1,7 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function ChevronRightIcon( export function ChevronRightIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement> props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ChevronUpIcon( export function ChevronUpIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function CommandIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function CommandIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function CommunityIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function CommunityIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ComposeIcon( export function ComposeIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ComposeFilledIcon( export function ComposeFilledIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function CopyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function CopyIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function DarkIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function DarkIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function DotsPattern(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function DotsPattern(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg {...props}> <svg {...props}>
<pattern <pattern
@@ -14,7 +16,13 @@ export function DotsPattern(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElem
> >
<circle cx="2" cy="2" r="1.626" fill="currentColor"></circle> <circle cx="2" cy="2" r="1.626" fill="currentColor"></circle>
</pattern> </pattern>
<rect width="100%" height="100%" x="0" y="0" fill="url(#pattern-circles)"></rect> <rect
width="100%"
height="100%"
x="0"
y="0"
fill="url(#pattern-circles)"
></rect>
</svg> </svg>
); );
} }

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function DownloadIcon( export function DownloadIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function EditIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function EditIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function EditInterestIcon( export function EditInterestIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function EmptyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function EmptyIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -50,7 +52,10 @@ export function EmptyIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElemen
> >
<feFlood floodOpacity="0" result="BackgroundImageFix" /> <feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur result="effect1_foregroundBlur_110_63" stdDeviation="5.5" /> <feGaussianBlur
result="effect1_foregroundBlur_110_63"
stdDeviation="5.5"
/>
</filter> </filter>
<clipPath id="clip0_110_63"> <clipPath id="clip0_110_63">
<path fill="#fff" d="M0 0H120V120H0z" /> <path fill="#fff" d="M0 0H120V120H0z" />

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function EnterIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function EnterIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"; import type { SVGProps } from "react";
export function ExpandIcon( export function ExpandIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function ExploreIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function ExploreIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function Explore2Icon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function Explore2Icon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function EyeOffIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function EyeOffIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function EyeOnIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function EyeOnIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function FeedIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function FeedIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
width={24} width={24}

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function FileIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function FileIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function FocusIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function FocusIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import type { SVGProps } from "react";
export function FollowIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function FollowIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

Some files were not shown because too many files have changed in this diff Show More