refactor: add event and user routes to default ui

This commit is contained in:
reya 2024-01-08 09:30:04 +07:00
parent c04ca3a1ab
commit aa80301778
36 changed files with 527 additions and 1765 deletions

View File

@ -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,
}); });

View File

@ -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>

View File

@ -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();

View File

@ -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>
);
}

View File

@ -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}`;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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}`;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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}`;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
); );
} }

View File

@ -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 (

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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}`;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
); );
} }

View File

@ -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 (

View File

@ -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>
);
}

View File

@ -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);

View File

@ -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:^",

View File

@ -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";

View 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>
);
}

View 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>
);
}

View File

@ -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
View File

@ -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",
] ]

View File

@ -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" }

View File

@ -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)
);

View File

@ -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
);

View File

@ -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;

View File

@ -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(())
}

View File

@ -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)