feat: Add support for NIP-51 (#236)

* feat: and follow and interest sets

* feat: improve query

* feat: improve
This commit is contained in:
雨宮蓮
2024-10-07 14:33:20 +07:00
committed by GitHub
parent d841163ba7
commit 090a815f99
37 changed files with 1500 additions and 765 deletions
@@ -1,186 +0,0 @@
import { commands } from "@/commands.gen";
import { Spinner } from "@/components";
import { User } from "@/components/user";
import { Plus, X } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useState, useTransition } from "react";
export const Route = createLazyFileRoute("/columns/_layout/create-group")({
component: Screen,
});
const REYA_NPUB =
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445";
function Screen() {
const contacts = Route.useLoaderData();
const search = Route.useSearch();
const navigate = Route.useNavigate();
const { queryClient } = Route.useRouteContext();
const [title, setTitle] = useState("");
const [npub, setNpub] = useState("");
const [users, setUsers] = useState<string[]>([REYA_NPUB]);
const [isPending, startTransition] = useTransition();
const toggleUser = (pubkey: string) => {
setUsers((prev) =>
prev.includes(pubkey)
? prev.filter((i) => i !== pubkey)
: [...prev, pubkey],
);
};
const addUser = () => {
if (!npub.startsWith("npub1")) return;
if (users.includes(npub)) return;
setUsers((prev) => [...prev, npub]);
setNpub("");
};
const submit = () => {
startTransition(async () => {
const key = `lume_v4:group:${search.label}`;
const res = await commands.setLumeStore(key, JSON.stringify(users));
if (res.status === "ok") {
await queryClient.invalidateQueries({
queryKey: [search.label, search.account],
});
// @ts-ignore, tanstack router bug.
navigate({ to: search.redirect, search: { ...search, name: title } });
} else {
await message(res.error, {
title: "Create Group",
kind: "error",
});
return;
}
});
};
return (
<div className="flex flex-col items-center justify-center w-full h-full gap-4">
<div className="flex flex-col items-center justify-center text-center">
<h1 className="font-serif text-2xl font-medium">Create a group</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
For the people that you want to keep up.
</p>
</div>
<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-neutral-200 dark:bg-neutral-800">
<label
htmlFor="name"
className="w-16 text-sm font-semibold text-center border-r border-neutral-300 dark:border-neutral-700 shrink-0"
>
Name
</label>
<input
name="name"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter a name for this group"
className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
</div>
<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-neutral-200 dark:bg-neutral-900 rounded-xl">
<div className="flex gap-2">
<input
name="npub"
value={npub}
onChange={(e) => setNpub(e.target.value)}
placeholder="npub1..."
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
type="button"
onClick={() => addUser()}
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" />
</button>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold">Added</span>
<div className="flex flex-col gap-2">
{users.length ? (
users.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="rounded-full size-8" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
<X className="size-4" />
</button>
))
) : (
<div className="flex items-center justify-center text-sm rounded-lg bg-neutral-300 dark:bg-neutral-700 h-14">
Empty.
</div>
)}
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold">Contacts</span>
<div className="flex flex-col gap-2">
{contacts.length ? (
contacts.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="rounded-full size-8" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
</button>
))
) : (
<div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
<p>
Find more user at{" "}
<a
href="https://www.nostr.directory/"
target="_blank"
className="text-blue-600 after:content-['_↗']"
rel="noreferrer"
>
Nostr Directory
</a>
</p>
</div>
)}
</div>
</div>
</div>
<button
type="button"
onClick={() => submit()}
disabled={isPending || users.length < 1}
className="inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-full w-36 h-9 hover:bg-blue-600 disabled:opacity-50"
>
{isPending ? <Spinner /> : "Confirm"}
</button>
</div>
</div>
</div>
);
}
+286 -32
View File
@@ -1,49 +1,30 @@
import type { LumeColumn } from "@/types";
import { commands } from "@/commands.gen";
import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system";
import type { LumeColumn, NostrEvent } from "@/types";
import { ArrowClockwise, Plus } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { useCallback } from "react";
export const Route = createLazyFileRoute("/columns/_layout/gallery")({
component: Screen,
});
function Screen() {
const { columns } = Route.useRouteContext();
const install = async (column: LumeColumn) => {
const mainWindow = getCurrentWindow();
await mainWindow.emit("columns", { type: "add", column });
};
return (
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport className="relative h-full px-3">
{columns.map((column) => (
<div
key={column.label}
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="text-sm">
<div className="mb-px leading-tight font-semibold">
{column.name}
</div>
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
{column.description}
</div>
</div>
<button
type="button"
onClick={() => install(column)}
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
>
Open
</button>
</div>
))}
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
<MyGroups />
<MyInterests />
<Core />
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
@@ -55,3 +36,276 @@ function Screen() {
</ScrollArea.Root>
);
}
function Core() {
const { isLoading, data } = useQuery({
queryKey: ["core"],
queryFn: async () => {
const systemPath = "resources/columns.json";
const resourcePath = await resolveResource(systemPath);
const resourceFile = await readTextFile(resourcePath);
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
const columns = systemColumns.filter((col) => !col.default);
return columns;
},
refetchOnWindowFocus: false,
});
return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between px-2">
<h3 className="font-semibold">Core</h3>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : (
data.map((column) => (
<div
key={column.label}
className="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="text-sm">
<div className="mb-px leading-tight font-semibold">
{column.name}
</div>
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
{column.description}
</div>
</div>
<button
type="button"
onClick={() => LumeWindow.openColumn(column)}
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
>
Open
</button>
</div>
))
)}
</div>
</div>
);
}
function MyGroups() {
const { account } = Route.useSearch();
const { isLoading, data, refetch } = useQuery({
queryKey: ["mygroups", account],
queryFn: async () => {
const res = await commands.getAllGroups();
if (res.status === "ok") {
const data = res.data.map((item) => JSON.parse(item) as NostrEvent);
return data;
} else {
throw new Error(res.error);
}
},
select: (data) =>
data.filter(
(item) => item.tags.filter((tag) => tag[0] === "p")?.length > 0,
),
refetchOnWindowFocus: false,
});
const renderItem = useCallback(
(item: NostrEvent) => {
const name = item.tags.filter((tag) => tag[0] === "d")[0][1] ?? "unnamed";
return (
<div
key={item.id}
className="group flex flex-col rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50 border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<div className="p-3 h-16 flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
{item.tags
.filter((tag) => tag[0] === "p")
.map((tag) => (
<div key={tag[1]}>
<User.Provider pubkey={tag[1]}>
<User.Root>
<User.Avatar className="size-8 rounded-full" />
</User.Root>
</User.Provider>
</div>
))}
</div>
<div className="p-3 flex items-center justify-between">
<div className="text-sm font-medium">{name}</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
label: name,
name,
url: `/columns/groups/${item.id}`,
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-blue-600 hover:bg-blue-500 text-white"
>
Add
</button>
</div>
</div>
</div>
);
},
[data],
);
return (
<div className="mb-12 flex flex-col gap-3">
<div className="flex items-center justify-between px-2">
<h3 className="font-semibold">My groups</h3>
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => refetch()}
className="size-7 inline-flex items-center justify-center rounded-full"
>
<ArrowClockwise className="size-4" />
</button>
<button
type="button"
onClick={() =>
LumeWindow.openPopup("New group", `/set-group?account=${account}`)
}
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
<Plus className="size-3" weight="bold" />
New
</button>
</div>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : !data.length ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">You don't have any groups yet.</p>
</div>
) : (
data.map((item) => renderItem(item))
)}
</div>
</div>
);
}
function MyInterests() {
const { account } = Route.useSearch();
const { isLoading, data, refetch } = useQuery({
queryKey: ["myinterests", account],
queryFn: async () => {
const res = await commands.getAllInterests();
if (res.status === "ok") {
const data = res.data.map((item) => JSON.parse(item) as NostrEvent);
return data;
} else {
throw new Error(res.error);
}
},
select: (data) =>
data.filter(
(item) => item.tags.filter((tag) => tag[0] === "t")?.length > 0,
),
refetchOnWindowFocus: false,
});
const renderItem = useCallback(
(item: NostrEvent) => {
const name = item.tags.filter((tag) => tag[0] === "d")[0][1] ?? "unnamed";
return (
<div
key={item.id}
className="group flex flex-col rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50 border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<div className="p-3 h-16 flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
{item.tags
.filter((tag) => tag[0] === "t")
.map((tag) => (
<div key={tag[1]} className="text-sm font-medium">
{tag[1]}
</div>
))}
</div>
<div className="p-3 flex items-center justify-between">
<div className="text-sm font-medium">{name}</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
label: name,
name,
url: `/columns/interests/${item.id}`,
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-blue-600 hover:bg-blue-500 text-white"
>
Add
</button>
</div>
</div>
</div>
);
},
[data],
);
return (
<div className="mb-12 flex flex-col gap-3">
<div className="flex items-center justify-between px-2">
<h3 className="font-semibold">My interests</h3>
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => refetch()}
className="size-7 inline-flex items-center justify-center rounded-full"
>
<ArrowClockwise className="size-4" />
</button>
<button
type="button"
onClick={() =>
LumeWindow.openPopup(
"New interest",
`/set-interest?account=${account}`,
)
}
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
<Plus className="size-3" weight="bold" />
New
</button>
</div>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : !data.length ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">You don't have any interests yet.</p>
</div>
) : (
data.map((item) => renderItem(item))
)}
</div>
</div>
);
}
-19
View File
@@ -1,19 +0,0 @@
import type { LumeColumn } from "@/types";
import { createFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile } from "@tauri-apps/plugin-fs";
export const Route = createFileRoute("/columns/_layout/gallery")({
beforeLoad: async () => {
const systemPath = "resources/columns.json";
const resourcePath = await resolveResource(systemPath);
const resourceFile = await readTextFile(resourcePath);
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
const columns = systemColumns.filter((col) => !col.default);
return {
columns,
};
},
});
-25
View File
@@ -1,25 +0,0 @@
import { commands } from "@/commands.gen";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/columns/_layout/group")({
beforeLoad: async ({ search }) => {
const key = `lume_v4:group:${search.label}`;
const res = await commands.getLumeStore(key);
if (res.status === "ok") {
const groups: string[] = JSON.parse(res.data);
if (groups.length) {
return { groups };
}
}
throw redirect({
to: "/columns/create-group",
search: {
...search,
redirect: "/columns/group",
},
});
},
});
@@ -7,16 +7,19 @@ import { ArrowDown } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useCallback, useRef } from "react";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/columns/_layout/group")({
export const Route = createLazyFileRoute("/columns/_layout/groups/$id")({
component: Screen,
});
export function Screen() {
const { label, account } = Route.useSearch();
const { groups } = Route.useRouteContext();
const group = Route.useLoaderData();
const params = Route.useParams();
const { queryClient } = Route.useRouteContext();
const {
data,
isLoading,
@@ -25,11 +28,11 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [label, account],
queryKey: ["groups", params.id],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const until = pageParam > 0 ? pageParam.toString() : undefined;
const res = await commands.getGroupEvents(groups, until);
const res = await commands.getAllEventsByAuthors(group, until);
if (res.status === "error") {
throw new Error(res.error);
@@ -39,6 +42,7 @@ export function Screen() {
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flat(),
enabled: group?.length > 0,
refetchOnWindowFocus: false,
});
@@ -80,6 +84,16 @@ export function Screen() {
[data],
);
useEffect(() => {
const unlisten = listen("synchronized", async () => {
await queryClient.invalidateQueries({ queryKey: ["groups", params.id] });
});
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<ScrollArea.Root
type={"scroll"}
+20
View File
@@ -0,0 +1,20 @@
import { commands } from "@/commands.gen";
import type { NostrEvent } from "@/types";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/columns/_layout/groups/$id")({
loader: async ({ params }) => {
const res = await commands.getGroup(params.id);
if (res.status === "ok") {
const event: NostrEvent = JSON.parse(res.data);
const tag = event.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1]);
return tag;
} else {
throw new Error(res.error);
}
},
});
@@ -7,16 +7,19 @@ import { ArrowDown } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useCallback, useRef } from "react";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/columns/_layout/hashtags/$content")({
export const Route = createLazyFileRoute("/columns/_layout/interests/$id")({
component: Screen,
});
export function Screen() {
const { label, account } = Route.useSearch();
const { content } = Route.useParams();
const hashtags = Route.useLoaderData();
const params = Route.useParams();
const { queryClient } = Route.useRouteContext();
const {
data,
isLoading,
@@ -25,12 +28,12 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [label, account],
queryKey: ["hashtags", params.id],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const hashtags = content.split("_");
const tags = hashtags.map((tag) => tag.toLowerCase().replace("#", ""));
const until = pageParam > 0 ? pageParam.toString() : undefined;
const res = await commands.getHashtagEvents(hashtags, until);
const res = await commands.getAllEventsByHashtags(tags, until);
if (res.status === "error") {
throw new Error(res.error);
@@ -81,6 +84,18 @@ export function Screen() {
[data],
);
useEffect(() => {
const unlisten = listen("synchronized", async () => {
await queryClient.invalidateQueries({
queryKey: ["hashtags", params.id],
});
});
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<ScrollArea.Root
type={"scroll"}
@@ -0,0 +1,20 @@
import { commands } from "@/commands.gen";
import type { NostrEvent } from "@/types";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/columns/_layout/interests/$id")({
loader: async ({ params }) => {
const res = await commands.getInterest(params.id);
if (res.status === "ok") {
const event: NostrEvent = JSON.parse(res.data);
const tag = event.tags
.filter((tag) => tag[0] === "t")
.map((tag) => tag[1]);
return tag;
} else {
throw new Error(res.error);
}
},
});
+4 -18
View File
@@ -6,11 +6,7 @@ import { Kind, type Meta } from "@/types";
import { ArrowDown, ArrowUp } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
import {
Navigate,
createLazyFileRoute,
useLocation,
} from "@tanstack/react-router";
import { createLazyFileRoute } from "@tanstack/react-router";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import {
@@ -33,12 +29,12 @@ export const Route = createLazyFileRoute("/columns/_layout/newsfeed")({
});
export function Screen() {
const contacts = Route.useLoaderData();
const { queryClient } = Route.useRouteContext();
const { label, account } = Route.useSearch();
const {
data,
isLoading,
isError,
isFetching,
isFetchingNextPage,
hasNextPage,
@@ -48,7 +44,7 @@ export function Screen() {
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const until = pageParam > 0 ? pageParam.toString() : undefined;
const res = await commands.getLocalEvents(until);
const res = await commands.getAllEventsByAuthors(contacts, until);
if (res.status === "error") {
throw new Error(res.error);
@@ -58,9 +54,9 @@ export function Screen() {
},
getNextPageParam: (lastPage) => lastPage?.at?.(-1)?.created_at - 1,
select: (data) => data?.pages.flat(),
enabled: contacts?.length > 0,
});
const location = useLocation();
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback(
@@ -109,16 +105,6 @@ export function Screen() {
};
}, []);
if (isError) {
return (
<Navigate
to="/columns/create-newsfeed/users"
// @ts-ignore, tanstack router bug.
search={{ label, account, redirect: location.href }}
/>
);
}
return (
<ScrollArea.Root
type={"scroll"}
@@ -1,7 +1,7 @@
import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/columns/_layout/create-group")({
export const Route = createFileRoute("/columns/_layout/newsfeed")({
loader: async () => {
const res = await commands.getContactList();
@@ -10,7 +10,7 @@ import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { nip19 } from "nostr-tools";
import { type ReactNode, useEffect, useRef } from "react";
import { type ReactNode, useEffect, useMemo, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/columns/_layout/notification")({
@@ -180,17 +180,7 @@ function Screen() {
</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>
<ZapReceipt key={event.id} event={event} />
))}
</div>
</div>
@@ -311,3 +301,36 @@ function TextNote({ event }: { event: LumeEvent }) {
</button>
);
}
function ZapReceipt({ event }: { event: LumeEvent }) {
const amount = useMemo(
() => decodeZapInvoice(event.tags).bitcoinFormatted ?? "0",
[event.id],
);
const sender = useMemo(
() => event.tags.find((tag) => tag[0] === "P")?.[1],
[event.id],
);
if (!sender) {
return (
<div className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
<div className="rounded-full size-6 bg-blue-500" />
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
{amount}
</div>
</div>
);
}
return (
<User.Provider pubkey={sender}>
<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">
{amount}
</div>
</User.Root>
</User.Provider>
);
}
+1 -1
View File
@@ -50,7 +50,7 @@ function StoryItem({ contact }: { contact: string }) {
} = useQuery({
queryKey: ["stories", contact],
queryFn: async () => {
const res = await commands.getEventsBy(contact, 10);
const res = await commands.getAllEventsByAuthor(contact, 10);
if (res.status === "ok") {
const data = toLumeEvents(res.data);
@@ -22,7 +22,7 @@ function Screen() {
const { isLoading, data: events } = useQuery({
queryKey: ["stories", id],
queryFn: async () => {
const res = await commands.getEventsBy(id, 100);
const res = await commands.getAllEventsByAuthor(id, 100);
if (res.status === "ok") {
const data = toLumeEvents(res.data);