This commit is contained in:
reya 2024-10-05 08:49:09 +07:00
parent 8398ae80d3
commit d841163ba7
28 changed files with 527 additions and 461 deletions

View File

@ -37,7 +37,6 @@
"@tauri-apps/plugin-upload": "^2.0.0", "@tauri-apps/plugin-upload": "^2.0.0",
"@tauri-apps/plugin-window-state": "^2.0.0", "@tauri-apps/plugin-window-state": "^2.0.0",
"bitcoin-units": "^1.0.0", "bitcoin-units": "^1.0.0",
"boring-avatars": "^1.11.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"embla-carousel-react": "^8.3.0", "embla-carousel-react": "^8.3.0",
"i18next": "^23.15.1", "i18next": "^23.15.1",

8
pnpm-lock.yaml generated
View File

@ -89,9 +89,6 @@ importers:
bitcoin-units: bitcoin-units:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
boring-avatars:
specifier: ^1.11.2
version: 1.11.2
dayjs: dayjs:
specifier: ^1.11.13 specifier: ^1.11.13
version: 1.11.13 version: 1.11.13
@ -1504,9 +1501,6 @@ packages:
bitcoin-units@1.0.0: bitcoin-units@1.0.0:
resolution: {integrity: sha512-brac+Ttz7ovf/8D0jQHSWHnN2hmdjxDRBStxhjO752URLJlQIFpfZxzUteSZ81UYnRNiMkvsW9WsYPDuxHfnYA==} resolution: {integrity: sha512-brac+Ttz7ovf/8D0jQHSWHnN2hmdjxDRBStxhjO752URLJlQIFpfZxzUteSZ81UYnRNiMkvsW9WsYPDuxHfnYA==}
boring-avatars@1.11.2:
resolution: {integrity: sha512-3+wkwPeObwS4R37FGXMYViqc4iTrIRj5yzfX9Qy4mnpZ26sX41dGMhsAgmKks1r/uufY1pl4vpgzMWHYfJRb2A==}
brace-expansion@2.0.1: brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
@ -3556,8 +3550,6 @@ snapshots:
dependencies: dependencies:
big.js: 6.2.2 big.js: 6.2.2
boring-avatars@1.11.2: {}
brace-expansion@2.0.1: brace-expansion@2.0.1:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@ -290,13 +290,7 @@ pub async fn get_group_events(
let authors: Vec<PublicKey> = public_keys let authors: Vec<PublicKey> = public_keys
.iter() .iter()
.map(|p| { .map(|p| PublicKey::from_str(p).map_err(|err| err.to_string()))
if p.starts_with("npub1") {
PublicKey::from_bech32(p).map_err(|err| err.to_string())
} else {
PublicKey::from_hex(p).map_err(|err| err.to_string())
}
})
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let filter = Filter::new() let filter = Filter::new()
@ -340,6 +334,7 @@ pub async fn get_global_events(
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> { ) -> Result<Vec<RichEvent>, String> {
let client = &state.client; let client = &state.client;
let as_of = match until { let as_of = match until {
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(), None => Timestamp::now(),

View File

@ -1,6 +1,6 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import type { LumeColumn } from "@/types"; import type { LumeColumn } from "@/types";
import { Check, DotsThree } from "@phosphor-icons/react"; import { CaretDown, Check } from "@phosphor-icons/react";
import { useParams } from "@tanstack/react-router"; import { useParams } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
@ -86,10 +86,10 @@ export const Column = memo(function Column({ column }: { column: LumeColumn }) {
}, [params.account]); }, [params.account]);
return ( return (
<div className="h-full w-[440px] shrink-0 p-2"> <div className="h-full w-[440px] shrink-0 border-r border-black/5 dark:border-white/5">
<div className="flex flex-col w-full h-full rounded-xl bg-black/5 dark:bg-white/20"> <div className="flex flex-col gap-px size-full">
<Header label={column.label} name={column.name} /> <Header label={column.label} name={column.name} />
<div ref={container} className="flex-1 w-full h-full"> <div ref={container} className="flex-1 size-full">
{!isCreated ? ( {!isCreated ? (
<div className="size-full flex items-center justify-center"> <div className="size-full flex items-center justify-center">
<Spinner /> <Spinner />
@ -102,7 +102,7 @@ export const Column = memo(function Column({ column }: { column: LumeColumn }) {
}); });
function Header({ label, name }: { label: string; name: string }) { function Header({ label, name }: { label: string; name: string }) {
const [title, setTitle] = useState(name); const [title, setTitle] = useState("");
const [isChanged, setIsChanged] = useState(false); const [isChanged, setIsChanged] = useState(false);
const saveNewTitle = async () => { const saveNewTitle = async () => {
@ -173,19 +173,18 @@ function Header({ label, name }: { label: string; name: string }) {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (title.length !== name.length) setIsChanged(true); if (title.length > 0) setIsChanged(true);
}, [title]); }, [title.length]);
return ( return (
<div className="flex items-center justify-between w-full px-1 h-9 shrink-0"> <div className="group flex items-center justify-center gap-2 w-full h-9 shrink-0">
<div className="size-7" />
<div className="flex items-center justify-center shrink-0 h-7"> <div className="flex items-center justify-center shrink-0 h-7">
<div className="relative flex items-center gap-2"> <div className="relative flex items-center gap-2">
<div <div
contentEditable contentEditable
suppressContentEditableWarning={true} suppressContentEditableWarning={true}
onBlur={(e) => setTitle(e.currentTarget.textContent)} onBlur={(e) => setTitle(e.currentTarget.textContent)}
className="text-sm font-medium focus:outline-none" className="text-[12px] font-semibold focus:outline-none"
> >
{name} {name}
</div> </div>
@ -193,9 +192,9 @@ function Header({ label, name }: { label: string; name: string }) {
<button <button
type="button" type="button"
onClick={() => saveNewTitle()} onClick={() => saveNewTitle()}
className="text-teal-500 hover:text-teal-600" className="text-teal-500 hover:text-teal-600 inline-flex items-center justify-center size-6 border-[.5px] border-neutral-200 dark:border-neutral-800 shadow shadow-neutral-200/50 dark:shadow-none rounded-full bg-white dark:bg-black"
> >
<Check className="size-4" /> <Check className="size-3" weight="bold" />
</button> </button>
) : null} ) : null}
</div> </div>
@ -203,9 +202,9 @@ function Header({ label, name }: { label: string; name: string }) {
<button <button
type="button" type="button"
onClick={(e) => showContextMenu(e)} onClick={(e) => showContextMenu(e)}
className="inline-flex items-center justify-center rounded-lg size-7 hover:bg-black/10 dark:hover:bg-white/10" className="hidden shrink-0 group-hover:inline-flex items-center justify-center size-6 border-[.5px] border-neutral-200 dark:border-neutral-800 shadow shadow-neutral-200/50 dark:shadow-none rounded-full bg-white dark:bg-black"
> >
<DotsThree className="size-5" /> <CaretDown className="size-3" weight="bold" />
</button> </button>
</div> </div>
); );

View File

@ -94,7 +94,6 @@ export function NoteContent({
<div <div
className={cn( className={cn(
"select-text text-pretty content-break overflow-hidden", "select-text text-pretty content-break overflow-hidden",
event.content.length > 500 ? "max-h-[250px] gradient-mask-b-0" : "",
className, className,
)} )}
> >

View File

@ -13,12 +13,7 @@ export const Quote = memo(function Quote({
}) { }) {
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root className={cn("", className)}>
className={cn(
"bg-white dark:bg-black/20 rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<Note.Child event={event.quote} isRoot /> <Note.Child event={event.quote} isRoot />
<div className="flex items-center gap-2 px-3"> <div className="flex items-center gap-2 px-3">

View File

@ -15,12 +15,7 @@ export const RepostNote = memo(function RepostNote({
const { isLoading, isError, data } = useEvent(event.repostId); const { isLoading, isError, data } = useEvent(event.repostId);
return ( return (
<Note.Root <Note.Root className={cn("", className)}>
className={cn(
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-20 gap-2"> <div className="flex items-center justify-center h-20 gap-2">
<Spinner /> <Spinner />

View File

@ -12,12 +12,7 @@ export const TextNote = memo(function TextNote({
}) { }) {
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root className={cn("", className)}>
className={cn(
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5",
className,
)}
>
<div className="flex items-center justify-between px-3 h-14"> <div className="flex items-center justify-between px-3 h-14">
<Note.User /> <Note.User />
<Note.Menu /> <Note.Menu />

View File

@ -1,9 +1,8 @@
import { appSettings, cn } from "@/commons"; import { cn } from "@/commons";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { LumeWindow } from "@/system"; import { LumeWindow } from "@/system";
import { CaretDown, Feather, MagnifyingGlass } from "@phosphor-icons/react"; import { CaretDown, Feather, MagnifyingGlass } from "@phosphor-icons/react";
import { Outlet, createLazyFileRoute } from "@tanstack/react-router"; import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
import { useStore } from "@tanstack/react-store";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { memo, useCallback } from "react"; import { memo, useCallback } from "react";
@ -14,7 +13,6 @@ export const Route = createLazyFileRoute("/$account/_app")({
function Screen() { function Screen() {
const context = Route.useRouteContext(); const context = Route.useRouteContext();
const transparent = useStore(appSettings, (state) => state.transparent);
return ( return (
<div className="flex flex-col w-screen h-screen"> <div className="flex flex-col w-screen h-screen">
@ -54,14 +52,7 @@ function Screen() {
className="relative z-[200] flex-1 flex items-center justify-end gap-1" className="relative z-[200] flex-1 flex items-center justify-end gap-1"
/> />
</div> </div>
<div <div className="flex-1 bg-neutral-100 dark:bg-neutral-900 border-t-[.5px] border-black/20 dark:border-white/20">
className={cn(
"flex-1",
transparent
? ""
: "bg-white dark:bg-black border-t border-black/20 dark:border-white/20",
)}
>
<Outlet /> <Outlet />
</div> </div>
</div> </div>

View File

@ -179,8 +179,8 @@ function Screen() {
<Column key={column.label} column={column} /> <Column key={column.label} column={column} />
)) ))
)} )}
<div className="shrink-0 p-2 h-full w-[450px]"> <div className="shrink-0 p-2 h-full w-[440px]">
<div className="size-full bg-black/5 dark:bg-white/15 rounded-xl flex items-center justify-center"> <div className="size-full flex items-center justify-center">
<button <button
type="button" type="button"
onClick={() => LumeWindow.openColumnsGallery()} onClick={() => LumeWindow.openColumnsGallery()}

View File

@ -49,6 +49,7 @@ function Screen() {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: [search.label, search.account], queryKey: [search.label, search.account],
}); });
// @ts-ignore, tanstack router bug.
navigate({ to: search.redirect, search: { ...search, name: title } }); navigate({ to: search.redirect, search: { ...search, name: title } });
} else { } else {
await message(res.error, { await message(res.error, {
@ -69,10 +70,10 @@ function Screen() {
</p> </p>
</div> </div>
<div className="flex flex-col w-4/5 max-w-full gap-3"> <div className="flex flex-col w-4/5 max-w-full gap-3">
<div className="flex items-center w-full rounded-lg h-9 shrink-0 bg-black/5 dark:bg-white/5"> <div className="flex items-center w-full rounded-lg h-9 shrink-0 bg-neutral-200 dark:bg-neutral-800">
<label <label
htmlFor="name" htmlFor="name"
className="w-16 text-sm font-semibold text-center border-r border-black/10 dark:border-white/10 shrink-0" className="w-16 text-sm font-semibold text-center border-r border-neutral-300 dark:border-neutral-700 shrink-0"
> >
Name Name
</label> </label>
@ -85,19 +86,19 @@ function Screen() {
/> />
</div> </div>
<div className="flex flex-col items-center w-full gap-3"> <div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 rounded-xl"> <div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-neutral-200 dark:bg-neutral-900 rounded-xl">
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
name="npub" name="npub"
value={npub} value={npub}
onChange={(e) => setNpub(e.target.value)} onChange={(e) => setNpub(e.target.value)}
placeholder="npub1..." placeholder="npub1..."
className="w-full px-3 text-sm border-none rounded-lg h-9 bg-black/10 dark:bg-white/10 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400" className="w-full px-3 text-sm border-none rounded-lg h-9 bg-neutral-300 dark:bg-neutral-700 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/> />
<button <button
type="button" type="button"
onClick={() => addUser()} onClick={() => addUser()}
className="inline-flex items-center justify-center text-white rounded-lg size-9 bg-black/20 dark:bg-white/20 shrink-0 hover:bg-blue-500" className="inline-flex items-center justify-center text-neutral-500 rounded-lg size-9 bg-neutral-300 dark:bg-neutral-700 shrink-0 hover:bg-blue-500 hover:text-white"
> >
<Plus className="size-5" /> <Plus className="size-5" />
</button> </button>
@ -125,7 +126,7 @@ function Screen() {
</button> </button>
)) ))
) : ( ) : (
<div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14"> <div className="flex items-center justify-center text-sm rounded-lg bg-neutral-300 dark:bg-neutral-700 h-14">
Empty. Empty.
</div> </div>
)} )}

View File

@ -29,9 +29,12 @@ function Screen() {
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full flex-1" className="overflow-hidden size-full px-3"
> >
<ScrollArea.Viewport ref={ref} className="h-full pt-1 px-3 pb-3"> <ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref}> <Virtualizer scrollRef={ref}>
<RootEvent /> <RootEvent />
<ReplyList /> <ReplyList />
@ -54,28 +57,24 @@ function RootEvent() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="bg-white flex items-center justify-center h-32 dark:bg-black/10 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"> <div className="flex items-center gap-2 text-sm">
<div className="flex items-center gap-2 text-sm"> <Spinner />
<Spinner /> Loading...
Loading...
</div>
</div> </div>
); );
} }
if (isError) { if (isError) {
return ( return (
<div className="bg-white flex items-center justify-center h-32 dark:bg-black/10 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"> <div className="flex items-center gap-2 text-sm text-red-500">
<div className="flex items-center gap-2 text-sm text-red-500"> {error.message}
{error.message}
</div>
</div> </div>
); );
} }
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root className="bg-white dark:bg-white/10 rounded-xl shadow-primary dark:shadow-none"> <Note.Root className="border-b-[.5px] border-neutral-300 dark:border-neutral-700">
<div className="flex items-center justify-between px-3 h-14"> <div className="flex items-center justify-between px-3 h-14">
<Note.User /> <Note.User />
<Note.Menu /> <Note.Menu />
@ -217,7 +216,7 @@ function ReplyList() {
}, []); }, []);
return ( return (
<div> <div className="px-3">
<div className="flex items-center text-sm font-semibold h-14 text-neutral-600 dark:text-white/30"> <div className="flex items-center text-sm font-semibold h-14 text-neutral-600 dark:text-white/30">
All replies All replies
</div> </div>

View File

@ -2,7 +2,6 @@ import type { LumeColumn } from "@/types";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import Avatar from "boring-avatars";
export const Route = createLazyFileRoute("/columns/_layout/gallery")({ export const Route = createLazyFileRoute("/columns/_layout/gallery")({
component: Screen, component: Screen,
@ -26,31 +25,14 @@ function Screen() {
{columns.map((column) => ( {columns.map((column) => (
<div <div
key={column.label} key={column.label}
className="mb-3 group flex px-2 items-center justify-between h-16 rounded-xl bg-white dark:bg-white/20 shadow-sm shadow-neutral-500/10" className="mb-3 group flex px-4 items-center justify-between h-16 rounded-xl bg-white dark:bg-black border-[.5px] border-neutral-300 dark:border-neutral-700"
> >
<div className="inline-flex items-center gap-2"> <div className="text-sm">
<div className="size-11 bg-neutral-200 rounded-lg overflow-hidden"> <div className="mb-px leading-tight font-semibold">
<Avatar {column.name}
name={column.name}
size={44}
square={true}
variant="pixel"
colors={[
"#84cc16",
"#22c55e",
"#0ea5e9",
"#3b82f6",
"#6366f1",
]}
/>
</div> </div>
<div className="text-sm"> <div className="leading-tight text-neutral-500 dark:text-neutral-400">
<div className="mb-px leading-tight font-semibold"> {column.description}
{column.name}
</div>
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
{column.description}
</div>
</div> </div>
</div> </div>
<button <button

View File

@ -3,7 +3,7 @@ import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { Quote, RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system"; import type { LumeEvent } from "@/system";
import { Kind } from "@/types"; import { Kind } from "@/types";
import { ArrowCircleRight } from "@phosphor-icons/react"; import { ArrowDown } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
@ -48,12 +48,30 @@ export function Screen() {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />; return (
<RepostNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
default: { default: {
if (event.isQuote) { if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return (
<Quote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} else { } else {
return <TextNote key={event.id} event={event} className="mb-3" />; return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} }
} }
} }
@ -65,9 +83,12 @@ export function Screen() {
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full" className="overflow-hidden size-full px-3"
> >
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3"> <ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref}> <Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3"> <div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
@ -79,11 +100,11 @@ export function Screen() {
) : null} ) : null}
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center w-full h-16 gap-2"> <div className="flex items-center justify-center w-full h-16 gap-2">
<Spinner className="size-5" /> <Spinner className="size-4" />
<span className="text-sm font-medium">Loading...</span> <span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5"> <div className="mb-3 flex items-center justify-center h-20 text-sm">
🎉 Yo. You're catching up on all latest notes. 🎉 Yo. You're catching up on all latest notes.
</div> </div>
) : ( ) : (
@ -95,13 +116,13 @@ export function Screen() {
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-4" />
) : ( ) : (
<> <>
<ArrowCircleRight className="size-5" /> <ArrowDown className="size-4" />
Load more Load more
</> </>
)} )}

View File

@ -3,7 +3,7 @@ import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { Quote, RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system"; import type { LumeEvent } from "@/system";
import { Kind } from "@/types"; import { Kind } from "@/types";
import { ArrowCircleRight } from "@phosphor-icons/react"; import { ArrowDown } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
@ -49,12 +49,30 @@ export function Screen() {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />; return (
<RepostNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
default: { default: {
if (event.isQuote) { if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return (
<Quote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} else { } else {
return <TextNote key={event.id} event={event} className="mb-3" />; return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} }
} }
} }
@ -66,9 +84,12 @@ export function Screen() {
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full" className="overflow-hidden size-full px-3"
> >
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3"> <ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref}> <Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3"> <div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
@ -80,11 +101,11 @@ export function Screen() {
) : null} ) : null}
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center w-full h-16 gap-2"> <div className="flex items-center justify-center w-full h-16 gap-2">
<Spinner className="size-5" /> <Spinner className="size-4" />
<span className="text-sm font-medium">Loading...</span> <span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5"> <div className="mb-3 flex items-center justify-center h-20 text-sm">
🎉 Yo. You're catching up on all latest notes. 🎉 Yo. You're catching up on all latest notes.
</div> </div>
) : ( ) : (
@ -96,13 +117,13 @@ export function Screen() {
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-4" />
) : ( ) : (
<> <>
<ArrowCircleRight className="size-5" /> <ArrowDown className="size-4" />
Load more Load more
</> </>
)} )}

View File

@ -3,7 +3,7 @@ import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { Quote, RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system"; import type { LumeEvent } from "@/system";
import { Kind } from "@/types"; import { Kind } from "@/types";
import { ArrowCircleRight } from "@phosphor-icons/react"; import { ArrowDown } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
@ -50,12 +50,30 @@ export function Screen() {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />; return (
<RepostNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
default: { default: {
if (event.isQuote) { if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return (
<Quote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} else { } else {
return <TextNote key={event.id} event={event} className="mb-3" />; return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} }
} }
} }
@ -67,9 +85,12 @@ export function Screen() {
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full" className="overflow-hidden size-full px-3"
> >
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3"> <ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref}> <Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3"> <div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
@ -81,11 +102,11 @@ export function Screen() {
) : null} ) : null}
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center w-full h-16 gap-2"> <div className="flex items-center justify-center w-full h-16 gap-2">
<Spinner className="size-5" /> <Spinner className="size-4" />
<span className="text-sm font-medium">Loading...</span> <span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5"> <div className="mb-3 flex items-center justify-center h-20 text-sm">
🎉 Yo. You're catching up on all latest notes. 🎉 Yo. You're catching up on all latest notes.
</div> </div>
) : ( ) : (
@ -97,13 +118,13 @@ export function Screen() {
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-4" />
) : ( ) : (
<> <>
<ArrowCircleRight className="size-5" /> <ArrowDown className="size-4" />
Load more Load more
</> </>
)} )}

View File

@ -3,7 +3,7 @@ import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { Quote, RepostNote, Spinner, TextNote } from "@/components";
import { LumeEvent } from "@/system"; import { LumeEvent } from "@/system";
import { Kind, type Meta } from "@/types"; import { Kind, type Meta } from "@/types";
import { ArrowCircleRight, ArrowUp } from "@phosphor-icons/react"; import { ArrowDown, ArrowUp } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query"; import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
import { import {
@ -68,12 +68,30 @@ export function Screen() {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />; return (
<RepostNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
default: { default: {
if (event.isQuote) { if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return (
<Quote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} else { } else {
return <TextNote key={event.id} event={event} className="mb-3" />; return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} }
} }
} }
@ -95,6 +113,7 @@ export function Screen() {
return ( return (
<Navigate <Navigate
to="/columns/create-newsfeed/users" to="/columns/create-newsfeed/users"
// @ts-ignore, tanstack router bug.
search={{ label, account, redirect: location.href }} search={{ label, account, redirect: location.href }}
/> />
); );
@ -104,10 +123,13 @@ export function Screen() {
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full" className="overflow-hidden size-full px-3"
> >
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3"> <ScrollArea.Viewport
<Listerner /> ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Listener />
<Virtualizer scrollRef={ref}> <Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3"> <div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
@ -119,11 +141,11 @@ export function Screen() {
) : null} ) : null}
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center w-full h-16 gap-2"> <div className="flex items-center justify-center w-full h-16 gap-2">
<Spinner className="size-5" /> <Spinner className="size-4" />
<span className="text-sm font-medium">Loading...</span> <span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5"> <div className="mb-3 flex items-center justify-center h-20 text-sm">
🎉 Yo. You're catching up on all latest notes. 🎉 Yo. You're catching up on all latest notes.
</div> </div>
) : ( ) : (
@ -135,13 +157,13 @@ export function Screen() {
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-4" />
) : ( ) : (
<> <>
<ArrowCircleRight className="size-5" /> <ArrowDown className="size-4" />
Load more Load more
</> </>
)} )}
@ -161,7 +183,7 @@ export function Screen() {
); );
} }
const Listerner = memo(function Listerner() { const Listener = memo(function Listerner() {
const { queryClient } = Route.useRouteContext(); const { queryClient } = Route.useRouteContext();
const { label, account } = Route.useSearch(); const { label, account } = Route.useSearch();

View File

@ -1,9 +1,9 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import { decodeZapInvoice, formatCreatedAt } from "@/commons"; import { decodeZapInvoice, formatCreatedAt } from "@/commons";
import { Note, Spinner, User } from "@/components"; import { Note, RepostIcon, Spinner, User } from "@/components";
import { LumeEvent, LumeWindow, useEvent } from "@/system"; import { LumeEvent, LumeWindow, useEvent } from "@/system";
import { Kind, type NostrEvent } from "@/types"; import { Kind, type NostrEvent } from "@/types";
import { Info, Repeat } from "@phosphor-icons/react"; import { Info } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import * as Tabs from "@radix-ui/react-tabs"; import * as Tabs from "@radix-ui/react-tabs";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@ -99,110 +99,114 @@ function Screen() {
} }
return ( return (
<Tabs.Root defaultValue="replies" className="flex flex-col h-full"> <div className="px-3 h-full overflow-y-auto">
<Tabs.List className="h-8 shrink-0 flex items-center"> <Tabs.Root
<Tabs.Trigger defaultValue="replies"
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50" className="flex flex-col h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
value="replies"
>
Replies
</Tabs.Trigger>
<Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
value="reactions"
>
Reactions
</Tabs.Trigger>
<Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
value="zaps"
>
Zaps
</Tabs.Trigger>
</Tabs.List>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="min-h-0 flex-1 overflow-x-hidden"
> >
<Tab value="replies"> <Tabs.List className="h-11 shrink-0 flex items-center">
{data.texts.map((event, index) => ( <Tabs.Trigger
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation> className="flex-1 inline-flex h-11 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-neutral-100 dark:border-neutral-900 data-[state=active]:border-neutral-200 dark:data-[state=active]:border-neutral-800 data-[state=inactive]:opacity-50"
<TextNote key={event.id + index} event={event} /> value="replies"
))} >
</Tab> Replies
<Tab value="reactions"> </Tabs.Trigger>
{[...data.reactions.entries()].map(([root, events]) => ( <Tabs.Trigger
<div className="flex-1 inline-flex h-11 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-neutral-100 dark:border-neutral-900 data-[state=active]:border-neutral-200 dark:data-[state=active]:border-neutral-800 data-[state=inactive]:opacity-50"
key={root} value="reactions"
className="flex flex-col gap-1 p-3 mb-3 bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5" >
> Reactions
<div className="flex flex-col flex-1 min-w-0 gap-2"> </Tabs.Trigger>
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5"> <Tabs.Trigger
<RootNote id={root} /> className="flex-1 inline-flex h-11 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-neutral-100 dark:border-neutral-900 data-[state=active]:border-neutral-200 dark:data-[state=active]:border-neutral-800 data-[state=inactive]:opacity-50"
</div> value="zaps"
<div className="flex flex-wrap items-center gap-3"> >
{events.map((event) => ( Zaps
<User.Provider key={event.id} pubkey={event.pubkey}> </Tabs.Trigger>
<User.Root className="shrink-0 flex rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]"> </Tabs.List>
<User.Avatar className="flex-1 rounded-full size-6" /> <ScrollArea.Root
<div className="inline-flex items-center justify-center flex-1 text-xs truncate rounded-full size-7"> type={"scroll"}
{event.kind === Kind.Reaction ? ( scrollHideDelay={300}
event.content === "+" ? ( className="min-h-0 flex-1 overflow-x-hidden"
"👍"
) : (
event.content
)
) : (
<Repeat className="text-teal-400 size-4 dark:text-teal-600" />
)}
</div>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tab>
<Tab value="zaps">
{[...data.zaps.entries()].map(([root, events]) => (
<div
key={root}
className="flex flex-col gap-1 p-3 mb-3 bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"
>
<div className="flex flex-col flex-1 min-w-0 gap-2">
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5">
<RootNote id={root} />
</div>
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider
key={event.id}
pubkey={event.tags.find((tag) => tag[0] === "P")[1]}
>
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
<User.Avatar className="rounded-full size-6" />
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
{decodeZapInvoice(event.tags).bitcoinFormatted}
</div>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tab>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
> >
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" /> <Tab value="replies">
</ScrollArea.Scrollbar> {data.texts.map((event, index) => (
<ScrollArea.Corner className="bg-transparent" /> <TextNote key={event.id + index} event={event} />
</ScrollArea.Root> ))}
</Tabs.Root> </Tab>
<Tab value="reactions">
{[...data.reactions.entries()].map(([root, events]) => (
<div
key={root}
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
>
<div className="flex flex-col flex-1 min-w-0 gap-2">
<div className="flex items-center gap-2 pb-2">
<RootNote id={root} />
</div>
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider key={event.id} pubkey={event.pubkey}>
<User.Root className="shrink-0 flex rounded-full h-7 bg-neutral-100 dark:bg-neutral-900 p-[2px]">
<User.Avatar className="flex-1 rounded-full size-6" />
<div className="inline-flex items-center justify-center flex-1 text-xs truncate rounded-full size-6">
{event.kind === Kind.Reaction ? (
event.content === "+" ? (
"👍"
) : (
event.content
)
) : (
<RepostIcon className="text-teal-400 size-4 dark:text-teal-600" />
)}
</div>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tab>
<Tab value="zaps">
{[...data.zaps.entries()].map(([root, events]) => (
<div
key={root}
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
>
<div className="flex flex-col flex-1 min-w-0 gap-2">
<div className="flex items-center gap-2 pb-2">
<RootNote id={root} />
</div>
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider
key={event.id}
pubkey={event.tags.find((tag) => tag[0] === "P")[1]}
>
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
<User.Avatar className="rounded-full size-6" />
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
{decodeZapInvoice(event.tags).bitcoinFormatted}
</div>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tab>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</Tabs.Root>
</div>
); );
} }
@ -211,7 +215,7 @@ function Tab({ value, children }: { value: string; children: ReactNode[] }) {
return ( return (
<Tabs.Content value={value} className="size-full"> <Tabs.Content value={value} className="size-full">
<ScrollArea.Viewport ref={ref} className="h-full p-3"> <ScrollArea.Viewport ref={ref} className="h-full">
<Virtualizer scrollRef={ref}>{children}</Virtualizer> <Virtualizer scrollRef={ref}>{children}</Virtualizer>
</ScrollArea.Viewport> </ScrollArea.Viewport>
</Tabs.Content> </Tabs.Content>
@ -267,10 +271,10 @@ function TextNote({ event }: { event: LumeEvent }) {
<button <button
type="button" type="button"
onClick={() => LumeWindow.openEvent(event)} onClick={() => LumeWindow.openEvent(event)}
className="w-full rounded-xl hover:ring-1 ring-blue-500 mb-3" className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
> >
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root className="flex flex-col p-3 rounded-xl bg-white dark:bg-black/20 shadow-primary dark:ring-1 dark:ring-white/5"> <Note.Root className="flex flex-col">
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<User.Root className="inline-flex items-center gap-2"> <User.Root className="inline-flex items-center gap-2">
<User.Avatar className="rounded-full size-9" /> <User.Avatar className="rounded-full size-9" />

View File

@ -1,3 +1,4 @@
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/columns/_layout/onboarding")({ export const Route = createLazyFileRoute("/columns/_layout/onboarding")({
@ -6,107 +7,97 @@ export const Route = createLazyFileRoute("/columns/_layout/onboarding")({
function Screen() { function Screen() {
return ( return (
<div className="h-full flex flex-col py-6 gap-6 overflow-y-auto scrollbar-none"> <ScrollArea.Root
<div className="text-center flex flex-col items-center justify-center"> type={"scroll"}
<h1 className="text-2xl font-serif font-medium">Welcome to Lume</h1> scrollHideDelay={300}
<p className="leading-tight text-neutral-700 dark:text-neutral-300"> className="overflow-hidden size-full"
Here are a few suggestions to help you get started. >
</p> <ScrollArea.Viewport className="relative h-full px-3 pb-3">
</div> <div className="my-10 text-center flex flex-col items-center justify-center">
<div className="px-3 flex flex-col gap-3"> <h1 className="text-2xl font-serif font-medium">Welcome to Lume</h1>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10"> <p className="leading-tight text-neutral-700 dark:text-neutral-300">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400"> Here are a few suggestions to help you get started.
01. </p>
</div>
<div className="flex flex-col gap-3">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
01.
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Navigate between columns.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full rounded-lg shadow-md"
controls
muted
preload="none"
poster="https://image.nostr.build/143354665d94b20013fde14ad05e53e958e11eec568a11b273921d1808c410cc.png"
>
<source
src="https://video.nostr.build/8fc3598ef85a1be292cee4ad6ad85b2c8bbf86da0aefd693e60416b56ec96e5f.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div> </div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text"> <div className="relative flex flex-col items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800">
Navigate between columns. <div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
02.
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Add a new column.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full rounded-lg shadow-md"
controls
muted
preload="none"
poster="https://image.nostr.build/216015bc81931725c23171700b4d2d73556ecfe3efe662afd7eea6627574b506.png"
>
<source
src="https://video.nostr.build/cdb842a1ffc6864bab009a668f68acb0a672e04e9538dbe7e2ac44182722d956.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div> </div>
<div className="flex-1 w-3/4 h-full pb-10"> <div className="relative flex flex-col items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800">
<video <div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
className="h-auto w-full aspect-square rounded-lg shadow-md transform" 03.
controls </div>
muted <div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
preload="none" View a thread.
poster="/poster_1.jpeg" </div>
> <div className="flex-1 w-3/4 h-full pb-10">
<source <video
src="https://video.nostr.build/692f71e2be47ecfc29edcbdaa198cc5979bfb9c900f05d78682895dd546d8d4f.mp4" className="h-auto w-full rounded-lg shadow-md"
type="video/mp4" controls
/> muted
Your browser does not support the video tag. preload="none"
</video> poster="https://image.nostr.build/3a3ead93bc64224e6397df4097998708b5aef4c6e7104f8e28c77db47ad1625a.png"
>
<source
src="https://video.nostr.build/088636b03976a4e54c5053c718300816a7c3f9f361a1fe7d8e7a0f663ab6a582.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div> </div>
</div> </div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10"> </ScrollArea.Viewport>
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400"> <ScrollArea.Scrollbar
02. className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
</div> orientation="vertical"
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text"> >
Switch between accounts. <ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</div> </ScrollArea.Scrollbar>
<div className="flex-1 w-3/4 h-full pb-10"> <ScrollArea.Corner className="bg-transparent" />
<video </ScrollArea.Root>
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls
muted
preload="none"
poster="/poster_2.jpeg"
>
<source
src="https://video.nostr.build/d33962520506d86acfb4b55a7b265821e10ae637f5ec830a173b7e6092b16ec8.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
03.
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Open Lume Store.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls
muted
preload="none"
poster="/poster_3.jpeg"
>
<source
src="https://video.nostr.build/927abbfde2097e470ac751181b1db456b7e4b9149550408efff1a966a7ffb9a8.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
04.
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Use the Tray Menu.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full rounded-lg shadow-md transform"
controls
muted
preload="none"
poster="/poster_4.jpeg"
>
<source
src="https://video.nostr.build/513de4824b6abaf7e9698c1dad2f68096574356848c0c200bc8cb8074df29410.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
); );
} }

View File

@ -19,37 +19,41 @@ function Screen() {
const { events } = useRouterState({ select: (s) => s.location.state }); const { events } = useRouterState({ select: (s) => s.location.state });
return ( return (
<div className="h-full flex flex-col"> <div className="px-3 h-full">
<div className="h-10 shrink-0 border-b border-black/5 dark:border-white/5 flex items-center justify-between px-2"> <div className="size-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700">
<button <div className="h-full flex flex-col">
type="button" <div className="h-10 shrink-0 border-b border-black/5 dark:border-white/5 flex items-center justify-between px-2">
onClick={() => router.history.back()} <button
className="inline-flex items-center justify-center gap-1.5 h-7 w-max px-1 text-sm font-semibold hover:bg-black/10 dark:hover:bg-white/10 rounded-md" type="button"
> onClick={() => router.history.back()}
<ArrowLeft className="size-4" /> className="inline-flex items-center justify-center gap-1.5 h-7 w-max px-1 text-sm font-semibold hover:bg-black/10 dark:hover:bg-white/10 rounded-md"
Back >
</button> <ArrowLeft className="size-4" />
Back
</button>
</div>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
>
<ScrollArea.Viewport ref={ref} className="h-full p-3">
<Virtualizer scrollRef={ref}>
{events.map((event) => (
<ReplyNote key={event.id} event={event} />
))}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
</div> </div>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
>
<ScrollArea.Viewport ref={ref} className="h-full p-3">
<Virtualizer scrollRef={ref}>
{events.map((event) => (
<ReplyNote key={event.id} event={event} />
))}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div> </div>
); );
} }

View File

@ -84,59 +84,61 @@ function Screen() {
}; };
return ( return (
<div className="flex flex-col gap-3 size-full overflow-hidden"> <div className="px-3 h-full">
<div className="h-9 shrink-0 px-3 flex items-center gap-2"> <div className="flex flex-col gap-3 size-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700">
<input <div className="h-9 shrink-0 pt-6 px-3 flex items-center gap-2">
name="search" <input
placeholder="Search nostr ..." name="search"
value={query} placeholder="Search nostr ..."
onChange={(e) => setQuery(e.target.value)} value={query}
onKeyDown={(event) => { onChange={(e) => setQuery(e.target.value)}
if (event.key === "Enter") search(); onKeyDown={(event) => {
}} if (event.key === "Enter") search();
className="h-9 px-5 flex-1 rounded-full border-none bg-black/5 dark:bg-white/10 placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:bg-black/10 dark:focus:bg-white/10 focus:outline-none focus:ring-0" }}
/> className="h-9 px-5 flex-1 rounded-full border-none bg-black/5 dark:bg-white/10 placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:bg-black/10 dark:focus:bg-white/10 focus:outline-none focus:ring-0"
<button />
type="button" <button
disabled={!query.length || isPending} type="button"
className="size-9 shrink-0 inline-flex items-center justify-center rounded-full bg-black/5 dark:bg-white/10" disabled={!query.length || isPending}
> className="size-9 shrink-0 inline-flex items-center justify-center rounded-full bg-black/5 dark:bg-white/10"
{isPending ? ( >
<Spinner className="size-4" />
) : (
<MagnifyingGlass className="size-4" />
)}
</button>
</div>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
>
<ScrollArea.Viewport ref={ref} className="relative h-full px-3">
<Virtualizer scrollRef={ref}>
{isPending ? ( {isPending ? (
<div className="w-full h-[200px] flex gap-2 items-center justify-center"> <Spinner className="size-4" />
<Spinner />
Searching...
</div>
) : !events.length ? (
<div className="w-full h-[200px] flex gap-2 items-center justify-center">
Type somethings to search.
</div>
) : ( ) : (
events.map((event) => renderItem(event)) <MagnifyingGlass className="size-4" />
)} )}
</Virtualizer> </button>
</ScrollArea.Viewport> </div>
<ScrollArea.Scrollbar <ScrollArea.Root
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2" type={"scroll"}
orientation="vertical" scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
> >
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" /> <ScrollArea.Viewport ref={ref} className="relative h-full px-3">
</ScrollArea.Scrollbar> <Virtualizer scrollRef={ref}>
<ScrollArea.Corner className="bg-transparent" /> {isPending ? (
</ScrollArea.Root> <div className="w-full h-[200px] flex gap-2 items-center justify-center">
<Spinner />
Searching...
</div>
) : !events.length ? (
<div className="w-full h-[200px] flex gap-2 items-center justify-center">
Type somethings to search.
</div>
) : (
events.map((event) => renderItem(event))
)}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
</div> </div>
); );
} }

View File

@ -65,7 +65,7 @@ function StoryItem({ contact }: { contact: string }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
return ( return (
<div className="mb-3 flex flex-col w-full h-[300px] bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"> <div className="mb-3 flex flex-col w-full h-[300px] bg-white dark:bg-black rounded-xl border-[.5px] border-neutral-300 dark:border-neutral-700">
<div className="h-12 shrink-0 px-2 flex items-center justify-between border-b border-neutral-100 dark:border-white/5"> <div className="h-12 shrink-0 px-2 flex items-center justify-between border-b border-neutral-100 dark:border-white/5">
<User.Provider pubkey={contact}> <User.Provider pubkey={contact}>
<User.Root className="inline-flex items-center gap-2"> <User.Root className="inline-flex items-center gap-2">

View File

@ -12,7 +12,7 @@ export const Route = createLazyFileRoute("/columns/_layout/trending")({
}); });
function Screen() { function Screen() {
const { isLoading, isError, data } = useQuery({ const { isLoading, data } = useQuery({
queryKey: ["trending-notes"], queryKey: ["trending-notes"],
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
const res = await fetch("https://api.nostr.band/v0/trending/notes", { const res = await fetch("https://api.nostr.band/v0/trending/notes", {
@ -43,12 +43,30 @@ function Screen() {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />; return (
<RepostNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
default: { default: {
if (event.isQuote) { if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return (
<Quote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} else { } else {
return <TextNote key={event.id} event={event} className="mb-3" />; return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} }
} }
} }
@ -60,22 +78,21 @@ function Screen() {
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full" className="overflow-hidden size-full px-3"
> >
<ScrollArea.Viewport ref={ref} className="h-full px-3"> <ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref} overscan={1}> <Virtualizer scrollRef={ref} overscan={1}>
{isLoading ? ( {isLoading ? (
<div className="flex flex-col items-center justify-center w-full h-20 gap-1"> <div className="flex items-center justify-center w-full h-16 gap-2">
<div className="inline-flex items-center gap-2 text-sm font-medium"> <Spinner className="size-4" />
<Spinner className="size-5" /> <span className="text-sm font-medium">Loading...</span>
Loading...
</div>
</div> </div>
) : isError ? ( ) : !data.length ? (
<div className="flex flex-col items-center justify-center w-full h-20 gap-1"> <div className="mb-3 flex items-center justify-center h-20 text-sm">
<div className="inline-flex items-center gap-2 text-sm font-medium"> 🎉 Yo. You're catching up on all latest notes.
Error.
</div>
</div> </div>
) : ( ) : (
data.map((item) => renderItem(item)) data.map((item) => renderItem(item))

View File

@ -22,7 +22,7 @@ function Screen() {
const { isLoading, data: events } = useQuery({ const { isLoading, data: events } = useQuery({
queryKey: ["stories", id], queryKey: ["stories", id],
queryFn: async () => { queryFn: async () => {
const res = await commands.getEventsBy(id, 20); const res = await commands.getEventsBy(id, 100);
if (res.status === "ok") { if (res.status === "ok") {
const data = toLumeEvents(res.data); const data = toLumeEvents(res.data);
@ -41,12 +41,30 @@ function Screen() {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />; return (
<RepostNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
default: { default: {
if (event.isQuote) { if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return (
<Quote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} }
return <TextNote key={event.id} event={event} className="mb-3" />; return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
} }
} }
}, },
@ -57,21 +75,24 @@ function Screen() {
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full" className="overflow-hidden size-full px-3"
> >
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3"> <ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref} overscan={0}> <Virtualizer scrollRef={ref} overscan={0}>
<User.Provider pubkey={id}> <User.Provider pubkey={id}>
<User.Root className="relative"> <User.Root className="relative">
<User.Cover className="object-cover w-full h-44 rounded-t-lg gradient-mask-b-0" /> <User.Cover className="object-cover w-full h-44 rounded-t-lg gradient-mask-b-0" />
<User.Button className="z-10 absolute top-4 right-4 inline-flex items-center justify-center w-20 text-xs font-medium text-white shadow-md bg-black hover:bg-black/80 rounded-full h-7" /> <User.Button className="z-10 absolute top-4 right-4 inline-flex items-center justify-center w-20 text-xs font-medium text-white shadow-md bg-black hover:bg-black/80 rounded-full h-7" />
<div className="z-10 relative flex flex-col items-center gap-1.5 -mt-16"> <div className="z-10 relative flex flex-col gap-1.5 -mt-16 px-4">
<User.Avatar className="rounded-full size-14" /> <User.Avatar className="rounded-full size-14" />
<div className="flex items-center gap-1"> <div className="flex gap-1">
<User.Name className="text-lg font-semibold leading-tight" /> <User.Name className="text-lg font-semibold leading-tight" />
<User.NIP05 /> <User.NIP05 />
</div> </div>
<User.About className="text-center" /> <User.About className="text-sm" />
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>