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 }
|
} // else { user cancel action }
|
||||||
|
|
||||||
|
await storage.createAccount({
|
||||||
|
pubkey: pubkey,
|
||||||
|
privkey: signer.privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
return navigate("/auth/onboarding");
|
return navigate("/auth/onboarding");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -129,9 +134,9 @@ export function CreateAccountScreen() {
|
|||||||
return toast.error("Failed to create new account, try again later");
|
return toast.error("Failed to create new account, try again later");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await storage.createSetting("nsecbunker", "1");
|
||||||
await storage.createAccount({
|
await storage.createAccount({
|
||||||
id: localUser.npub,
|
pubkey: account,
|
||||||
pubkey: localUser.pubkey,
|
|
||||||
privkey: localSigner.privateKey,
|
privkey: localSigner.privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,22 +6,15 @@ import {
|
|||||||
requestPermission,
|
requestPermission,
|
||||||
} from "@tauri-apps/plugin-notification";
|
} from "@tauri-apps/plugin-notification";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export function OnboardingScreen() {
|
export function OnboardingScreen() {
|
||||||
const storage = useStorage();
|
const storage = useStorage();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
autoupdate: false,
|
autoupdate: false,
|
||||||
notification: false,
|
notification: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const next = () => {
|
|
||||||
if (!storage.account.contacts.length) return navigate("/auth/follow");
|
|
||||||
return navigate("/auth/finish");
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAutoupdate = async () => {
|
const toggleAutoupdate = async () => {
|
||||||
await storage.createSetting("autoupdate", String(+!settings.autoupdate));
|
await storage.createSetting("autoupdate", String(+!settings.autoupdate));
|
||||||
// update state
|
// update state
|
||||||
@ -107,13 +100,12 @@ export function OnboardingScreen() {
|
|||||||
"Settings" screen. Be sure to visit it later.
|
"Settings" screen. Be sure to visit it later.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Link
|
||||||
type="button"
|
to="/"
|
||||||
onClick={next}
|
|
||||||
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"
|
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
|
Continue
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +51,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
// NIP-46 Signer
|
// NIP-46 Signer
|
||||||
if (nsecbunker) {
|
if (nsecbunker) {
|
||||||
const localSignerPrivkey = await storage.loadPrivkey(
|
const localSignerPrivkey = await storage.loadPrivkey(
|
||||||
`${storage.account.id}-nsecbunker`,
|
storage.account.pubkey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!localSignerPrivkey) return null;
|
if (!localSignerPrivkey) return null;
|
||||||
@ -63,7 +63,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
"wss://nostr.vulpem.com/",
|
"wss://nostr.vulpem.com/",
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
await bunker.connect(3000);
|
await bunker.connect(2000);
|
||||||
|
|
||||||
const remoteSigner = new NDKNip46Signer(
|
const remoteSigner = new NDKNip46Signer(
|
||||||
bunker,
|
bunker,
|
||||||
@ -91,7 +91,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const platformName = await platform();
|
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);
|
const storage = new LumeStorage(sqliteAdapter, platformName);
|
||||||
await storage.init();
|
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 { GroupFeedsIcon } from "@lume/icons";
|
||||||
import { IColumn } from "@lume/types";
|
import { IColumn } from "@lume/types";
|
||||||
import { AntenasForm } from "./components/form";
|
import { AntenasForm } from "./components/form";
|
||||||
import { EventRoute } from "./event";
|
|
||||||
import { HomeRoute } from "./home";
|
import { HomeRoute } from "./home";
|
||||||
import { UserRoute } from "./user";
|
import { EventRoute, UserRoute } from "@lume/ui";
|
||||||
|
|
||||||
export function Antenas({ column }: { column: IColumn }) {
|
export function Antenas({ column }: { column: IColumn }) {
|
||||||
const colKey = `antenas-${column.id}`;
|
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 { GroupFeedsIcon } from "@lume/icons";
|
||||||
import { IColumn } from "@lume/types";
|
import { IColumn } from "@lume/types";
|
||||||
import { GroupForm } from "./components/form";
|
import { GroupForm } from "./components/form";
|
||||||
import { EventRoute } from "./event";
|
|
||||||
import { HomeRoute } from "./home";
|
import { HomeRoute } from "./home";
|
||||||
import { UserRoute } from "./user";
|
import { EventRoute, UserRoute } from "@lume/ui";
|
||||||
|
|
||||||
export function Group({ column }: { column: IColumn }) {
|
export function Group({ column }: { column: IColumn }) {
|
||||||
const colKey = `group-${column.id}`;
|
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 { Column } from "@lume/ark";
|
||||||
import { HashtagIcon } from "@lume/icons";
|
import { HashtagIcon } from "@lume/icons";
|
||||||
import { IColumn } from "@lume/types";
|
import { IColumn } from "@lume/types";
|
||||||
import { EventRoute } from "./event";
|
|
||||||
import { HomeRoute } from "./home";
|
import { HomeRoute } from "./home";
|
||||||
import { UserRoute } from "./user";
|
import { EventRoute, UserRoute } from "@lume/ui";
|
||||||
|
|
||||||
export function Hashtag({ column }: { column: IColumn }) {
|
export function Hashtag({ column }: { column: IColumn }) {
|
||||||
const colKey = `hashtag-${column.id}`;
|
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 { ReplyList } from "@lume/ui";
|
||||||
import { WVList } from "virtua";
|
import { WindowVirtualizer } from "virtua";
|
||||||
|
|
||||||
export function HomeRoute({ id }: { id: string }) {
|
export function HomeRoute({ id }: { id: string }) {
|
||||||
return (
|
return (
|
||||||
<WVList className="pb-5 overflow-y-auto">
|
<div className="pb-5 overflow-y-auto">
|
||||||
<div className="px-3">
|
<WindowVirtualizer>
|
||||||
<ThreadNote eventId={id} />
|
<div className="px-3">
|
||||||
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
<ThreadNote eventId={id} />
|
||||||
</div>
|
<ReplyList eventId={id} title="All replies" className="mt-5" />
|
||||||
</WVList>
|
</div>
|
||||||
|
</WindowVirtualizer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Column } from "@lume/ark";
|
import { Column } from "@lume/ark";
|
||||||
import { IColumn } from "@lume/types";
|
import { IColumn } from "@lume/types";
|
||||||
import { EventRoute } from "./event";
|
|
||||||
import { HomeRoute } from "./home";
|
import { HomeRoute } from "./home";
|
||||||
import { UserRoute } from "./user";
|
import { EventRoute, UserRoute } from "@lume/ui";
|
||||||
|
|
||||||
export function Thread({ column }: { column: IColumn }) {
|
export function Thread({ column }: { column: IColumn }) {
|
||||||
return (
|
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 { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { EventRoute } from "./event";
|
|
||||||
import { HomeRoute } from "./home";
|
import { HomeRoute } from "./home";
|
||||||
import { UserRoute } from "./user";
|
import { EventRoute, UserRoute } from "@lume/ui";
|
||||||
|
|
||||||
export function Timeline({ column }: { column: IColumn }) {
|
export function Timeline({ column }: { column: IColumn }) {
|
||||||
const colKey = `timeline-${column.id}`;
|
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 { useEffect, useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { WVList } from "virtua";
|
import { WindowVirtualizer } from "virtua";
|
||||||
|
|
||||||
export function HomeRoute({ id }: { id: string }) {
|
export function HomeRoute({ id }: { id: string }) {
|
||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
@ -100,102 +100,104 @@ export function HomeRoute({ id }: { id: string }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WVList className="py-5 overflow-y-auto">
|
<div className="py-5 overflow-y-auto">
|
||||||
<div className="px-3">
|
<WindowVirtualizer>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="px-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-2">
|
||||||
<img
|
<div className="flex items-center justify-between">
|
||||||
src={user?.picture || user?.image}
|
<img
|
||||||
alt={id}
|
src={user?.picture || user?.image}
|
||||||
className="object-cover w-12 h-12 rounded-lg shrink-0"
|
alt={id}
|
||||||
loading="lazy"
|
className="object-cover w-12 h-12 rounded-lg shrink-0"
|
||||||
decoding="async"
|
loading="lazy"
|
||||||
/>
|
decoding="async"
|
||||||
<div className="inline-flex items-center gap-2">
|
/>
|
||||||
{followed ? (
|
<div className="inline-flex items-center gap-2">
|
||||||
<button
|
{followed ? (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => unfollow(id)}
|
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"
|
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
|
Message
|
||||||
</button>
|
</Link>
|
||||||
) : (
|
</div>
|
||||||
<button
|
</div>
|
||||||
type="button"
|
<div className="flex flex-1 flex-col gap-1.5">
|
||||||
onClick={() => follow(id)}
|
<div className="flex flex-col">
|
||||||
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"
|
<h5 className="text-lg font-semibold">
|
||||||
>
|
{user?.name ||
|
||||||
Follow
|
user?.display_name ||
|
||||||
</button>
|
user?.displayName ||
|
||||||
)}
|
"Anon"}
|
||||||
<Link
|
</h5>
|
||||||
to={`/chats/${id}`}
|
{user?.nip05 ? (
|
||||||
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"
|
<NIP05
|
||||||
>
|
pubkey={id}
|
||||||
Message
|
nip05={user?.nip05}
|
||||||
</Link>
|
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||||
</div>
|
/>
|
||||||
</div>
|
) : (
|
||||||
<div className="flex flex-1 flex-col gap-1.5">
|
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<div className="flex flex-col">
|
{displayNpub(id, 16)}
|
||||||
<h5 className="text-lg font-semibold">
|
</span>
|
||||||
{user?.name ||
|
)}
|
||||||
user?.display_name ||
|
</div>
|
||||||
user?.displayName ||
|
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||||
"Anon"}
|
{user?.about}
|
||||||
</h5>
|
</div>
|
||||||
{user?.nip05 ? (
|
</div>
|
||||||
<NIP05
|
</div>
|
||||||
pubkey={id}
|
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||||
nip05={user?.nip05}
|
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
Latest posts
|
||||||
/>
|
</h3>
|
||||||
) : (
|
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
{isLoading ? (
|
||||||
{displayNpub(id, 16)}
|
<div className="flex items-center justify-center">
|
||||||
</span>
|
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||||
)}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
allEvents.map((item) => renderItem(item))
|
||||||
{user?.about}
|
)}
|
||||||
</div>
|
<div className="flex items-center justify-center h-16 px-3 pb-3">
|
||||||
</div>
|
{hasNextPage ? (
|
||||||
</div>
|
<button
|
||||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
type="button"
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
onClick={() => fetchNextPage()}
|
||||||
Latest posts
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
</h3>
|
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"
|
||||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
>
|
||||||
{isLoading ? (
|
{isFetchingNextPage ? (
|
||||||
<div className="flex items-center justify-center">
|
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||||
|
Load more
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</WindowVirtualizer>
|
||||||
</WVList>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { Column } from "@lume/ark";
|
import { Column } from "@lume/ark";
|
||||||
import { UserIcon } from "@lume/icons";
|
import { UserIcon } from "@lume/icons";
|
||||||
import { IColumn } from "@lume/types";
|
import { IColumn } from "@lume/types";
|
||||||
import { EventRoute } from "./event";
|
|
||||||
import { HomeRoute } from "./home";
|
import { HomeRoute } from "./home";
|
||||||
import { UserRoute } from "./user";
|
import { EventRoute, UserRoute } from "@lume/ui";
|
||||||
|
|
||||||
export function User({ column }: { column: IColumn }) {
|
export function User({ column }: { column: IColumn }) {
|
||||||
return (
|
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({
|
public async createAccount({
|
||||||
id,
|
|
||||||
pubkey,
|
pubkey,
|
||||||
privkey,
|
privkey,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
privkey?: string;
|
privkey?: string;
|
||||||
}) {
|
}) {
|
||||||
@ -274,8 +272,8 @@ export class LumeStorage {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.#db.execute(
|
await this.#db.execute(
|
||||||
"INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);",
|
"INSERT OR IGNORE INTO accounts (pubkey, is_active) VALUES ($1, $2);",
|
||||||
[id, pubkey, 1],
|
[pubkey, 1],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (privkey) await this.#keyring_save(pubkey, privkey);
|
if (privkey) await this.#keyring_save(pubkey, privkey);
|
||||||
|
@ -28,7 +28,8 @@
|
|||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
"slate": "^0.101.5",
|
"slate": "^0.101.5",
|
||||||
"slate-react": "^0.101.5",
|
"slate-react": "^0.101.5",
|
||||||
"sonner": "^1.3.1"
|
"sonner": "^1.3.1",
|
||||||
|
"virtua": "^0.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lume/tailwindcss": "workspace:^",
|
"@lume/tailwindcss": "workspace:^",
|
||||||
|
@ -11,3 +11,5 @@ export * from "./layouts/home";
|
|||||||
export * from "./layouts/settings";
|
export * from "./layouts/settings";
|
||||||
export * from "./mentions";
|
export * from "./mentions";
|
||||||
export * from "./replyList";
|
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:
|
sonner:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.3.1(react-dom@18.2.0)(react@18.2.0)
|
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:
|
devDependencies:
|
||||||
'@lume/tailwindcss':
|
'@lume/tailwindcss':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
|
25
src-tauri/Cargo.lock
generated
25
src-tauri/Cargo.lock
generated
@ -2690,7 +2690,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "2.2.0"
|
version = "3.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"keyring",
|
"keyring",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2702,7 +2702,7 @@ dependencies = [
|
|||||||
"tauri-plugin-cli",
|
"tauri-plugin-cli",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
"tauri-plugin-dialog",
|
"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-http",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
@ -5110,7 +5110,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs 2.0.0-alpha.7 (git+https://github.com/tauri-apps/plugins-workspace?branch=v2)",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5129,10 +5129,25 @@ dependencies = [
|
|||||||
"uuid",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-http"
|
name = "tauri-plugin-http"
|
||||||
version = "2.0.0-alpha.9"
|
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 = [
|
dependencies = [
|
||||||
"data-url",
|
"data-url",
|
||||||
"glob",
|
"glob",
|
||||||
@ -5141,7 +5156,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs 2.0.0-alpha.7 (git+https://github.com/tauri-apps/plugins-workspace?rev=ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3)",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "2.2.0"
|
version = "3.0.0"
|
||||||
description = "the communication app"
|
description = "the communication app"
|
||||||
authors = ["Ren Amamiya"]
|
authors = ["Ren Amamiya"]
|
||||||
license = "GPL-3.0"
|
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-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-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-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-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-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
tauri-plugin-process = { 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 accounts table
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
accounts (
|
accounts (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
pubkey TEXT NOT NULL UNIQUE,
|
pubkey TEXT NOT NULL UNIQUE,
|
||||||
follows TEXT,
|
|
||||||
circles TEXT,
|
|
||||||
is_active INTEGER NOT NULL DEFAULT 0,
|
is_active INTEGER NOT NULL DEFAULT 0,
|
||||||
last_login_at NUMBER NOT NULL DEFAULT 0,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- create notes table
|
-- create ndk cache users table
|
||||||
CREATE 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,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
account_id INTEGER NOT NULL,
|
pubkey TEXT,
|
||||||
event TEXT NOT NULL,
|
content TEXT,
|
||||||
author TEXT NOT NULL,
|
kind NUMBER,
|
||||||
kind NUMBER NOT NULL DEFAULt 1,
|
createdAt NUMBER,
|
||||||
root_id TEXT,
|
relay TEXT,
|
||||||
reply_id TEXT,
|
event TEXT
|
||||||
created_at INTEGER NOT NULL,
|
);
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
|
||||||
|
-- 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
|
-- create settings table
|
||||||
@ -33,8 +46,9 @@ CREATE TABLE
|
|||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- create columns table
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
widgets (
|
columns (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
account_id INTEGER NOT NULL,
|
account_id INTEGER NOT NULL,
|
||||||
kind INTEGER NOT NULL,
|
kind INTEGER NOT NULL,
|
||||||
@ -43,13 +57,3 @@ CREATE TABLE
|
|||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
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 std::process::Command;
|
||||||
|
use keyring::Entry;
|
||||||
|
use std::time::Duration;
|
||||||
|
use webpage::{Webpage, WebpageOptions};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn show_in_folder(path: String) {
|
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();
|
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;
|
mod commands;
|
||||||
|
|
||||||
use keyring::Entry;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
use tauri_plugin_theme::ThemePlugin;
|
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() {
|
fn main() {
|
||||||
let mut ctx = tauri::generate_context!();
|
let mut ctx = tauri::generate_context!();
|
||||||
@ -120,7 +22,7 @@ fn main() {
|
|||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_sql::Builder::default()
|
tauri_plugin_sql::Builder::default()
|
||||||
.add_migrations(
|
.add_migrations(
|
||||||
"sqlite:lume_v2.db",
|
"sqlite:lume_v3.db",
|
||||||
vec![
|
vec![
|
||||||
Migration {
|
Migration {
|
||||||
version: 20230418013219,
|
version: 20230418013219,
|
||||||
@ -128,18 +30,6 @@ fn main() {
|
|||||||
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
|
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
|
||||||
kind: MigrationKind::Up,
|
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(),
|
.build(),
|
||||||
@ -160,10 +50,10 @@ fn main() {
|
|||||||
Some(vec![]),
|
Some(vec![]),
|
||||||
))
|
))
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
opengraph,
|
commands::opengraph,
|
||||||
secure_save,
|
commands::secure_save,
|
||||||
secure_load,
|
commands::secure_load,
|
||||||
secure_remove,
|
commands::secure_remove,
|
||||||
commands::show_in_folder,
|
commands::show_in_folder,
|
||||||
])
|
])
|
||||||
.run(ctx)
|
.run(ctx)
|
||||||
|
Loading…
Reference in New Issue
Block a user