Event Subscriptions (#218)
* feat: improve create column command * refactor: thread * feat: add window virtualized to event screen * chore: update deps * fix: window decoration * feat: improve mention ntoe * feat: add subscription to event screen
This commit is contained in:
@@ -8,30 +8,22 @@ import { Link } from "@tanstack/react-router";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/$account")({
|
||||
beforeLoad: async () => {
|
||||
const accounts = await NostrAccount.getAccounts();
|
||||
const os = await type();
|
||||
|
||||
return { accounts, os };
|
||||
return { accounts };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { os } = Route.useRouteContext();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-screen h-screen">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={cn(
|
||||
"flex h-11 shrink-0 items-center justify-between pr-2",
|
||||
os === "macos" ? "ml-2 pl-20" : "pl-4",
|
||||
)}
|
||||
className="flex h-11 shrink-0 items-center justify-between pr-2 ml-2 pl-20"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Accounts />
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Spinner } from "@lume/ui";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||
import type { OsType } from "@tauri-apps/plugin-os";
|
||||
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient;
|
||||
platform: OsType;
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Note } from "@/components/note";
|
||||
import { type LumeEvent, NostrQuery } from "@lume/system";
|
||||
import { Box, Container, Spinner } from "@lume/ui";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
import { Reply } from "./-components/reply";
|
||||
|
||||
export const Route = createFileRoute("/events/$eventId")({
|
||||
beforeLoad: async () => {
|
||||
const settings = await NostrQuery.getUserSettings();
|
||||
return { settings };
|
||||
},
|
||||
loader: async ({ params }) => {
|
||||
const event = await NostrQuery.getEvent(params.eventId);
|
||||
return event;
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const event = Route.useLoaderData();
|
||||
|
||||
const [reload, setReload] = useState(false);
|
||||
const [replies, setReplies] = useState<LumeEvent[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
if (event) {
|
||||
event.getAllReplies().then((data) => {
|
||||
if (mounted) setReplies(data);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [event]);
|
||||
|
||||
return (
|
||||
<Container withDrag>
|
||||
<Box className="scrollbar-none">
|
||||
<WindowVirtualizer>
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root>
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.User />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.ContentLarge className="px-3" />
|
||||
<div className="flex items-center justify-end gap-2 px-3 mt-4 h-11">
|
||||
<Note.Reply large />
|
||||
<Note.Repost large />
|
||||
<Note.Zap large />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center px-3 text-sm font-semibold border-t h-11 text-neutral-700 dark:text-neutral-300 border-neutral-100 dark:border-neutral-900">
|
||||
Replies ({replies?.length ?? 0})
|
||||
</div>
|
||||
{!replies ? (
|
||||
<Spinner />
|
||||
) : !replies.length ? (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
||||
<h3 className="text-3xl">👋</h3>
|
||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Be the first to Reply!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
replies.map((event) => <Reply key={event.id} event={event} />)
|
||||
)}
|
||||
</div>
|
||||
</WindowVirtualizer>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
143
apps/desktop2/src/routes/events/$id.tsx
Normal file
143
apps/desktop2/src/routes/events/$id.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Note } from "@/components/note";
|
||||
import { LumeEvent, NostrQuery } from "@lume/system";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Virtualizer } from "virtua";
|
||||
import NoteParent from "./-components/parent";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import type { Meta } from "@lume/types";
|
||||
|
||||
type Payload = {
|
||||
raw: string;
|
||||
parsed: Meta;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/events/$id")({
|
||||
beforeLoad: async () => {
|
||||
const settings = await NostrQuery.getUserSettings();
|
||||
return { settings };
|
||||
},
|
||||
loader: async ({ params }) => {
|
||||
const event = await NostrQuery.getEvent(params.id);
|
||||
return event;
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="shrink-0 h-8 w-full border-b border-black/5 dark:border-white/5"
|
||||
/>
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
scrollHideDelay={300}
|
||||
className="overflow-hidden size-full flex-1"
|
||||
>
|
||||
<ScrollArea.Viewport ref={ref} className="h-full p-3">
|
||||
<RootEvent />
|
||||
<Virtualizer scrollRef={ref}>
|
||||
<ReplyList />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function RootEvent() {
|
||||
const event = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root className="bg-white dark:bg-black/10 backdrop-blur rounded-xl shadow-primary dark:ring-1 dark:ring-white/5">
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.User />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.ContentLarge className="px-3" />
|
||||
<div className="flex items-center gap-2 px-3 mt-6 h-12 rounded-b-xl bg-neutral-50 dark:bg-white/5">
|
||||
<Note.Reply large />
|
||||
<Note.Repost large />
|
||||
<Note.Zap large />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function ReplyList() {
|
||||
const event = Route.useLoaderData();
|
||||
const [replies, setReplies] = useState<LumeEvent[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenEvent = getCurrent().listen<Payload>("new_reply", (data) => {
|
||||
const event = LumeEvent.from(data.payload.raw, data.payload.parsed);
|
||||
setReplies((prev) => [event, ...prev]);
|
||||
});
|
||||
|
||||
const unlistenWindow = getCurrent().onCloseRequested(async () => {
|
||||
await event.unlistenEventReply();
|
||||
await getCurrent().destroy();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlistenEvent.then((f) => f());
|
||||
unlistenWindow.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
async function getReplies() {
|
||||
const data = await event.getEventReplies();
|
||||
|
||||
if (mounted) {
|
||||
setReplies(data);
|
||||
// Start listen for new reply
|
||||
event.listenEventReply();
|
||||
}
|
||||
}
|
||||
|
||||
getReplies();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center text-sm font-semibold h-14 text-neutral-600 dark:text-white/30">
|
||||
All replies
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{!replies.length ? (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-4">
|
||||
<h3 className="text-3xl">👋</h3>
|
||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Be the first to Reply!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
replies.map((event) => <NoteParent key={event.id} event={event} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
apps/desktop2/src/routes/events/-components/child.tsx
Normal file
41
apps/desktop2/src/routes/events/-components/child.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Note } from "@/components/note";
|
||||
import type { LumeEvent } from "@lume/system";
|
||||
import NoteParent from "./parent";
|
||||
import { memo } from "react";
|
||||
|
||||
const NoteChild = memo(function NoteChild({ event }: { event: LumeEvent }) {
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root className="flex flex-col gap-6 mb-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Note.User />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-8 shrink-0" />
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<Note.ContentLarge />
|
||||
<div className="flex items-center gap-1">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{event.replies?.length ? (
|
||||
<div className="flex flex-col gap-3 pl-4">
|
||||
<div className="flex flex-col pl-6 border-l border-black/10 dark:border-white/10">
|
||||
{event.replies?.map((childEvent) => (
|
||||
<NoteParent key={childEvent.id} event={childEvent} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export default NoteChild;
|
||||
41
apps/desktop2/src/routes/events/-components/parent.tsx
Normal file
41
apps/desktop2/src/routes/events/-components/parent.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Note } from "@/components/note";
|
||||
import type { LumeEvent } from "@lume/system";
|
||||
import NoteChild from "./child";
|
||||
import { memo } from "react";
|
||||
|
||||
const NoteParent = memo(function NoteParent({ event }: { event: LumeEvent }) {
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root className="flex flex-col gap-6 mb-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Note.User />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-8 shrink-0" />
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<Note.ContentLarge />
|
||||
<div className="flex items-center gap-1">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{event.replies?.length ? (
|
||||
<div className="flex flex-col gap-3 pl-4">
|
||||
<div className="flex flex-col gap-3 pl-6 border-l border-black/10 dark:border-white/10">
|
||||
{event.replies?.map((childEvent) => (
|
||||
<NoteChild key={childEvent.id} event={childEvent} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export default NoteParent;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Note } from "@/components/note";
|
||||
import type { LumeEvent } from "@lume/system";
|
||||
import { cn } from "@lume/utils";
|
||||
import { SubReply } from "./subReply";
|
||||
|
||||
export function Reply({ event }: { event: LumeEvent }) {
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root className="border-t border-neutral-100 dark:border-neutral-900">
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.User />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.ContentLarge className="px-3" />
|
||||
<div className="flex items-center gap-4 px-3 mt-3 h-14">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
event.replies?.length > 0
|
||||
? "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"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{event.replies?.length > 0
|
||||
? event.replies?.map((childEvent) => (
|
||||
<SubReply key={childEvent.id} event={childEvent} />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { NostrEvent } from "@lume/types";
|
||||
import { Note } from "@/components/note";
|
||||
|
||||
export function SubReply({
|
||||
event,
|
||||
}: {
|
||||
event: NostrEvent;
|
||||
rootEventId?: string;
|
||||
}) {
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<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-3 flex items-center gap-4 px-3">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
@@ -67,8 +67,12 @@ function Screen() {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex flex-col items-center justify-between w-full h-full"
|
||||
className="relative flex flex-col items-center justify-between w-full h-full"
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="absolute top-0 left-0 h-14 w-full"
|
||||
/>
|
||||
<div className="flex items-end justify-center flex-1 w-full px-4 pb-10">
|
||||
<div className="text-center">
|
||||
<h2 className="mb-1 text-lg text-neutral-700 dark:text-neutral-300">
|
||||
|
||||
Reference in New Issue
Block a user