feat: Multi Accounts (#237)
* wip: new sync * wip: restructure routes * update * feat: improve sync * feat: repost with multi-account * feat: improve sync * feat: publish with multi account * fix: settings screen * feat: add zap for multi accounts
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appSettings } from "@/commons";
|
||||
import type { ColumnRouteSearch } from "@/types";
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export interface RouteSearch {
|
||||
label?: string;
|
||||
name?: string;
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout")({
|
||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||
validateSearch: (search: Record<string, string>): RouteSearch => {
|
||||
return {
|
||||
account: search.account,
|
||||
label: search.label,
|
||||
name: search.name,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/columns/_layout/global")({
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
const { label, account } = Route.useSearch();
|
||||
const { label } = Route.useSearch();
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
@@ -24,7 +24,7 @@ export function Screen() {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [label, account],
|
||||
queryKey: ["events", "global", label],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Screen() {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ["groups", params.id],
|
||||
queryKey: ["events", "groups", params.id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Screen() {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ["hashtags", params.id],
|
||||
queryKey: ["events", "hashtags", params.id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const tags = hashtags.map((tag) => tag.toLowerCase().replace("#", ""));
|
||||
|
||||
@@ -26,6 +26,7 @@ function Screen() {
|
||||
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
|
||||
<Groups />
|
||||
<Interests />
|
||||
<Accounts />
|
||||
<Core />
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
@@ -39,66 +40,9 @@ function Screen() {
|
||||
);
|
||||
}
|
||||
|
||||
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 Groups() {
|
||||
const { account } = Route.useSearch();
|
||||
const { isLoading, data, refetch, isRefetching } = useQuery({
|
||||
queryKey: ["groups", account],
|
||||
queryKey: ["others", "groups"],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAllGroups();
|
||||
|
||||
@@ -125,23 +69,32 @@ function Groups() {
|
||||
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"
|
||||
className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
|
||||
>
|
||||
<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 className="px-2 pt-2">
|
||||
<div className="p-3 h-16 bg-neutral-100 rounded-lg 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>
|
||||
<div className="p-3 flex items-center justify-between">
|
||||
<div className="text-sm font-medium">{name}</div>
|
||||
<div className="p-2 flex items-center justify-between">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<User.Provider pubkey={item.pubkey}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-7 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<h5 className="text-xs font-medium">{name}</h5>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -181,9 +134,7 @@ function Groups() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
LumeWindow.openPopup("New group", `/set-group?account=${account}`)
|
||||
}
|
||||
onClick={() => LumeWindow.openPopup("/set-group", "New group")}
|
||||
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" />
|
||||
@@ -210,9 +161,8 @@ function Groups() {
|
||||
}
|
||||
|
||||
function Interests() {
|
||||
const { account } = Route.useSearch();
|
||||
const { isLoading, data, refetch, isRefetching } = useQuery({
|
||||
queryKey: ["interests", account],
|
||||
queryKey: ["others", "interests"],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAllInterests();
|
||||
|
||||
@@ -240,19 +190,28 @@ function Interests() {
|
||||
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"
|
||||
className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
|
||||
>
|
||||
<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 className="px-2 pt-2">
|
||||
<div className="p-3 h-16 bg-neutral-100 rounded-lg flex flex-wrap items-center justify-center gap-4 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>
|
||||
<div className="p-3 flex items-center justify-between">
|
||||
<div className="text-sm font-medium">{name}</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<User.Provider pubkey={item.pubkey}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-7 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<h5 className="text-xs font-medium">{name}</h5>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -293,10 +252,7 @@ function Interests() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
LumeWindow.openPopup(
|
||||
"New interest",
|
||||
`/set-interest?account=${account}`,
|
||||
)
|
||||
LumeWindow.openPopup("/set-interest", "New interest")
|
||||
}
|
||||
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"
|
||||
>
|
||||
@@ -322,3 +278,134 @@ function Interests() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Accounts() {
|
||||
const { isLoading, data: accounts } = useQuery({
|
||||
queryKey: ["accounts"],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAccounts();
|
||||
return res;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-12 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<h3 className="font-semibold">Accounts</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isLoading ? (
|
||||
<div className="inline-flex items-center gap-1.5 text-sm">
|
||||
<Spinner className="size-4" />
|
||||
Loading...
|
||||
</div>
|
||||
) : (
|
||||
accounts.map((account) => (
|
||||
<div
|
||||
key={account}
|
||||
className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
|
||||
>
|
||||
<div className="px-2 pt-2">
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root className="inline-flex items-center gap-2">
|
||||
<User.Avatar className="size-7 rounded-full" />
|
||||
<User.Name className="text-xs font-medium" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
<div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800">
|
||||
<div className="text-sm font-medium">Newsfeed</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openNewsfeed(account)}
|
||||
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800">
|
||||
<div className="text-sm font-medium">Stories</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openStory(account)}
|
||||
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800">
|
||||
<div className="text-sm font-medium">Notification</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openNotification(account)}
|
||||
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Core() {
|
||||
const { isLoading, data } = useQuery({
|
||||
queryKey: ["other-columns"],
|
||||
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">Others</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isLoading ? (
|
||||
<div className="inline-flex items-center gap-1.5 text-sm">
|
||||
<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 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"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+35
-105
@@ -1,35 +1,23 @@
|
||||
import { events, commands } from "@/commands.gen";
|
||||
import { toLumeEvents } from "@/commons";
|
||||
import { RepostNote, Spinner, TextNote } from "@/components";
|
||||
import { LumeEvent } from "@/system";
|
||||
import { Kind, type Meta } from "@/types";
|
||||
import { ArrowDown, ArrowUp } from "@phosphor-icons/react";
|
||||
import type { LumeEvent } from "@/system";
|
||||
import { Kind } from "@/types";
|
||||
import { ArrowDown } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
type Payload = {
|
||||
raw: string;
|
||||
parsed: Meta;
|
||||
};
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/newsfeed")({
|
||||
export const Route = createLazyFileRoute("/columns/_layout/newsfeed/$id")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
const contacts = Route.useLoaderData();
|
||||
const { label, account } = Route.useSearch();
|
||||
const search = Route.useSearch();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
@@ -38,7 +26,7 @@ export function Screen() {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [label, account],
|
||||
queryKey: ["events", "newsfeed", search.label],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
@@ -59,7 +47,10 @@ export function Screen() {
|
||||
|
||||
const renderItem = useCallback(
|
||||
(event: LumeEvent) => {
|
||||
if (!event) return;
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.kind) {
|
||||
case Kind.Repost:
|
||||
return (
|
||||
@@ -82,6 +73,28 @@ export function Screen() {
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
events.subscription
|
||||
.emit({
|
||||
label: search.label,
|
||||
kind: "Subscribe",
|
||||
event_id: undefined,
|
||||
contacts,
|
||||
})
|
||||
.then(() => console.log("Subscribe: ", search.label));
|
||||
|
||||
return () => {
|
||||
events.subscription
|
||||
.emit({
|
||||
label: search.label,
|
||||
kind: "Unsubscribe",
|
||||
event_id: undefined,
|
||||
contacts,
|
||||
})
|
||||
.then(() => console.log("Unsubscribe: ", search.label));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
@@ -92,7 +105,6 @@ export function Screen() {
|
||||
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}>
|
||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||
@@ -145,85 +157,3 @@ export function Screen() {
|
||||
</ScrollArea.Root>
|
||||
);
|
||||
}
|
||||
|
||||
const Listener = memo(function Listerner() {
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
const { label, account } = Route.useSearch();
|
||||
|
||||
const [lumeEvents, setLumeEvents] = useState<LumeEvent[]>([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const queryStatus = queryClient.getQueryState([label, account]);
|
||||
|
||||
const pushNewEvents = () => {
|
||||
startTransition(() => {
|
||||
queryClient.setQueryData(
|
||||
[label, account],
|
||||
(oldData: InfiniteData<LumeEvent[], number> | undefined) => {
|
||||
if (oldData) {
|
||||
const firstPage = oldData.pages[0];
|
||||
const newPage = [...lumeEvents, ...firstPage];
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
pages: [newPage, ...oldData.pages.slice(1)],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Reset array
|
||||
setLumeEvents([]);
|
||||
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
events.subscription
|
||||
.emit({ label, kind: "Subscribe", event_id: undefined })
|
||||
.then(() => console.log("Subscribe: ", label));
|
||||
|
||||
return () => {
|
||||
events.subscription
|
||||
.emit({
|
||||
label,
|
||||
kind: "Unsubscribe",
|
||||
event_id: undefined,
|
||||
})
|
||||
.then(() => console.log("Unsubscribe: ", label));
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = getCurrentWindow().listen<Payload>("event", (data) => {
|
||||
const event = LumeEvent.from(data.payload.raw, data.payload.parsed);
|
||||
setLumeEvents((prev) => [event, ...prev]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (lumeEvents.length && queryStatus.fetchStatus !== "fetching") {
|
||||
return (
|
||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pushNewEvents()}
|
||||
className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white"
|
||||
>
|
||||
{isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<ArrowUp className="size-4" />
|
||||
)}
|
||||
{lumeEvents.length} new notes
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/newsfeed")({
|
||||
loader: async () => {
|
||||
const res = await commands.getContactList();
|
||||
export const Route = createFileRoute("/columns/_layout/newsfeed/$id")({
|
||||
loader: async ({ params }) => {
|
||||
const res = await commands.getContactList(params.id);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
+9
-9
@@ -13,17 +13,17 @@ import { nip19 } from "nostr-tools";
|
||||
import { type ReactNode, useEffect, useMemo, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/notification")({
|
||||
export const Route = createLazyFileRoute("/columns/_layout/notification/$id")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { account } = Route.useSearch();
|
||||
const { id } = Route.useParams();
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
const { isLoading, data } = useQuery({
|
||||
queryKey: ["notification", account],
|
||||
queryKey: ["notification", id],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getNotifications();
|
||||
const res = await commands.getNotifications(id);
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.error);
|
||||
@@ -37,7 +37,7 @@ function Screen() {
|
||||
select: (events) => {
|
||||
const zaps = new Map<string, LumeEvent[]>();
|
||||
const reactions = new Map<string, LumeEvent[]>();
|
||||
const hex = nip19.decode(account).data;
|
||||
const hex = nip19.decode(id).data;
|
||||
|
||||
const texts = events.filter(
|
||||
(ev) => ev.kind === Kind.Text && ev.pubkey !== hex,
|
||||
@@ -80,7 +80,7 @@ function Screen() {
|
||||
const unlisten = getCurrentWindow().listen("event", async (data) => {
|
||||
const event: LumeEvent = JSON.parse(data.payload as string);
|
||||
await queryClient.setQueryData(
|
||||
["notification", account],
|
||||
["notification", id],
|
||||
(data: LumeEvent[]) => [event, ...data],
|
||||
);
|
||||
});
|
||||
@@ -88,7 +88,7 @@ function Screen() {
|
||||
return () => {
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, [account]);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -130,8 +130,8 @@ function Screen() {
|
||||
className="min-h-0 flex-1 overflow-x-hidden"
|
||||
>
|
||||
<Tab value="replies">
|
||||
{data.texts.map((event, index) => (
|
||||
<TextNote key={event.id + index} event={event} />
|
||||
{data.texts.map((event) => (
|
||||
<TextNote key={event.id} event={event} />
|
||||
))}
|
||||
</Tab>
|
||||
<Tab value="reactions">
|
||||
+3
-3
@@ -14,7 +14,7 @@ import { type ReactNode, memo, useMemo, useRef } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/stories")({
|
||||
export const Route = createLazyFileRoute("/columns/_layout/stories/$id")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ function StoryItem({ contact }: { contact: string }) {
|
||||
error,
|
||||
data: events,
|
||||
} = useQuery({
|
||||
queryKey: ["stories", contact],
|
||||
queryKey: ["events", "story", contact],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAllEventsByAuthor(contact, 10);
|
||||
|
||||
@@ -113,7 +113,7 @@ function StoryItem({ contact }: { contact: string }) {
|
||||
</div>
|
||||
) : !events.length ? (
|
||||
<div className="w-full h-[calc(300px-48px)] flex items-center justify-center text-sm">
|
||||
This user didn't have any new notes.
|
||||
This user didn't have any new notes in the last few days.
|
||||
</div>
|
||||
) : (
|
||||
events.map((event) => <StoryEvent key={event.id} event={event} />)
|
||||
@@ -1,9 +1,9 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/stories")({
|
||||
loader: async () => {
|
||||
const res = await commands.getContactList();
|
||||
export const Route = createFileRoute("/columns/_layout/stories/$id")({
|
||||
loader: async ({ params }) => {
|
||||
const res = await commands.getContactList(params.id);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
@@ -13,7 +13,7 @@ export const Route = createLazyFileRoute("/columns/_layout/trending")({
|
||||
|
||||
function Screen() {
|
||||
const { isLoading, data } = useQuery({
|
||||
queryKey: ["trending-notes"],
|
||||
queryKey: ["trending"],
|
||||
queryFn: async ({ signal }) => {
|
||||
const res = await fetch("https://api.nostr.band/v0/trending/notes", {
|
||||
signal,
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Route = createLazyFileRoute("/columns/_layout/users/$id")({
|
||||
function Screen() {
|
||||
const { id } = Route.useParams();
|
||||
const { isLoading, data: events } = useQuery({
|
||||
queryKey: ["stories", id],
|
||||
queryKey: ["events", "story", id],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAllEventsByAuthor(id, 100);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user