feat: readd for you column
@ -45,15 +45,15 @@
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@tanstack/router-devtools": "^1.26.7",
|
||||
"@tanstack/router-vite-plugin": "^1.26.6",
|
||||
"@tanstack/router-vite-plugin": "^1.26.8",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.7",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-top-level-await": "^1.4.1",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
}
|
||||
|
BIN
apps/desktop2/public/anime.jpg
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
apps/desktop2/public/art.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
apps/desktop2/public/gaming.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
apps/desktop2/public/movie.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
apps/desktop2/public/music.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
apps/desktop2/public/nsfw.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
apps/desktop2/public/photography.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
apps/desktop2/public/technology.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
@ -28,7 +28,7 @@ const persister = createSyncStoragePersister({
|
||||
|
||||
const ark = new Ark();
|
||||
const platformName = await platform();
|
||||
const osLocale = (await locale()).slice(0, 2);
|
||||
const osLocale = await locale();
|
||||
|
||||
// Set up a Router instance
|
||||
const router = createRouter({
|
||||
@ -37,6 +37,8 @@ const router = createRouter({
|
||||
platform: platformName,
|
||||
locale: osLocale,
|
||||
settings: null,
|
||||
accounts: null,
|
||||
interests: null,
|
||||
ark,
|
||||
queryClient,
|
||||
},
|
||||
|
@ -19,7 +19,7 @@ const DEFAULT_COLUMNS: LumeColumn[] = [
|
||||
];
|
||||
|
||||
function Screen() {
|
||||
const search = Route.useSearch();
|
||||
const { account } = Route.useParams();
|
||||
const vlistRef = useRef<VListHandle>(null);
|
||||
|
||||
const [columns, setColumns] = useState(DEFAULT_COLUMNS);
|
||||
@ -139,8 +139,7 @@ function Screen() {
|
||||
<Col
|
||||
key={column.id}
|
||||
column={column}
|
||||
// @ts-ignore, yolo !!!
|
||||
account={search.acccount}
|
||||
account={account}
|
||||
isScroll={isScroll}
|
||||
/>
|
||||
))}
|
||||
|
@ -5,6 +5,16 @@ import { Accounts } from "@/components/accounts";
|
||||
|
||||
export const Route = createFileRoute("/$account")({
|
||||
component: App,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
const ark = context.ark;
|
||||
const settings = await ark.get_settings(params.account);
|
||||
const interests = await ark.get_interest(params.account);
|
||||
|
||||
return {
|
||||
settings,
|
||||
interests,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
import { type Ark } from "@lume/ark";
|
||||
import { type QueryClient } from "@tanstack/react-query";
|
||||
import { type Platform } from "@tauri-apps/plugin-os";
|
||||
import { Settings } from "@lume/types";
|
||||
import { Account, Interests, Settings } from "@lume/types";
|
||||
|
||||
interface RouterContext {
|
||||
ark: Ark;
|
||||
@ -15,6 +15,8 @@ interface RouterContext {
|
||||
platform: Platform;
|
||||
locale: string;
|
||||
settings: Settings;
|
||||
interests: Interests;
|
||||
accounts: Account[];
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
@ -221,12 +221,7 @@ function Screen() {
|
||||
</div>
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
|
||||
{reply_to && !quote ? (
|
||||
<div className="flex flex-col rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
||||
<h3 className="font-medium">Reply to:</h3>
|
||||
<MentionNote eventId={reply_to} />
|
||||
</div>
|
||||
) : null}
|
||||
{reply_to && !quote ? <MentionNote eventId={reply_to} /> : null}
|
||||
<div className="h-full w-full flex-1 overflow-hidden overflow-y-auto rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
||||
<Editable
|
||||
key={JSON.stringify(editorValue)}
|
||||
@ -235,7 +230,9 @@ function Screen() {
|
||||
autoCorrect="none"
|
||||
spellCheck={false}
|
||||
renderElement={(props) => <Element {...props} />}
|
||||
placeholder={t("editor.placeholder")}
|
||||
placeholder={
|
||||
reply_to ? "Type your reply..." : t("editor.placeholder")
|
||||
}
|
||||
className="focus:outline-none"
|
||||
/>
|
||||
{target && filters.length > 0 && (
|
||||
|
@ -2,9 +2,9 @@ import { useEvent } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { Box, Container, Note, User } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
import { ReplyList } from "./-components/replyList";
|
||||
import { Event } from "@lume/types";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
import { type Event } from "@lume/types";
|
||||
|
||||
export const Route = createLazyFileRoute("/events/$eventId")({
|
||||
component: Event,
|
||||
@ -29,14 +29,14 @@ function Event() {
|
||||
}
|
||||
|
||||
return (
|
||||
<WindowVirtualizer>
|
||||
<Container withDrag>
|
||||
<Box className="px-3 pt-3">
|
||||
<Container withDrag>
|
||||
<Box className="px-3 pt-3 scrollbar-none">
|
||||
<WindowVirtualizer>
|
||||
<MainNote data={data} />
|
||||
{data ? <ReplyList eventId={eventId} /> : null}
|
||||
</Box>
|
||||
</Container>
|
||||
</WindowVirtualizer>
|
||||
</WindowVirtualizer>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,30 +1,60 @@
|
||||
import { RepostNote } from "@/components/repost";
|
||||
import { Suggest } from "@/components/suggest";
|
||||
import { TextNote } from "@/components/text";
|
||||
import { useEvents } from "@lume/ark";
|
||||
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||
import { Event, Kind } from "@lume/types";
|
||||
import { Column } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/foryou")({
|
||||
export const Route = createFileRoute("/foryou")({
|
||||
beforeLoad: async ({ search, context }) => {
|
||||
const ark = context.ark;
|
||||
// @ts-ignore, useless !!!
|
||||
const interests = await ark.get_interest(search.account);
|
||||
|
||||
if (!interests) {
|
||||
throw redirect({
|
||||
to: "/interests",
|
||||
replace: false,
|
||||
search,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
interests,
|
||||
};
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { ark, interests } = Route.useRouteContext();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
hasNextPage,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useEvents("local", account);
|
||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["foryou", account],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const events = await ark.get_events_from_interests(
|
||||
interests.hashtags,
|
||||
20,
|
||||
pageParam,
|
||||
true,
|
||||
);
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage?.at(-1);
|
||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
||||
},
|
||||
select: (data) => data?.pages.flatMap((page) => page),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const renderItem = (event: Event) => {
|
||||
if (!event) return;
|
||||
@ -40,17 +70,23 @@ export function Screen() {
|
||||
<Column.Root>
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
{isLoading ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
<button type="button" className="size-5" disabled>
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-100 p-5 dark:bg-neutral-900">
|
||||
<InfoIcon className="size-6" />
|
||||
<div>
|
||||
<p className="leading-tight">{t("emptyFeedTitle")}</p>
|
||||
<p className="leading-tight">{t("emptyFeedSubtitle")}</p>
|
||||
<p className="font-medium leading-tight">
|
||||
{t("global.emptyFeedTitle")}
|
||||
</p>
|
||||
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
{t("global.emptyFeedSubtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Suggest />
|
||||
@ -60,12 +96,13 @@ export function Screen() {
|
||||
{data.map((item) => renderItem(item))}
|
||||
</Virtualizer>
|
||||
)}
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
{hasNextPage ? (
|
||||
|
||||
{data?.length && hasNextPage ? (
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
disabled={isFetchingNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
@ -77,8 +114,8 @@ export function Screen() {
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Column.Content>
|
||||
</Column.Root>
|
||||
);
|
@ -1,10 +1,10 @@
|
||||
import { RepostNote } from "@/components/repost";
|
||||
import { Suggest } from "@/components/suggest";
|
||||
import { TextNote } from "@/components/text";
|
||||
import { useEvents } from "@lume/ark";
|
||||
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||
import { Event, Kind } from "@lume/types";
|
||||
import { Column } from "@lume/ui";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtualizer } from "virtua";
|
||||
@ -16,15 +16,23 @@ export const Route = createLazyFileRoute("/global")({
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { ark } = Route.useRouteContext();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
hasNextPage,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useEvents("global", account);
|
||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["global", account],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const events = await ark.get_events(20, pageParam, undefined, true);
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage?.at(-1);
|
||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
||||
},
|
||||
select: (data) => data?.pages.flatMap((page) => page),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const renderItem = (event: Event) => {
|
||||
if (!event) return;
|
||||
@ -40,17 +48,23 @@ export function Screen() {
|
||||
<Column.Root>
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
{isLoading ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
<button type="button" className="size-5" disabled>
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-100 p-5 dark:bg-neutral-900">
|
||||
<InfoIcon className="size-6" />
|
||||
<div>
|
||||
<p className="leading-tight">{t("emptyFeedTitle")}</p>
|
||||
<p className="leading-tight">{t("emptyFeedSubtitle")}</p>
|
||||
<p className="font-medium leading-tight">
|
||||
{t("global.emptyFeedTitle")}
|
||||
</p>
|
||||
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
{t("global.emptyFeedSubtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Suggest />
|
||||
@ -60,12 +74,13 @@ export function Screen() {
|
||||
{data.map((item) => renderItem(item))}
|
||||
</Virtualizer>
|
||||
)}
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
{hasNextPage ? (
|
||||
|
||||
{data?.length && hasNextPage ? (
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
disabled={isFetchingNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
@ -77,8 +92,8 @@ export function Screen() {
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Column.Content>
|
||||
</Column.Root>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
beforeLoad: async ({ search, context }) => {
|
||||
beforeLoad: async ({ context }) => {
|
||||
const ark = context.ark;
|
||||
const accounts = await ark.get_all_accounts();
|
||||
|
||||
@ -18,15 +18,8 @@ export const Route = createFileRoute("/")({
|
||||
});
|
||||
// Only 1 account, skip account selection screen
|
||||
case 1:
|
||||
// @ts-ignore, totally fine !!!
|
||||
if (search.manually) return;
|
||||
|
||||
const account = accounts[0].npub;
|
||||
const loadedAccount = await ark.load_selected_account(account);
|
||||
const settings = await ark.get_settings(account);
|
||||
|
||||
// Update settings
|
||||
context.settings = settings;
|
||||
|
||||
if (loadedAccount) {
|
||||
throw redirect({
|
||||
@ -37,7 +30,7 @@ export const Route = createFileRoute("/")({
|
||||
}
|
||||
// Account selection
|
||||
default:
|
||||
return;
|
||||
return { accounts };
|
||||
}
|
||||
},
|
||||
component: Screen,
|
||||
@ -51,11 +44,12 @@ function Screen() {
|
||||
|
||||
const select = async (npub: string) => {
|
||||
setLoading(true);
|
||||
const loadAccount = await context.ark.load_selected_account(npub);
|
||||
context.settings = await context.ark.get_settings(npub);
|
||||
|
||||
const ark = context.ark;
|
||||
const loadAccount = await ark.load_selected_account(npub);
|
||||
|
||||
if (loadAccount) {
|
||||
navigate({
|
||||
return navigate({
|
||||
to: "/$account/home",
|
||||
params: { account: npub },
|
||||
replace: true,
|
||||
@ -83,7 +77,7 @@ function Screen() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{context.ark.accounts.map((account) => (
|
||||
{context.accounts.map((account) => (
|
||||
<button
|
||||
type="button"
|
||||
key={account.npub}
|
||||
|
115
apps/desktop2/src/routes/interests.lazy.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { Column } from "@lume/ui";
|
||||
import { TOPICS, cn } from "@lume/utils";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createLazyFileRoute("/interests")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [hashtags, setHashtags] = useState<string[]>([]);
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
|
||||
const context = Route.useRouteContext();
|
||||
const search = Route.useSearch();
|
||||
|
||||
const toggleHashtag = (item: string) => {
|
||||
const arr = hashtags.includes(item)
|
||||
? hashtags.filter((i) => i !== item)
|
||||
: [...hashtags, item];
|
||||
setHashtags(arr);
|
||||
};
|
||||
|
||||
const toggleAll = (item: string[]) => {
|
||||
const sets = new Set([...hashtags, ...item]);
|
||||
setHashtags([...sets]);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (isDone) {
|
||||
return history.back();
|
||||
}
|
||||
|
||||
const ark = context.ark;
|
||||
const eventId = await ark.set_interest(undefined, undefined, hashtags);
|
||||
|
||||
if (eventId) {
|
||||
setIsDone(true);
|
||||
toast.success("Interest has been updated successfully.");
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header id={search.id} name={search.name} />
|
||||
<Column.Content>
|
||||
<div className="sticky left-0 top-0 flex h-16 w-full items-center justify-between border-b border-neutral-100 bg-white px-3 dark:border-neutral-900 dark:bg-black">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<h3 className="font-semibold">Interests</h3>
|
||||
<p className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
Pick things you'd like to see.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
className="inline-flex h-8 w-20 items-center justify-center rounded-full bg-blue-500 px-2 text-sm font-medium text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isDone ? t("global.back") : t("global.update")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex w-full flex-col p-3">
|
||||
<div className="flex flex-col gap-8">
|
||||
{TOPICS.map((topic) => (
|
||||
<div key={topic.title} className="flex flex-col gap-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="inline-flex items-center gap-2.5">
|
||||
<img
|
||||
src={topic.icon}
|
||||
alt={topic.title}
|
||||
className="size-8 rounded-lg object-cover"
|
||||
/>
|
||||
<h3 className="text-lg font-semibold">{topic.title}</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAll(topic.content)}
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
{t("interests.followAll")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{topic.content.map((hashtag) => (
|
||||
<button
|
||||
key={hashtag}
|
||||
type="button"
|
||||
onClick={() => toggleHashtag(hashtag)}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-full border border-transparent bg-neutral-100 px-2 py-1 text-sm font-medium dark:bg-neutral-900",
|
||||
hashtags.includes(hashtag)
|
||||
? "border-blue-500 text-blue-500"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{hashtag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Column.Content>
|
||||
</Column.Root>
|
||||
);
|
||||
}
|
@ -23,13 +23,12 @@ export function Screen() {
|
||||
queryKey: ["local", account],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const events = await ark.get_events("local", 20, pageParam, true);
|
||||
const events = await ark.get_events(20, pageParam);
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage?.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
||||
},
|
||||
select: (data) => data?.pages.flatMap((page) => page),
|
||||
refetchOnWindowFocus: false,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ArrowRightIcon, ZapIcon } from "@lume/icons";
|
||||
import { ZapIcon } from "@lume/icons";
|
||||
import { Container } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
@ -15,14 +15,11 @@ function Screen() {
|
||||
|
||||
const save = async () => {
|
||||
const nwc = await ark.set_nwc(uri);
|
||||
|
||||
if (nwc) {
|
||||
setIsDone(true);
|
||||
}
|
||||
setIsDone(nwc);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container withDrag>
|
||||
<Container withDrag withNavigate={false}>
|
||||
<div className="h-full w-full flex-1 px-5">
|
||||
{!isDone ? (
|
||||
<>
|
||||
@ -44,17 +41,15 @@ function Screen() {
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
placeholder="nostrconnect://"
|
||||
className="h-24 w-full resize-none rounded-lg border-transparent bg-white placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-black dark:focus:ring-blue-900"
|
||||
className="h-24 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={save}
|
||||
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
||||
>
|
||||
<div className="size-5" />
|
||||
<div>Save & Connect</div>
|
||||
<ArrowRightIcon className="size-5" />
|
||||
Save & Connect
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
import { User } from "@lume/ui";
|
||||
import { Box, Container, User } from "@lume/ui";
|
||||
import { EventList } from "./-components/eventList";
|
||||
|
||||
export const Route = createLazyFileRoute("/users/$pubkey")({
|
||||
@ -11,36 +11,33 @@ function Screen() {
|
||||
const { pubkey } = Route.useParams();
|
||||
|
||||
return (
|
||||
<WindowVirtualizer>
|
||||
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||
<div className="h-full w-full overflow-hidden overflow-y-auto rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
||||
<User.Provider pubkey={pubkey}>
|
||||
<User.Root>
|
||||
<User.Cover className="h-44 w-full object-cover" />
|
||||
<div className="relative -mt-8 flex flex-col gap-4 px-5">
|
||||
<User.Avatar className="size-14 rounded-full" />
|
||||
<div className="inline-flex items-start justify-between">
|
||||
<div>
|
||||
<User.Name className="font-semibold leading-tight" />
|
||||
<User.NIP05 className="text-sm leading-tight text-neutral-600 dark:text-neutral-400" />
|
||||
</div>
|
||||
<User.Button className="h-9 w-24 rounded-full bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
|
||||
</div>
|
||||
<User.About />
|
||||
<Container withDrag>
|
||||
<Box className="px-0 scrollbar-none">
|
||||
<WindowVirtualizer>
|
||||
<User.Provider pubkey={pubkey}>
|
||||
<User.Root>
|
||||
<User.Cover className="h-44 w-full object-cover" />
|
||||
<div className="relative -mt-8 flex flex-col gap-4 px-3">
|
||||
<User.Avatar className="size-14 rounded-full" />
|
||||
<div className="inline-flex items-start justify-between">
|
||||
<div>
|
||||
<User.Name className="font-semibold leading-tight" />
|
||||
<User.NIP05 className="text-sm leading-tight text-neutral-600 dark:text-neutral-400" />
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="mt-4 px-5">
|
||||
<h3 className="mb-4 text-lg font-semibold">Notes</h3>
|
||||
<EventList id={pubkey} />
|
||||
<User.Button className="h-9 w-24 rounded-full bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
|
||||
</div>
|
||||
<User.About />
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="mt-4">
|
||||
<div className="px-3">
|
||||
<h3 className="text-lg font-semibold">Latest notes</h3>
|
||||
</div>
|
||||
<EventList id={pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WindowVirtualizer>
|
||||
</WindowVirtualizer>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
"@astrojs/check": "^0.5.10",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@fontsource/geist-mono": "^5.0.2",
|
||||
"astro": "^4.5.14",
|
||||
"astro": "^4.5.15",
|
||||
"astro-seo-meta": "^4.1.0",
|
||||
"astro-seo-schema": "^4.0.0",
|
||||
"schema-dts": "^1.1.2",
|
||||
|
@ -11,8 +11,8 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.6.3",
|
||||
"@tauri-apps/cli": "2.0.0-beta.9",
|
||||
"@biomejs/biome": "^1.6.4",
|
||||
"@tauri-apps/cli": "2.0.0-beta.12",
|
||||
"turbo": "^1.13.2"
|
||||
},
|
||||
"packageManager": "pnpm@8.9.0",
|
||||
@ -20,7 +20,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.0.0-beta.5",
|
||||
"@tauri-apps/api": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.0",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-beta.2",
|
||||
@ -30,7 +30,6 @@
|
||||
"@tauri-apps/plugin-os": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-process": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-sql": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-upload": "2.0.0-beta.2"
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import type {
|
||||
Contact,
|
||||
Event,
|
||||
EventWithReplies,
|
||||
Interests,
|
||||
Keys,
|
||||
Metadata,
|
||||
Settings,
|
||||
@ -14,10 +15,10 @@ import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { generateContentTags } from "@lume/utils";
|
||||
|
||||
export class Ark {
|
||||
public accounts: Account[];
|
||||
public windows: WebviewWindow[];
|
||||
|
||||
constructor() {
|
||||
this.accounts = [];
|
||||
this.windows = [];
|
||||
}
|
||||
|
||||
public async get_all_accounts() {
|
||||
@ -29,8 +30,6 @@ export class Ark {
|
||||
for (const item of cmd) {
|
||||
accounts.push({ npub: item.replace(".npub", "") });
|
||||
}
|
||||
|
||||
this.accounts = accounts;
|
||||
return accounts;
|
||||
}
|
||||
} catch {
|
||||
@ -127,21 +126,24 @@ export class Ark {
|
||||
}
|
||||
|
||||
public async get_events(
|
||||
type: "local" | "global",
|
||||
limit: number,
|
||||
asOf?: number,
|
||||
dedup?: boolean,
|
||||
contacts?: string[],
|
||||
global?: boolean,
|
||||
) {
|
||||
try {
|
||||
let until: string = undefined;
|
||||
if (asOf && asOf > 0) until = asOf.toString();
|
||||
|
||||
const dedup = true;
|
||||
const seenIds = new Set<string>();
|
||||
const dedupQueue = new Set<string>();
|
||||
|
||||
const nostrEvents: Event[] = await invoke(`get_${type}_events`, {
|
||||
const nostrEvents: Event[] = await invoke("get_events", {
|
||||
limit,
|
||||
until,
|
||||
contacts,
|
||||
global,
|
||||
});
|
||||
|
||||
if (dedup) {
|
||||
@ -156,7 +158,6 @@ export class Ark {
|
||||
dedupQueue.add(event.id);
|
||||
break;
|
||||
}
|
||||
|
||||
seenIds.add(tag);
|
||||
}
|
||||
}
|
||||
@ -173,6 +174,51 @@ export class Ark {
|
||||
}
|
||||
}
|
||||
|
||||
public async get_events_from_interests(
|
||||
hashtags: string[],
|
||||
limit: number,
|
||||
asOf?: number,
|
||||
global?: boolean,
|
||||
) {
|
||||
let until: string = undefined;
|
||||
if (asOf && asOf > 0) until = asOf.toString();
|
||||
|
||||
const dedup = true;
|
||||
const seenIds = new Set<string>();
|
||||
const dedupQueue = new Set<string>();
|
||||
|
||||
const nostrEvents: Event[] = await invoke("get_events_from_interests", {
|
||||
hashtags,
|
||||
limit,
|
||||
until,
|
||||
global,
|
||||
});
|
||||
|
||||
if (dedup) {
|
||||
for (const event of nostrEvents) {
|
||||
const tags = event.tags
|
||||
.filter((el) => el[0] === "e")
|
||||
?.map((item) => item[1]);
|
||||
|
||||
if (tags.length) {
|
||||
for (const tag of tags) {
|
||||
if (seenIds.has(tag)) {
|
||||
dedupQueue.add(event.id);
|
||||
break;
|
||||
}
|
||||
seenIds.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nostrEvents
|
||||
.filter((event) => !dedupQueue.has(event.id))
|
||||
.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
public async publish(content: string, reply_to?: string, quote?: boolean) {
|
||||
try {
|
||||
const g = await generateContentTags(content);
|
||||
@ -548,89 +594,159 @@ export class Ark {
|
||||
}
|
||||
}
|
||||
|
||||
public async get_interest(id: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("get_interest", { id });
|
||||
if (!cmd) return null;
|
||||
if (!cmd.length) return null;
|
||||
|
||||
const interests: Interests = JSON.parse(cmd);
|
||||
return interests;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async set_interest(
|
||||
words: string[],
|
||||
users: string[],
|
||||
hashtags: string[],
|
||||
) {
|
||||
try {
|
||||
const interests: Interests = {
|
||||
words: words ?? [],
|
||||
users: users ?? [],
|
||||
hashtags: hashtags ?? [],
|
||||
};
|
||||
const cmd: string = await invoke("set_interest", {
|
||||
content: JSON.stringify(interests),
|
||||
});
|
||||
return cmd;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public open_thread(id: string) {
|
||||
return new WebviewWindow(`event-${id}`, {
|
||||
title: "Thread",
|
||||
url: `/events/${id}`,
|
||||
minWidth: 500,
|
||||
width: 500,
|
||||
height: 800,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
center: false,
|
||||
});
|
||||
try {
|
||||
const window = new WebviewWindow(`event-${id}`, {
|
||||
title: "Thread",
|
||||
url: `/events/${id}`,
|
||||
minWidth: 500,
|
||||
minHeight: 800,
|
||||
width: 500,
|
||||
height: 800,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
center: false,
|
||||
});
|
||||
|
||||
this.windows.push(window);
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public open_profile(pubkey: string) {
|
||||
return new WebviewWindow(`user-${pubkey}`, {
|
||||
title: "Profile",
|
||||
url: `/users/${pubkey}`,
|
||||
minWidth: 500,
|
||||
width: 500,
|
||||
height: 800,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
try {
|
||||
const window = new WebviewWindow(`user-${pubkey}`, {
|
||||
title: "Profile",
|
||||
url: `/users/${pubkey}`,
|
||||
minWidth: 500,
|
||||
minHeight: 800,
|
||||
width: 500,
|
||||
height: 800,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
|
||||
this.windows.push(window);
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public open_editor(reply_to?: string, quote: boolean = false) {
|
||||
let url: string;
|
||||
try {
|
||||
let url: string;
|
||||
|
||||
if (reply_to) {
|
||||
url = `/editor?reply_to=${reply_to}"e=${quote}`;
|
||||
} else {
|
||||
url = "/editor";
|
||||
if (reply_to) {
|
||||
url = `/editor?reply_to=${reply_to}"e=${quote}`;
|
||||
} else {
|
||||
url = "/editor";
|
||||
}
|
||||
|
||||
const window = new WebviewWindow(`editor-${reply_to ? reply_to : 0}`, {
|
||||
title: "Editor",
|
||||
url,
|
||||
minWidth: 500,
|
||||
minHeight: 400,
|
||||
width: 600,
|
||||
height: 400,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
|
||||
this.windows.push(window);
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
|
||||
return new WebviewWindow("editor", {
|
||||
title: "Editor",
|
||||
url,
|
||||
minWidth: 500,
|
||||
minHeight: 400,
|
||||
width: 600,
|
||||
height: 400,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
fileDropEnabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
public open_nwc() {
|
||||
return new WebviewWindow("nwc", {
|
||||
title: "Nostr Wallet Connect",
|
||||
url: "/nwc",
|
||||
minWidth: 400,
|
||||
width: 400,
|
||||
height: 600,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
fileDropEnabled: true,
|
||||
});
|
||||
try {
|
||||
const window = new WebviewWindow("nwc", {
|
||||
title: "Nostr Wallet Connect",
|
||||
url: "/nwc",
|
||||
minWidth: 400,
|
||||
minHeight: 600,
|
||||
width: 400,
|
||||
height: 600,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
|
||||
this.windows.push(window);
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public open_zap(id: string, pubkey: string, account: string) {
|
||||
return new WebviewWindow(`zap-${id}`, {
|
||||
title: "Nostr Wallet Connect",
|
||||
url: `/zap/${id}?pubkey=${pubkey}&account=${account}`,
|
||||
minWidth: 400,
|
||||
width: 400,
|
||||
height: 500,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
fileDropEnabled: true,
|
||||
});
|
||||
try {
|
||||
const window = new WebviewWindow(`zap-${id}`, {
|
||||
title: "Zap",
|
||||
url: `/zap/${id}?pubkey=${pubkey}&account=${account}`,
|
||||
minWidth: 400,
|
||||
minHeight: 500,
|
||||
width: 400,
|
||||
height: 500,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
|
||||
this.windows.push(window);
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public open_settings() {
|
||||
return new WebviewWindow("settings", {
|
||||
title: "Settings",
|
||||
url: "/settings",
|
||||
minWidth: 600,
|
||||
width: 800,
|
||||
height: 500,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
fileDropEnabled: true,
|
||||
});
|
||||
try {
|
||||
const window = new WebviewWindow("settings", {
|
||||
title: "Settings",
|
||||
url: "/settings",
|
||||
minWidth: 600,
|
||||
minHeight: 500,
|
||||
width: 800,
|
||||
height: 500,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
|
||||
this.windows.push(window);
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,12 @@ export function ColumnContent({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex-1 overflow-y-auto overflow-x-hidden", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto overflow-x-hidden scrollbar-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -34,6 +34,7 @@ export function NoteUser({ className }: { className?: string }) {
|
||||
<HoverCard.Content
|
||||
className="w-[300px] rounded-xl bg-black p-3 data-[side=bottom]:animate-slideUpAndFade data-[state=open]:transition-all dark:bg-white dark:shadow-none"
|
||||
sideOffset={5}
|
||||
side="right"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<User.Avatar className="size-11 rounded-lg object-cover" />
|
||||
|
@ -22,7 +22,7 @@
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
|
796
pnpm-lock.yaml
@ -6,12 +6,12 @@
|
||||
"windows": [
|
||||
"main",
|
||||
"splash",
|
||||
"editor",
|
||||
"settings",
|
||||
"nwc",
|
||||
"zap-*",
|
||||
"event-*",
|
||||
"user-*",
|
||||
"editor-*",
|
||||
"column-*"
|
||||
],
|
||||
"permissions": [
|
||||
|
@ -1 +1 @@
|
||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","nwc","zap-*","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","window:allow-create","window:allow-close","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
|
||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","nwc","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","window:allow-create","window:allow-close","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
|
@ -126,8 +126,8 @@ fn main() {
|
||||
nostr::metadata::zap_event,
|
||||
nostr::event::get_event,
|
||||
nostr::event::get_events_from,
|
||||
nostr::event::get_local_events,
|
||||
nostr::event::get_global_events,
|
||||
nostr::event::get_events,
|
||||
nostr::event::get_events_from_interests,
|
||||
nostr::event::get_event_thread,
|
||||
nostr::event::publish,
|
||||
nostr::event::repost,
|
||||
|
@ -72,66 +72,120 @@ pub async fn get_events_from(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_local_events(
|
||||
pub async fn get_events(
|
||||
limit: usize,
|
||||
until: Option<&str>,
|
||||
contacts: Option<Vec<&str>>,
|
||||
global: Option<bool>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<Event>, String> {
|
||||
let client = &state.client;
|
||||
let f_until = match until {
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).unwrap(),
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let contact_list = client
|
||||
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
||||
.await;
|
||||
|
||||
if let Ok(authors) = contact_list {
|
||||
if authors.len() == 0 {
|
||||
return Err("Get text event failed".into());
|
||||
let authors = match contacts {
|
||||
Some(val) => {
|
||||
let c: Vec<PublicKey> = val
|
||||
.into_iter()
|
||||
.map(|key| PublicKey::from_str(key).unwrap())
|
||||
.collect();
|
||||
Some(c)
|
||||
}
|
||||
|
||||
let filter = Filter::new()
|
||||
None => match global {
|
||||
Some(val) => match val {
|
||||
true => None,
|
||||
false => {
|
||||
match client
|
||||
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
||||
.await
|
||||
{
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
match client
|
||||
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
||||
.await
|
||||
{
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
let filter = match authors {
|
||||
Some(val) => Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(authors)
|
||||
.authors(val)
|
||||
.limit(limit)
|
||||
.until(f_until);
|
||||
.until(as_of),
|
||||
None => Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(limit)
|
||||
.until(as_of),
|
||||
};
|
||||
|
||||
if let Ok(events) = client
|
||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||
.await
|
||||
{
|
||||
Ok(events)
|
||||
} else {
|
||||
Err("Get text event failed".into())
|
||||
}
|
||||
if let Ok(events) = client
|
||||
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
|
||||
.await
|
||||
{
|
||||
println!("total events: {}", events.len());
|
||||
Ok(events)
|
||||
} else {
|
||||
Err("Get contact list failed".into())
|
||||
Err("Get text event failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_global_events(
|
||||
pub async fn get_events_from_interests(
|
||||
hashtags: Vec<&str>,
|
||||
limit: usize,
|
||||
until: Option<&str>,
|
||||
global: Option<bool>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<Event>, String> {
|
||||
let client = &state.client;
|
||||
let f_until = match until {
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).unwrap(),
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(limit)
|
||||
.until(f_until);
|
||||
let authors = match global {
|
||||
Some(val) => match val {
|
||||
true => None,
|
||||
false => {
|
||||
match client
|
||||
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
||||
.await
|
||||
{
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let filter = match authors {
|
||||
Some(val) => Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(val)
|
||||
.limit(limit)
|
||||
.until(as_of)
|
||||
.hashtags(hashtags),
|
||||
None => Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(limit)
|
||||
.until(as_of)
|
||||
.hashtags(hashtags),
|
||||
};
|
||||
|
||||
if let Ok(events) = client
|
||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
|
||||
.await
|
||||
{
|
||||
println!("total events: {}", events.len());
|
||||
Ok(events)
|
||||
} else {
|
||||
Err("Get text event failed".into())
|
||||
|
@ -44,14 +44,24 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||
}
|
||||
}
|
||||
"editor" => {
|
||||
let _ = WebviewWindowBuilder::new(app, "editor", WebviewUrl::App(PathBuf::from("editor")))
|
||||
.title("Editor")
|
||||
.min_inner_size(500., 400.)
|
||||
.inner_size(600., 400.)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.build()
|
||||
.unwrap();
|
||||
if let Some(window) = app.get_window("editor-0") {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
};
|
||||
} else {
|
||||
let _ =
|
||||
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
||||
.title("Editor")
|
||||
.min_inner_size(500., 400.)
|
||||
.inner_size(600., 400.)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
"about" => {
|
||||
app.shell().open("https://lume.nu", None).unwrap();
|
||||
|