feat: add nstore
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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">
|
||||
|
5
apps/desktop2/src/routes/group.create.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/group/create')({
|
||||
component: () => <div>Hello /group/create!</div>
|
||||
})
|
@ -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>
|
@ -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">
|
@ -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">
|
||||
|
@ -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">
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
|
@ -6,6 +6,7 @@ import type {
|
||||
EventWithReplies,
|
||||
Interests,
|
||||
Keys,
|
||||
LumeColumn,
|
||||
Metadata,
|
||||
Settings,
|
||||
} from "@lume/types";
|
||||
@ -14,6 +15,13 @@ import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { generateContentTags } from "@lume/utils";
|
||||
|
||||
enum NSTORE_KEYS {
|
||||
settings = "lume_user_settings",
|
||||
interests = "lume_user_interests",
|
||||
columns = "lume_user_columns",
|
||||
group = "lume_group_",
|
||||
}
|
||||
|
||||
export class Ark {
|
||||
public windows: WebviewWindow[];
|
||||
|
||||
@ -565,13 +573,36 @@ export class Ark {
|
||||
}
|
||||
}
|
||||
|
||||
public async get_settings(id: string) {
|
||||
public async get_columns() {
|
||||
try {
|
||||
const cmd: string = await invoke("get_settings", { id });
|
||||
if (!cmd) return null;
|
||||
if (!cmd.length) return null;
|
||||
const cmd: string = await invoke("get_nstore", {
|
||||
key: NSTORE_KEYS.columns,
|
||||
});
|
||||
const columns: LumeColumn[] = cmd ? JSON.parse(cmd) : [];
|
||||
return columns;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const settings: Settings = JSON.parse(cmd);
|
||||
public async set_columns(columns: LumeColumn[]) {
|
||||
try {
|
||||
const cmd: string = await invoke("set_nstore", {
|
||||
key: NSTORE_KEYS.columns,
|
||||
content: JSON.stringify(columns),
|
||||
});
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async get_settings() {
|
||||
try {
|
||||
const cmd: string = await invoke("get_nstore", {
|
||||
key: NSTORE_KEYS.settings,
|
||||
});
|
||||
const settings: Settings = cmd ? JSON.parse(cmd) : null;
|
||||
return settings;
|
||||
} catch {
|
||||
const defaultSettings: Settings = {
|
||||
@ -585,7 +616,8 @@ export class Ark {
|
||||
|
||||
public async set_settings(settings: Settings) {
|
||||
try {
|
||||
const cmd: string = await invoke("set_settings", {
|
||||
const cmd: string = await invoke("set_nstore", {
|
||||
key: NSTORE_KEYS.settings,
|
||||
content: JSON.stringify(settings),
|
||||
});
|
||||
return cmd;
|
||||
@ -594,13 +626,12 @@ export class Ark {
|
||||
}
|
||||
}
|
||||
|
||||
public async get_interest(id: string) {
|
||||
public async get_interest() {
|
||||
try {
|
||||
const cmd: string = await invoke("get_interest", { id });
|
||||
if (!cmd) return null;
|
||||
if (!cmd.length) return null;
|
||||
|
||||
const interests: Interests = JSON.parse(cmd);
|
||||
const cmd: string = await invoke("get_nstore", {
|
||||
key: NSTORE_KEYS.interests,
|
||||
});
|
||||
const interests: Interests = cmd ? JSON.parse(cmd) : null;
|
||||
return interests;
|
||||
} catch {
|
||||
return null;
|
||||
@ -618,7 +649,8 @@ export class Ark {
|
||||
users: users ?? [],
|
||||
hashtags: hashtags ?? [],
|
||||
};
|
||||
const cmd: string = await invoke("set_interest", {
|
||||
const cmd: string = await invoke("set_nstore", {
|
||||
key: NSTORE_KEYS.interests,
|
||||
content: JSON.stringify(interests),
|
||||
});
|
||||
return cmd;
|
||||
|
@ -1,18 +1,17 @@
|
||||
export function RefreshIcon(props: JSX.IntrinsicElements['svg']) {
|
||||
export function RefreshIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M17.5 2.474c.51 1.192.861 2.444 1.049 3.726a.479.479 0 0 1-.298.515l-.181.07M6.5 21.527A15 15 0 0 1 5.451 17.8a.48.48 0 0 1 .298-.515l.181-.07M14.5 7.67a15 15 0 0 0 3.57-.884m0 0a8 8 0 0 0-13.912 6.797m15.75-2.79A8 8 0 0 1 5.93 17.215m3.571-.885a15.002 15.002 0 0 0-3.57.884" />
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M13 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8-10a1 1 0 1 0-2 0 1 1 0 0 0 2 0Zm-1.07 3.268a1 1 0 1 1-1 1.732 1 1 0 0 1 1-1.732Zm-2.562 5.026a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732ZM18.927 8a1 1 0 1 1-1-1.732 1 1 0 0 1 1 1.732Z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M9.25 14.75v5.5h-5.5M9 19.688a8.25 8.25 0 1 1 6.25-15.273"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
10
packages/types/index.d.ts
vendored
@ -75,8 +75,14 @@ export interface RichContent {
|
||||
notes: string[];
|
||||
}
|
||||
|
||||
export interface ColumnRouteSearch {
|
||||
account: string;
|
||||
label: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LumeColumn {
|
||||
id: number;
|
||||
label: string;
|
||||
name: string;
|
||||
content: URL | string;
|
||||
description?: string;
|
||||
@ -89,7 +95,7 @@ export interface LumeColumn {
|
||||
|
||||
export interface EventColumns {
|
||||
type: "add" | "remove" | "update" | "left" | "right";
|
||||
id?: number;
|
||||
label?: string;
|
||||
column?: LumeColumn;
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { ChevronDownIcon, RefreshIcon, TrashIcon } from "@lume/icons";
|
||||
import { CancelIcon, RefreshIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function ColumnHeader({
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
id: number;
|
||||
label: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const reload = () => {
|
||||
window.location.reload();
|
||||
@ -18,46 +20,37 @@ export function ColumnHeader({
|
||||
|
||||
const close = async () => {
|
||||
const mainWindow = getCurrent();
|
||||
await mainWindow.emit("columns", { type: "remove", id });
|
||||
await mainWindow.emit("columns", { type: "remove", label });
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-11 w-full shrink-0 items-center justify-center gap-2 border-b border-neutral-100 dark:border-neutral-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button type="button" className="inline-flex items-center gap-2">
|
||||
<div className="text-[13px] font-medium">{name}</div>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
</div>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={5}
|
||||
className="flex w-[200px] flex-col overflow-hidden rounded-xl bg-black p-1 focus:outline-none dark:bg-white"
|
||||
<div
|
||||
className={cn(
|
||||
"h-11 w-full flex items-center justify-between shrink-0 px-3 border-b border-neutral-100 dark:border-neutral-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!children ? (
|
||||
<div className="text-[13px] font-medium">{name}</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={reload}
|
||||
className="size-7 inline-flex hover:bg-neutral-100 rounded-md dark:hover:bg-neutral-900 items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onClick={reload}
|
||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
||||
>
|
||||
<RefreshIcon className="size-4" />
|
||||
Reload
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onClick={close}
|
||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
Close
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Arrow className="fill-black dark:fill-white" />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
<RefreshIcon className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
className="size-7 inline-flex items-center hover:bg-neutral-100 rounded-md dark:hover:bg-neutral-900 justify-center text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
|
||||
>
|
||||
<CancelIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3,20 +3,20 @@ import { ReactNode } from "react";
|
||||
|
||||
export function ColumnRoot({
|
||||
children,
|
||||
shadow = true,
|
||||
background = true,
|
||||
className,
|
||||
background = true,
|
||||
shadow = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
shadow?: boolean;
|
||||
background?: boolean;
|
||||
className?: string;
|
||||
background?: boolean;
|
||||
shadow?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="h-full w-full p-2">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex h-full w-full flex-col rounded-xl",
|
||||
"relative flex h-full w-full flex-col rounded-xl overflow-hidden",
|
||||
shadow ? "shadow-primary" : "",
|
||||
background ? "bg-white dark:bg-black" : "",
|
||||
className,
|
||||
|
@ -59,6 +59,9 @@
|
||||
"allow": [
|
||||
{
|
||||
"path": "$RESOURCE/locales/*"
|
||||
},
|
||||
{
|
||||
"path": "$RESOURCE/resources/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
{"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"]}}
|
||||
{"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/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 181 KiB |
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 810 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 590 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 20 KiB |
52
src-tauri/resources/official_columns.json
Normal file
@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"label": "rRtguZwIpd5G8Wt54OTb7",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"label": "fve9fk2fVyFWORPBkjd79",
|
||||
"name": "Group Feeds",
|
||||
"content": "/group",
|
||||
"logo": "",
|
||||
"cover": "/group.png",
|
||||
"coverRetina": "/group@2x.png",
|
||||
"author": "Lume",
|
||||
"description": "Collective of people you're interested in."
|
||||
},
|
||||
{
|
||||
"label": "sDbO6XxAGnW5XuUZEgZMn",
|
||||
"name": "Antenas",
|
||||
"content": "/antenas",
|
||||
"logo": "",
|
||||
"cover": "/antenas.png",
|
||||
"coverRetina": "/antenas@2x.png",
|
||||
"author": "Lume",
|
||||
"description": "Keep track to specific content."
|
||||
},
|
||||
{
|
||||
"label": "gxtcIbgD8YNPbeI5o92I8",
|
||||
"name": "Trending",
|
||||
"content": "/trending",
|
||||
"logo": "",
|
||||
"cover": "/trending.png",
|
||||
"coverRetina": "/trending@2x.png",
|
||||
"author": "Lume",
|
||||
"description": "What is trending on Nostr?."
|
||||
},
|
||||
{
|
||||
"label": "GLFm44za8rhJDP04LMr3M",
|
||||
"name": "Global",
|
||||
"content": "/global",
|
||||
"logo": "",
|
||||
"cover": "/global.png",
|
||||
"coverRetina": "/global@2x.png",
|
||||
"author": "Lume",
|
||||
"description": "All events from connected relays."
|
||||
}
|
||||
]
|
9
src-tauri/resources/system_columns.json
Normal file
@ -0,0 +1,9 @@
|
||||
[
|
||||
{ "label": "newsfeed", "name": "Newsfeed", "content": "/newsfeed" },
|
||||
{ "label": "open", "name": "Open", "content": "/open" },
|
||||
{
|
||||
"label": "store",
|
||||
"name": "Store",
|
||||
"content": "/store/official"
|
||||
}
|
||||
]
|
@ -115,10 +115,8 @@ fn main() {
|
||||
nostr::metadata::create_profile,
|
||||
nostr::metadata::follow,
|
||||
nostr::metadata::unfollow,
|
||||
nostr::metadata::set_interest,
|
||||
nostr::metadata::get_interest,
|
||||
nostr::metadata::set_settings,
|
||||
nostr::metadata::get_settings,
|
||||
nostr::metadata::get_nstore,
|
||||
nostr::metadata::set_nstore,
|
||||
nostr::metadata::set_nwc,
|
||||
nostr::metadata::load_nwc,
|
||||
nostr::metadata::get_balance,
|
||||
|
@ -179,38 +179,34 @@ pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<EventId, Stri
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_interest(content: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
||||
pub async fn set_nstore(
|
||||
key: &str,
|
||||
content: &str,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<EventId, String> {
|
||||
let client = &state.client;
|
||||
let tag = Tag::Identifier("lume_user_interest".into());
|
||||
let tag = Tag::Identifier(key.into());
|
||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, content, vec![tag]);
|
||||
|
||||
if let Ok(event_id) = client.send_event_builder(builder).await {
|
||||
println!("set nstore: {}", event_id);
|
||||
Ok(event_id)
|
||||
} else {
|
||||
Err("Set interest failed".into())
|
||||
Err("Event has been published failled".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_interest(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
||||
Ok(val) => match val {
|
||||
Nip19::Pubkey(pubkey) => Some(pubkey),
|
||||
Nip19::Profile(profile) => Some(profile.public_key),
|
||||
_ => None,
|
||||
},
|
||||
Err(_) => match PublicKey::from_str(id) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
},
|
||||
};
|
||||
let signer = client.signer().await.unwrap();
|
||||
let public_key = signer.public_key().await;
|
||||
|
||||
if let Some(author) = public_key {
|
||||
if let Ok(author) = public_key {
|
||||
let filter = Filter::new()
|
||||
.author(author)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier("lume_user_interest")
|
||||
.identifier(key)
|
||||
.limit(1);
|
||||
|
||||
let query = client
|
||||
@ -219,68 +215,17 @@ pub async fn get_interest(id: &str, state: State<'_, Nostr>) -> Result<String, S
|
||||
|
||||
if let Ok(events) = query {
|
||||
if let Some(event) = events.first() {
|
||||
println!("get nstore key: {} - received: {}", key, event.id);
|
||||
Ok(event.content.to_string())
|
||||
} else {
|
||||
Err("User interest not found".into())
|
||||
println!("get nstore key: {}", key);
|
||||
Err("Value not found".into())
|
||||
}
|
||||
} else {
|
||||
Err("User interest not found".into())
|
||||
Err("Query nstore event failed".into())
|
||||
}
|
||||
} else {
|
||||
Err("Get interest failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_settings(content: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
||||
let client = &state.client;
|
||||
let tag = Tag::Identifier("lume_user_settings".into());
|
||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, content, vec![tag]);
|
||||
|
||||
if let Ok(event_id) = client.send_event_builder(builder).await {
|
||||
Ok(event_id)
|
||||
} else {
|
||||
Err("Set settings failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_settings(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
||||
Ok(val) => match val {
|
||||
Nip19::Pubkey(pubkey) => Some(pubkey),
|
||||
Nip19::Profile(profile) => Some(profile.public_key),
|
||||
_ => None,
|
||||
},
|
||||
Err(_) => match PublicKey::from_str(id) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(author) = public_key {
|
||||
let filter = Filter::new()
|
||||
.author(author)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier("lume_user_settings")
|
||||
.limit(1);
|
||||
|
||||
let query = client
|
||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||
.await;
|
||||
|
||||
if let Ok(events) = query {
|
||||
if let Some(event) = events.first() {
|
||||
Ok(event.content.to_string())
|
||||
} else {
|
||||
Err("User settings not found".into())
|
||||
}
|
||||
} else {
|
||||
Err("User settings not found".into())
|
||||
}
|
||||
} else {
|
||||
Err("Get settings failed".into())
|
||||
Err("Something is wrong".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
||||
"targets": "all",
|
||||
"active": true,
|
||||
"category": "SocialNetworking",
|
||||
"resources": ["resources/*", "./locales/*"],
|
||||
"resources": ["resources/*", "locales/*"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|