feat: readd for you column

This commit is contained in:
reya 2024-04-04 13:47:15 +07:00
parent 174b28f1a7
commit 999073f84c
34 changed files with 984 additions and 647 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">
<Box className="px-3 pt-3 scrollbar-none">
<WindowVirtualizer>
<MainNote data={data} />
{data ? <ReplyList eventId={eventId} /> : null}
</WindowVirtualizer>
</Box>
</Container>
</WindowVirtualizer>
);
}

View File

@ -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">
<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>
)}
{data?.length && hasNextPage ? (
<div className="flex h-20 items-center justify-center">
{hasNextPage ? (
<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>
) : null}
</Column.Content>
</Column.Root>
);

View File

@ -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">
<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>
)}
{data?.length && hasNextPage ? (
<div className="flex h-20 items-center justify-center">
{hasNextPage ? (
<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>
) : null}
</Column.Content>
</Column.Root>
);

View File

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

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

View File

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

View File

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

View File

@ -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,16 +11,13 @@ function Screen() {
const { pubkey } = Route.useParams();
return (
<Container withDrag>
<Box className="px-0 scrollbar-none">
<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">
<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>
@ -33,14 +30,14 @@ function Screen() {
</div>
</User.Root>
</User.Provider>
<div className="mt-4 px-5">
<h3 className="mb-4 text-lg font-semibold">Notes</h3>
<div className="mt-4">
<div className="px-3">
<h3 className="text-lg font-semibold">Latest notes</h3>
</div>
<EventList id={pubkey} />
</div>
</div>
</div>
</div>
</div>
</WindowVirtualizer>
</Box>
</Container>
);
}

View File

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

View File

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

View File

@ -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,32 +594,80 @@ 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}`, {
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}`, {
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) {
try {
let url: string;
if (reply_to) {
@ -582,7 +676,7 @@ export class Ark {
url = "/editor";
}
return new WebviewWindow("editor", {
const window = new WebviewWindow(`editor-${reply_to ? reply_to : 0}`, {
title: "Editor",
url,
minWidth: 500,
@ -591,46 +685,68 @@ export class Ark {
height: 400,
hiddenTitle: true,
titleBarStyle: "overlay",
fileDropEnabled: true,
});
this.windows.push(window);
} catch (e) {
throw new Error(String(e));
}
}
public open_nwc() {
return new WebviewWindow("nwc", {
try {
const window = new WebviewWindow("nwc", {
title: "Nostr Wallet Connect",
url: "/nwc",
minWidth: 400,
minHeight: 600,
width: 400,
height: 600,
hiddenTitle: true,
titleBarStyle: "overlay",
fileDropEnabled: true,
});
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",
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",
fileDropEnabled: true,
});
this.windows.push(window);
} catch (e) {
throw new Error(String(e));
}
}
public open_settings() {
return new WebviewWindow("settings", {
try {
const window = new WebviewWindow("settings", {
title: "Settings",
url: "/settings",
minWidth: 600,
minHeight: 500,
width: 800,
height: 500,
hiddenTitle: true,
titleBarStyle: "overlay",
fileDropEnabled: true,
});
this.windows.push(window);
} catch (e) {
throw new Error(String(e));
}
}
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,12 @@
"windows": [
"main",
"splash",
"editor",
"settings",
"nwc",
"zap-*",
"event-*",
"user-*",
"editor-*",
"column-*"
],
"permissions": [

View File

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

View File

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

View File

@ -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()
.kinds(vec![Kind::TextNote, Kind::Repost])
.authors(authors)
.limit(limit)
.until(f_until);
if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
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(val)
.limit(limit)
.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(15)))
.await
{
println!("total events: {}", events.len());
Ok(events)
} else {
Err("Get text event failed".into())
}
} else {
Err("Get contact list 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);
if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
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(15)))
.await
{
println!("total events: {}", events.len());
Ok(events)
} else {
Err("Get text event failed".into())

View File

@ -44,7 +44,16 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
}
}
"editor" => {
let _ = WebviewWindowBuilder::new(app, "editor", WebviewUrl::App(PathBuf::from("editor")))
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.)
@ -53,6 +62,7 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
.build()
.unwrap();
}
}
"about" => {
app.shell().open("https://lume.nu", None).unwrap();
}