feat: add nstore

This commit is contained in:
2024-04-07 15:11:20 +07:00
parent 999073f84c
commit 420be77b5c
75 changed files with 410 additions and 349 deletions

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from "react";
import { getCurrent } from "@tauri-apps/api/window";
import { LumeColumn } from "@lume/types";
import { invoke } from "@tauri-apps/api/core";
import { LoaderIcon } from "@lume/icons";
export function Col({
column,
@@ -18,10 +19,10 @@ export function Col({
const createWebview = async () => {
const rect = container.current.getBoundingClientRect();
const label = `column-${column.id}`;
const label = `column-${column.label}`;
const url =
column.content +
`?account=${account}&id=${column.id}&name=${column.name}`;
`?account=${account}&label=${column.label}&name=${column.name}`;
// create new webview
webview.current = await invoke("create_column", {
@@ -71,5 +72,14 @@ export function Col({
};
}, []);
return <div ref={container} className="h-full w-[440px] shrink-0 p-2" />;
return (
<div
ref={container}
className="h-full w-[440px] shrink-0 p-2 flex items-center justify-center"
>
<button type="button" disabled>
<LoaderIcon className="size-5 animate-spin" />
</button>
</div>
);
}

View File

@@ -4,27 +4,38 @@ import { LoaderIcon } from "@lume/icons";
import { EventColumns, LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { UnlistenFn } from "@tauri-apps/api/event";
import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { useEffect, useRef, useState } from "react";
import { VList, VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({
component: Screen,
pendingComponent: Pending,
});
beforeLoad: async ({ context }) => {
const ark = context.ark;
const resourcePath = await resolveResource("resources/system_columns.json");
const systemColumns: LumeColumn[] = JSON.parse(
await readTextFile(resourcePath),
);
const userColumns = await ark.get_columns();
const DEFAULT_COLUMNS: LumeColumn[] = [
{ id: 10001, name: "Newsfeed", content: "/newsfeed" },
{ id: 10000, name: "Open Lume Store", content: "/open" },
];
return {
storedColumns: !userColumns.length ? systemColumns : userColumns,
};
},
});
function Screen() {
const { account } = Route.useParams();
const vlistRef = useRef<VListHandle>(null);
const { ark, storedColumns } = Route.useRouteContext();
const [columns, setColumns] = useState(DEFAULT_COLUMNS);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [isScroll, setIsScroll] = useState(false);
const [columns, setColumns] = useState(storedColumns);
const vlistRef = useRef<VListHandle>(null);
const goLeft = () => {
const prevIndex = Math.max(selectedIndex - 1, 0);
@@ -43,41 +54,43 @@ function Screen() {
};
const add = (column: LumeColumn) => {
const existed = columns.find((item) => item.id === column.id);
const existed = columns.find((item) => item.label === column.label);
if (!existed) {
let lastColIndex: number;
const openColIndex = columns.findIndex((item) => item.id === 10000);
const storeColIndex = columns.findIndex((item) => item.id === 9999);
if (storeColIndex) {
lastColIndex = storeColIndex;
} else {
lastColIndex = openColIndex;
}
const lastColIndex = columns.findIndex((item) => item.label === "open");
const newColumns = [
...columns.slice(0, lastColIndex),
column,
...columns.slice(lastColIndex),
];
// update state & scroll to new column
// update state
setColumns(newColumns);
setSelectedIndex(newColumns.length - 1);
vlistRef.current.scrollToIndex(newColumns.length - 1, {
align: "center",
});
}
};
const remove = (id: number) => {
setColumns((prev) => prev.filter((t) => t.id !== id));
setSelectedIndex(columns.length);
vlistRef.current.scrollToIndex(columns.length, {
// save state
ark.set_columns(newColumns);
}
// scroll to new column
vlistRef.current.scrollToIndex(columns.length - 1, {
align: "center",
});
};
const remove = (label: string) => {
const newColumns = columns.filter((t) => t.label !== label);
// update state
setColumns(newColumns);
setSelectedIndex(newColumns.length - 1);
vlistRef.current.scrollToIndex(newColumns.length - 1, {
align: "center",
});
// save state
ark.set_columns(newColumns);
};
useEffect(() => {
let unlisten: UnlistenFn = undefined;
@@ -86,7 +99,7 @@ function Screen() {
if (!unlisten) {
unlisten = await mainWindow.listen<EventColumns>("columns", (data) => {
if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.id);
if (data.payload.type === "remove") remove(data.payload.label);
});
}
};
@@ -98,7 +111,7 @@ function Screen() {
return () => {
if (unlisten) {
unlisten();
unlisten = null;
unlisten = undefined;
}
};
}, []);
@@ -137,7 +150,7 @@ function Screen() {
>
{columns.map((column) => (
<Col
key={column.id}
key={column.label}
column={column}
account={account}
isScroll={isScroll}

View File

@@ -5,16 +5,6 @@ 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

@@ -17,7 +17,6 @@ export const Route = createLazyFileRoute("/auth/settings")({
function Screen() {
const navigate = useNavigate();
// @ts-ignore, magic!!!
const { account } = Route.useSearch();
const { t } = useTranslation();
const { ark } = Route.useRouteContext();
@@ -64,7 +63,7 @@ function Screen() {
useEffect(() => {
async function loadSettings() {
const permissionGranted = await isPermissionGranted(); // get notification permission
const settings = await ark.get_settings(account);
const settings = await ark.get_settings();
setSettings({ ...settings, notification: permissionGranted });
}
@@ -146,7 +145,7 @@ function Screen() {
<button
type="button"
onClick={submit}
className="inline-flex h-11 flex-1 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{t("global.continue")}
</button>

View File

@@ -275,8 +275,13 @@ function Screen() {
function Pending() {
return (
<div className="flex h-full w-full items-center justify-center gap-2.5">
<LoaderIcon className="size-5 animate-spin" />
<div
data-tauri-drag-region
className="flex h-full w-full items-center justify-center gap-2.5"
>
<button type="button" disabled>
<LoaderIcon className="size-5 animate-spin" />
</button>
<p>Loading cache...</p>
</div>
);

View File

@@ -2,7 +2,7 @@ import { RepostNote } from "@/components/repost";
import { Suggest } from "@/components/suggest";
import { TextNote } from "@/components/text";
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { ColumnRouteSearch, Event, Kind } from "@lume/types";
import { Column } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
@@ -10,10 +10,16 @@ import { useTranslation } from "react-i18next";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/foryou")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
beforeLoad: async ({ search, context }) => {
const ark = context.ark;
// @ts-ignore, useless !!!
const interests = await ark.get_interest(search.account);
const interests = await ark.get_interest();
if (!interests) {
throw redirect({
@@ -31,13 +37,12 @@ export const Route = createFileRoute("/foryou")({
});
export function Screen() {
// @ts-ignore, just work!!!
const { id, name, account } = Route.useSearch();
const { label, name, account } = Route.useSearch();
const { ark, interests } = Route.useRouteContext();
const { t } = useTranslation();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["foryou", account],
queryKey: [name, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from_interests(
@@ -68,7 +73,7 @@ export function Screen() {
return (
<Column.Root>
<Column.Header id={id} name={name} />
<Column.Header label={label} name={name} />
<Column.Content>
{isLoading ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">

View File

@@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/group/create')({
component: () => <div>Hello /group/create!</div>
})

View File

@@ -1,30 +1,52 @@
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 { ColumnRouteSearch, 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("/group")({
export const Route = createFileRoute("/group")({
component: Screen,
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
beforeLoad: async ({ context }) => {
const ark = context.ark;
if (!ark) {
throw redirect({
to: "/group/create",
});
}
},
});
export function Screen() {
// @ts-ignore, just work!!!
const { id, name, account } = Route.useSearch();
const { label, name, account } = Route.useSearch();
const { ark } = Route.useRouteContext();
const { t } = useTranslation();
const {
data,
hasNextPage,
isLoading,
isRefetching,
isFetchingNextPage,
fetchNextPage,
} = useEvents("local", account);
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [name, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam);
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;
@@ -38,9 +60,9 @@ export function Screen() {
return (
<Column.Root>
<Column.Header id={id} name={name} />
<Column.Header label={label} 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" />
</div>

View File

@@ -1,23 +1,30 @@
import { ColumnRouteSearch } from "@lume/types";
import { Column } from "@lume/ui";
import { TOPICS, cn } from "@lume/utils";
import { createLazyFileRoute } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/interests")({
export const Route = createFileRoute("/interests")({
component: Screen,
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
});
function Screen() {
const { t } = useTranslation();
const { label, name } = Route.useSearch();
const { ark } = Route.useRouteContext();
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)
@@ -36,9 +43,7 @@ function Screen() {
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.");
@@ -50,7 +55,7 @@ function Screen() {
return (
<Column.Root>
<Column.Header id={search.id} name={search.name} />
<Column.Header label={label} name={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">

View File

@@ -8,7 +8,6 @@ export const Route = createFileRoute("/landing/")({
function Screen() {
const { t } = useTranslation();
const context = Route.useRouteContext();
return (
<div className="relative flex h-screen w-screen bg-black">

View File

@@ -2,25 +2,39 @@ import { RepostNote } from "@/components/repost";
import { Suggest } from "@/components/suggest";
import { TextNote } from "@/components/text";
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { ColumnRouteSearch, Event, Kind } from "@lume/types";
import { Column } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/newsfeed")({
export const Route = createFileRoute("/newsfeed")({
component: Screen,
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
beforeLoad: async ({ context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
return {
settings,
};
},
});
export function Screen() {
// @ts-ignore, just work!!!
const { id, name, account } = Route.useSearch();
const { label, name, account } = Route.useSearch();
const { ark } = Route.useRouteContext();
const { t } = useTranslation();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["local", account],
queryKey: [name, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam);
@@ -46,7 +60,7 @@ export function Screen() {
return (
<Column.Root>
<Column.Header id={id} name={name} />
<Column.Header label={label} name={name} />
<Column.Content>
{isLoading ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">

View File

@@ -22,8 +22,8 @@ function Screen() {
type="button"
onClick={() =>
install({
id: 9999,
name: "Lume Store",
label: "store",
name: "Store",
content: "/store/official",
})
}
@@ -36,8 +36,8 @@ function Screen() {
type="button"
onClick={() =>
install({
id: 9999,
name: "Lume Store",
label: "store",
name: "Store",
content: "/store/official",
})
}

View File

@@ -1,68 +1,27 @@
import { LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window";
import { readTextFile } from "@tauri-apps/plugin-fs";
export const Route = createFileRoute("/store/official")({
component: Screen,
loader: () => {
const columns: LumeColumn[] = [
{
id: 10002,
name: "For you",
content: "/foryou",
logo: "",
cover: "/foryou.png",
coverRetina: "/foryou@2x.png",
author: "Lume",
description: "Keep up to date with content based on your interests.",
},
{
id: 10003,
name: "Group Feeds",
content: "/group",
logo: "",
cover: "/group.png",
coverRetina: "/group@2x.png",
author: "Lume",
description: "Collective of people you're interested in.",
},
{
id: 10004,
name: "Antenas",
content: "/antenas",
logo: "",
cover: "/antenas.png",
coverRetina: "/antenas@2x.png",
author: "Lume",
description: "Keep track to specific content.",
},
{
id: 10005,
name: "Trending",
content: "/trending",
logo: "",
cover: "/trending.png",
coverRetina: "/trending@2x.png",
author: "Lume",
description: "What is trending on Nostr?.",
},
{
id: 10006,
name: "Global",
content: "/global",
logo: "",
cover: "/global.png",
coverRetina: "/global@2x.png",
author: "Lume",
description: "All events from connected relays.",
},
];
return columns;
beforeLoad: async () => {
const resourcePath = await resolveResource(
"resources/official_columns.json",
);
const officialColumns: LumeColumn[] = JSON.parse(
await readTextFile(resourcePath),
);
return {
officialColumns,
};
},
});
function Screen() {
const data = Route.useLoaderData();
const { officialColumns } = Route.useRouteContext();
const install = async (column: LumeColumn) => {
const mainWindow = getCurrent();
@@ -71,9 +30,9 @@ function Screen() {
return (
<div className="flex flex-col gap-3 p-3">
{data.map((column) => (
{officialColumns.map((column) => (
<div
key={column.id}
key={column.label}
className="relative h-[200px] w-full overflow-hidden rounded-xl bg-gradient-to-tr from-orange-100 to-blue-200 px-3 pt-3"
>
{column.cover ? (

View File

@@ -1,63 +1,61 @@
import { CancelIcon, GlobalIcon, LaurelIcon } from "@lume/icons";
import { GlobalIcon, LaurelIcon } from "@lume/icons";
import { ColumnRouteSearch } from "@lume/types";
import { Column } from "@lume/ui";
import { cn } from "@lume/utils";
import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/window";
export const Route = createFileRoute("/store")({
component: Screen,
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
});
function Screen() {
// @ts-ignore, just work!!!
const { id } = Route.useSearch();
const close = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "remove", id });
};
const { label, name } = Route.useSearch();
return (
<Column.Root>
<Column.Content>
<div className="flex h-14 shrink-0 items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900">
<div className="inline-flex h-full w-full items-center gap-2">
<Link to="/store/official">
{({ isActive }) => (
<div
className={cn(
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
isActive
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)}
>
<LaurelIcon className="size-5" />
Official
</div>
)}
</Link>
<Link to="/store/community">
{({ isActive }) => (
<div
className={cn(
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
isActive
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)}
>
<GlobalIcon className="size-5" />
Community
</div>
)}
</Link>
</div>
<button type="button" onClick={close}>
<CancelIcon className="size-4 text-neutral-700 dark:text-neutral-300" />
</button>
<Column.Header label={label} name={name}>
<div className="inline-flex h-full w-full items-center gap-1">
<Link to="/store/official">
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)}
>
<LaurelIcon className="size-4" />
Official
</div>
)}
</Link>
<Link to="/store/community">
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive
? "bg-neutral-100 dark:bg-neutral-900"
: "opacity-50",
)}
>
<GlobalIcon className="size-4" />
Community
</div>
)}
</Link>
</div>
</Column.Header>
<Column.Content>
<Outlet />
</Column.Content>
</Column.Root>