mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
refactor: add event and user routes to default ui
This commit is contained in:
parent
c04ca3a1ab
commit
aa80301778
@ -75,6 +75,11 @@ export function CreateAccountScreen() {
|
||||
);
|
||||
} // else { user cancel action }
|
||||
|
||||
await storage.createAccount({
|
||||
pubkey: pubkey,
|
||||
privkey: signer.privateKey,
|
||||
});
|
||||
|
||||
return navigate("/auth/onboarding");
|
||||
};
|
||||
|
||||
@ -129,9 +134,9 @@ export function CreateAccountScreen() {
|
||||
return toast.error("Failed to create new account, try again later");
|
||||
}
|
||||
|
||||
await storage.createSetting("nsecbunker", "1");
|
||||
await storage.createAccount({
|
||||
id: localUser.npub,
|
||||
pubkey: localUser.pubkey,
|
||||
pubkey: account,
|
||||
privkey: localSigner.privateKey,
|
||||
});
|
||||
|
||||
|
@ -6,22 +6,15 @@ import {
|
||||
requestPermission,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function OnboardingScreen() {
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [settings, setSettings] = useState({
|
||||
autoupdate: false,
|
||||
notification: false,
|
||||
});
|
||||
|
||||
const next = () => {
|
||||
if (!storage.account.contacts.length) return navigate("/auth/follow");
|
||||
return navigate("/auth/finish");
|
||||
};
|
||||
|
||||
const toggleAutoupdate = async () => {
|
||||
await storage.createSetting("autoupdate", String(+!settings.autoupdate));
|
||||
// update state
|
||||
@ -107,13 +100,12 @@ export function OnboardingScreen() {
|
||||
"Settings" screen. Be sure to visit it later.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={next}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,7 +51,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
// NIP-46 Signer
|
||||
if (nsecbunker) {
|
||||
const localSignerPrivkey = await storage.loadPrivkey(
|
||||
`${storage.account.id}-nsecbunker`,
|
||||
storage.account.pubkey,
|
||||
);
|
||||
|
||||
if (!localSignerPrivkey) return null;
|
||||
@ -63,7 +63,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
"wss://nostr.vulpem.com/",
|
||||
]),
|
||||
});
|
||||
await bunker.connect(3000);
|
||||
await bunker.connect(2000);
|
||||
|
||||
const remoteSigner = new NDKNip46Signer(
|
||||
bunker,
|
||||
@ -91,7 +91,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
|
||||
async function init() {
|
||||
const platformName = await platform();
|
||||
const sqliteAdapter = await Database.load("sqlite:lume_v2.db");
|
||||
const sqliteAdapter = await Database.load("sqlite:lume_v3.db");
|
||||
|
||||
const storage = new LumeStorage(sqliteAdapter, platformName);
|
||||
await storage.init();
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ArrowLeftIcon } from "@lume/icons";
|
||||
import { ReplyList } from "@lume/ui";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function EventRoute() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="flex items-center px-3 mb-3 border-b h-11 bg-neutral-50 dark:bg-neutral-950 border-neutral-100 dark:border-neutral-900">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -2,9 +2,8 @@ import { Column } from "@lume/ark";
|
||||
import { GroupFeedsIcon } from "@lume/icons";
|
||||
import { IColumn } from "@lume/types";
|
||||
import { AntenasForm } from "./components/form";
|
||||
import { EventRoute } from "./event";
|
||||
import { HomeRoute } from "./home";
|
||||
import { UserRoute } from "./user";
|
||||
import { EventRoute, UserRoute } from "@lume/ui";
|
||||
|
||||
export function Antenas({ column }: { column: IColumn }) {
|
||||
const colKey = `antenas-${column.id}`;
|
||||
|
@ -1,213 +0,0 @@
|
||||
import {
|
||||
RepostNote,
|
||||
TextNote,
|
||||
useArk,
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { NIP05 } from "@lume/ui";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [id],
|
||||
},
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storage.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ArrowLeftIcon } from "@lume/icons";
|
||||
import { ReplyList } from "@lume/ui";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function EventRoute() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -2,9 +2,8 @@ import { Column } from "@lume/ark";
|
||||
import { GroupFeedsIcon } from "@lume/icons";
|
||||
import { IColumn } from "@lume/types";
|
||||
import { GroupForm } from "./components/form";
|
||||
import { EventRoute } from "./event";
|
||||
import { HomeRoute } from "./home";
|
||||
import { UserRoute } from "./user";
|
||||
import { EventRoute, UserRoute } from "@lume/ui";
|
||||
|
||||
export function Group({ column }: { column: IColumn }) {
|
||||
const colKey = `group-${column.id}`;
|
||||
|
@ -1,213 +0,0 @@
|
||||
import {
|
||||
RepostNote,
|
||||
TextNote,
|
||||
useArk,
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { NIP05 } from "@lume/ui";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [id],
|
||||
},
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storage.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ArrowLeftIcon } from "@lume/icons";
|
||||
import { ReplyList } from "@lume/ui";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function EventRoute() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
import { Column } from "@lume/ark";
|
||||
import { HashtagIcon } from "@lume/icons";
|
||||
import { IColumn } from "@lume/types";
|
||||
import { EventRoute } from "./event";
|
||||
import { HomeRoute } from "./home";
|
||||
import { UserRoute } from "./user";
|
||||
import { EventRoute, UserRoute } from "@lume/ui";
|
||||
|
||||
export function Hashtag({ column }: { column: IColumn }) {
|
||||
const colKey = `hashtag-${column.id}`;
|
||||
|
@ -1,213 +0,0 @@
|
||||
import {
|
||||
RepostNote,
|
||||
TextNote,
|
||||
useArk,
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { NIP05 } from "@lume/ui";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [id],
|
||||
},
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storage.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ArrowLeftIcon } from "@lume/icons";
|
||||
import { ReplyList } from "@lume/ui";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function EventRoute() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
import { Note, ThreadNote } from "@lume/ark";
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ReplyList } from "@lume/ui";
|
||||
import { WVList } from "virtua";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
|
||||
export function HomeRoute({ id }: { id: string }) {
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WVList>
|
||||
<div className="pb-5 overflow-y-auto">
|
||||
<WindowVirtualizer>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WindowVirtualizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Column } from "@lume/ark";
|
||||
import { IColumn } from "@lume/types";
|
||||
import { EventRoute } from "./event";
|
||||
import { HomeRoute } from "./home";
|
||||
import { UserRoute } from "./user";
|
||||
import { EventRoute, UserRoute } from "@lume/ui";
|
||||
|
||||
export function Thread({ column }: { column: IColumn }) {
|
||||
return (
|
||||
|
@ -1,213 +0,0 @@
|
||||
import {
|
||||
RepostNote,
|
||||
TextNote,
|
||||
useArk,
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { NIP05 } from "@lume/ui";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [id],
|
||||
},
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storage.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ArrowLeftIcon } from "@lume/icons";
|
||||
import { ReplyList } from "@lume/ui";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function EventRoute() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -4,9 +4,8 @@ import { IColumn } from "@lume/types";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useRef } from "react";
|
||||
import { EventRoute } from "./event";
|
||||
import { HomeRoute } from "./home";
|
||||
import { UserRoute } from "./user";
|
||||
import { EventRoute, UserRoute } from "@lume/ui";
|
||||
|
||||
export function Timeline({ column }: { column: IColumn }) {
|
||||
const colKey = `timeline-${column.id}`;
|
||||
|
@ -1,213 +0,0 @@
|
||||
import {
|
||||
RepostNote,
|
||||
TextNote,
|
||||
useArk,
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { NIP05 } from "@lume/ui";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [id],
|
||||
},
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storage.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ArrowLeftIcon } from "@lume/icons";
|
||||
import { ReplyList } from "@lume/ui";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function EventRoute() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -13,7 +13,7 @@ import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WVList } from "virtua";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
|
||||
export function HomeRoute({ id }: { id: string }) {
|
||||
const ark = useArk();
|
||||
@ -100,102 +100,104 @@ export function HomeRoute({ id }: { id: string }) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WVList className="py-5 overflow-y-auto">
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="object-cover w-12 h-12 rounded-lg shrink-0"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
<div className="py-5 overflow-y-auto">
|
||||
<WindowVirtualizer>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="object-cover w-12 h-12 rounded-lg shrink-0"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex items-center justify-center h-16 px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex items-center justify-center h-10 gap-2 px-6 font-medium text-white bg-blue-500 rounded-full w-max hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex items-center justify-center h-16 px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex items-center justify-center h-10 gap-2 px-6 font-medium text-white bg-blue-500 rounded-full w-max hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WVList>
|
||||
</WindowVirtualizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Column } from "@lume/ark";
|
||||
import { UserIcon } from "@lume/icons";
|
||||
import { IColumn } from "@lume/types";
|
||||
import { EventRoute } from "./event";
|
||||
import { HomeRoute } from "./home";
|
||||
import { UserRoute } from "./user";
|
||||
import { EventRoute, UserRoute } from "@lume/ui";
|
||||
|
||||
export function User({ column }: { column: IColumn }) {
|
||||
return (
|
||||
|
@ -1,213 +0,0 @@
|
||||
import {
|
||||
RepostNote,
|
||||
TextNote,
|
||||
useArk,
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { NIP05 } from "@lume/ui";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WVList } from "virtua";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [id],
|
||||
},
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storage.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WVList className="pb-5 overflow-y-auto">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WVList>
|
||||
);
|
||||
}
|
@ -254,11 +254,9 @@ export class LumeStorage {
|
||||
}
|
||||
|
||||
public async createAccount({
|
||||
id,
|
||||
pubkey,
|
||||
privkey,
|
||||
}: {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
privkey?: string;
|
||||
}) {
|
||||
@ -274,8 +272,8 @@ export class LumeStorage {
|
||||
);
|
||||
} else {
|
||||
await this.#db.execute(
|
||||
"INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);",
|
||||
[id, pubkey, 1],
|
||||
"INSERT OR IGNORE INTO accounts (pubkey, is_active) VALUES ($1, $2);",
|
||||
[pubkey, 1],
|
||||
);
|
||||
|
||||
if (privkey) await this.#keyring_save(pubkey, privkey);
|
||||
|
@ -28,7 +28,8 @@
|
||||
"react-router-dom": "^6.21.1",
|
||||
"slate": "^0.101.5",
|
||||
"slate-react": "^0.101.5",
|
||||
"sonner": "^1.3.1"
|
||||
"sonner": "^1.3.1",
|
||||
"virtua": "^0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tailwindcss": "workspace:^",
|
||||
|
@ -11,3 +11,5 @@ export * from "./layouts/home";
|
||||
export * from "./layouts/settings";
|
||||
export * from "./mentions";
|
||||
export * from "./replyList";
|
||||
export * from "./routes/event";
|
||||
export * from "./routes/user";
|
||||
|
31
packages/ui/src/routes/event.tsx
Normal file
31
packages/ui/src/routes/event.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { ThreadNote } from "@lume/ark";
|
||||
import { ArrowLeftIcon } from "@lume/icons";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
import { ReplyList } from "../replyList";
|
||||
|
||||
export function EventRoute() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="pb-5 overflow-y-auto">
|
||||
<WindowVirtualizer>
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<ThreadNote eventId={id} />
|
||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||
</div>
|
||||
</WindowVirtualizer>
|
||||
</div>
|
||||
);
|
||||
}
|
215
packages/ui/src/routes/user.tsx
Normal file
215
packages/ui/src/routes/user.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
import {
|
||||
RepostNote,
|
||||
TextNote,
|
||||
useArk,
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
import { NIP05 } from "../nip05";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: [id],
|
||||
},
|
||||
limit: FETCH_LIMIT,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mt-3" />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} className="mt-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storage.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="pb-5 overflow-y-auto">
|
||||
<WindowVirtualizer>
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
allEvents.map((item) => renderItem(item))
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center px-3 pb-3">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WindowVirtualizer>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -942,6 +942,9 @@ importers:
|
||||
sonner:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(react-dom@18.2.0)(react@18.2.0)
|
||||
virtua:
|
||||
specifier: ^0.20.0
|
||||
version: 0.20.0(react-dom@18.2.0)(react@18.2.0)
|
||||
devDependencies:
|
||||
'@lume/tailwindcss':
|
||||
specifier: workspace:^
|
||||
|
25
src-tauri/Cargo.lock
generated
25
src-tauri/Cargo.lock
generated
@ -2690,7 +2690,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lume"
|
||||
version = "2.2.0"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"keyring",
|
||||
"serde",
|
||||
@ -2702,7 +2702,7 @@ dependencies = [
|
||||
"tauri-plugin-cli",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-fs 2.0.0-alpha.7 (git+https://github.com/tauri-apps/plugins-workspace?branch=v2)",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
@ -5110,7 +5110,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-fs 2.0.0-alpha.7 (git+https://github.com/tauri-apps/plugins-workspace?branch=v2)",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@ -5129,10 +5129,25 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.0.0-alpha.7"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?rev=ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3#ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.0.0-alpha.9"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#1b1d795b5866e5524a9a9925f0fb7b2f8e3e3675"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?rev=ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3#ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"glob",
|
||||
@ -5141,7 +5156,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-fs 2.0.0-alpha.7 (git+https://github.com/tauri-apps/plugins-workspace?rev=ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3)",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lume"
|
||||
version = "2.2.0"
|
||||
version = "3.0.0"
|
||||
description = "the communication app"
|
||||
authors = ["Ren Amamiya"]
|
||||
license = "GPL-3.0"
|
||||
@ -23,7 +23,7 @@ tauri-plugin-cli = { git = "https://github.com/tauri-apps/plugins-workspace", br
|
||||
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", rev = "ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3" }
|
||||
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
|
@ -1,27 +1,40 @@
|
||||
-- create accounts table
|
||||
CREATE TABLE
|
||||
accounts (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL UNIQUE,
|
||||
follows TEXT,
|
||||
circles TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
last_login_at NUMBER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- create notes table
|
||||
-- create ndk cache users table
|
||||
CREATE TABLE
|
||||
events (
|
||||
ndk_users (
|
||||
pubkey TEXT NOT NULL PRIMARY KEY,
|
||||
profile TEXT,
|
||||
createdAt NUMBER
|
||||
);
|
||||
|
||||
-- create ndk cache events table
|
||||
CREATE TABLE
|
||||
ndk_events (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
kind NUMBER NOT NULL DEFAULt 1,
|
||||
root_id TEXT,
|
||||
reply_id TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
pubkey TEXT,
|
||||
content TEXT,
|
||||
kind NUMBER,
|
||||
createdAt NUMBER,
|
||||
relay TEXT,
|
||||
event TEXT
|
||||
);
|
||||
|
||||
-- create ndk cache eventtags table
|
||||
CREATE TABLE
|
||||
ndk_eventtags (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
eventId TEXT,
|
||||
tag TEXT,
|
||||
value TEXT,
|
||||
tagValue TEXT
|
||||
);
|
||||
|
||||
-- create settings table
|
||||
@ -33,8 +46,9 @@ CREATE TABLE
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- create columns table
|
||||
CREATE TABLE
|
||||
widgets (
|
||||
columns (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
kind INTEGER NOT NULL,
|
||||
@ -43,13 +57,3 @@ CREATE TABLE
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
||||
|
||||
CREATE TABLE
|
||||
relays (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
relay TEXT NOT NULL UNIQUE,
|
||||
purpose TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
||||
|
@ -1,27 +0,0 @@
|
||||
-- Add migration script here
|
||||
CREATE TABLE
|
||||
ndk_users (
|
||||
pubkey TEXT NOT NULL PRIMARY KEY,
|
||||
profile TEXT,
|
||||
createdAt NUMBER
|
||||
);
|
||||
|
||||
CREATE TABLE
|
||||
ndk_events (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
pubkey TEXT,
|
||||
content TEXT,
|
||||
kind NUMBER,
|
||||
createdAt NUMBER,
|
||||
relay TEXT,
|
||||
event TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE
|
||||
ndk_eventtags (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
eventId TEXT,
|
||||
tag TEXT,
|
||||
value TEXT,
|
||||
tagValue TEXT
|
||||
);
|
@ -1,5 +0,0 @@
|
||||
ALTER TABLE accounts DROP COLUMN follows;
|
||||
ALTER TABLE accounts DROP COLUMN circles;
|
||||
ALTER TABLE accounts DROP COLUMN last_login_at;
|
||||
DROP TABLE IF EXISTS events;
|
||||
DROP TABLE IF EXISTS relays;
|
@ -1,4 +1,7 @@
|
||||
use std::process::Command;
|
||||
use keyring::Entry;
|
||||
use std::time::Duration;
|
||||
use webpage::{Webpage, WebpageOptions};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn show_in_folder(path: String) {
|
||||
@ -46,3 +49,92 @@ pub async fn show_in_folder(path: String) {
|
||||
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct OpenGraphResponse {
|
||||
title: String,
|
||||
description: String,
|
||||
url: String,
|
||||
image: String,
|
||||
}
|
||||
|
||||
pub async fn fetch_opengraph(url: String) -> OpenGraphResponse {
|
||||
let options = WebpageOptions {
|
||||
allow_insecure: false,
|
||||
max_redirections: 3,
|
||||
timeout: Duration::from_secs(15),
|
||||
useragent: "lume - desktop app".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = match Webpage::from_url(&url, options) {
|
||||
Ok(webpage) => webpage,
|
||||
Err(_) => {
|
||||
return OpenGraphResponse {
|
||||
title: "".to_string(),
|
||||
description: "".to_string(),
|
||||
url: "".to_string(),
|
||||
image: "".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let html = result.html;
|
||||
|
||||
return OpenGraphResponse {
|
||||
title: html
|
||||
.opengraph
|
||||
.properties
|
||||
.get("title")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
description: html
|
||||
.opengraph
|
||||
.properties
|
||||
.get("description")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
url: html
|
||||
.opengraph
|
||||
.properties
|
||||
.get("url")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
image: html
|
||||
.opengraph
|
||||
.images
|
||||
.get(0)
|
||||
.and_then(|i| Some(i.url.clone()))
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn opengraph(url: String) -> OpenGraphResponse {
|
||||
let result = fetch_opengraph(url).await;
|
||||
return result;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn secure_save(key: String, value: String) -> Result<(), ()> {
|
||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
||||
let _ = entry.set_password(&value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn secure_load(key: String) -> Result<String, String> {
|
||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
||||
if let Ok(password) = entry.get_password() {
|
||||
Ok(password)
|
||||
} else {
|
||||
Err("not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn secure_remove(key: String) -> Result<(), ()> {
|
||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
||||
let _ = entry.delete_password();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -5,107 +5,9 @@
|
||||
|
||||
mod commands;
|
||||
|
||||
use keyring::Entry;
|
||||
use std::time::Duration;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||
use tauri_plugin_theme::ThemePlugin;
|
||||
use webpage::{Webpage, WebpageOptions};
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct Payload {
|
||||
args: Vec<String>,
|
||||
cwd: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct OpenGraphResponse {
|
||||
title: String,
|
||||
description: String,
|
||||
url: String,
|
||||
image: String,
|
||||
}
|
||||
|
||||
async fn fetch_opengraph(url: String) -> OpenGraphResponse {
|
||||
let options = WebpageOptions {
|
||||
allow_insecure: false,
|
||||
max_redirections: 3,
|
||||
timeout: Duration::from_secs(15),
|
||||
useragent: "lume - desktop app".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = match Webpage::from_url(&url, options) {
|
||||
Ok(webpage) => webpage,
|
||||
Err(_) => {
|
||||
return OpenGraphResponse {
|
||||
title: "".to_string(),
|
||||
description: "".to_string(),
|
||||
url: "".to_string(),
|
||||
image: "".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let html = result.html;
|
||||
|
||||
return OpenGraphResponse {
|
||||
title: html
|
||||
.opengraph
|
||||
.properties
|
||||
.get("title")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
description: html
|
||||
.opengraph
|
||||
.properties
|
||||
.get("description")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
url: html
|
||||
.opengraph
|
||||
.properties
|
||||
.get("url")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
image: html
|
||||
.opengraph
|
||||
.images
|
||||
.get(0)
|
||||
.and_then(|i| Some(i.url.clone()))
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn opengraph(url: String) -> OpenGraphResponse {
|
||||
let result = fetch_opengraph(url).await;
|
||||
return result;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn secure_save(key: String, value: String) -> Result<(), ()> {
|
||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
||||
let _ = entry.set_password(&value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn secure_load(key: String) -> Result<String, String> {
|
||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
||||
if let Ok(password) = entry.get_password() {
|
||||
Ok(password)
|
||||
} else {
|
||||
Err("not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn secure_remove(key: String) -> Result<(), ()> {
|
||||
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
||||
let _ = entry.delete_password();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut ctx = tauri::generate_context!();
|
||||
@ -120,7 +22,7 @@ fn main() {
|
||||
.plugin(
|
||||
tauri_plugin_sql::Builder::default()
|
||||
.add_migrations(
|
||||
"sqlite:lume_v2.db",
|
||||
"sqlite:lume_v3.db",
|
||||
vec![
|
||||
Migration {
|
||||
version: 20230418013219,
|
||||
@ -128,18 +30,6 @@ fn main() {
|
||||
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20231028083224,
|
||||
description: "add ndk cache table",
|
||||
sql: include_str!("../migrations/20231028083224_add_ndk_cache_table.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20231130105202,
|
||||
description: "clean up table",
|
||||
sql: include_str!("../migrations/20231130105202_clean_up_table.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
@ -160,10 +50,10 @@ fn main() {
|
||||
Some(vec![]),
|
||||
))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
opengraph,
|
||||
secure_save,
|
||||
secure_load,
|
||||
secure_remove,
|
||||
commands::opengraph,
|
||||
commands::secure_save,
|
||||
commands::secure_load,
|
||||
commands::secure_remove,
|
||||
commands::show_in_folder,
|
||||
])
|
||||
.run(ctx)
|
||||
|
Loading…
Reference in New Issue
Block a user