wip: migrate frontend to new backend

This commit is contained in:
reya 2024-02-09 15:33:27 +07:00
parent ec78cf8bf7
commit 739ba63e6c
55 changed files with 351 additions and 933 deletions

View File

@ -19,11 +19,11 @@ export default function App() {
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<QueryClientProvider client={queryClient}>
<Toaster position="top-center" theme="system" closeButton />
<StorageProvider>
<ArkProvider>
<ArkProvider>
<StorageProvider>
<Router />
</ArkProvider>
</StorageProvider>
</StorageProvider>
</ArkProvider>
</QueryClientProvider>
</I18nextProvider>
);

View File

@ -3,7 +3,7 @@ import { Timeline } from "@columns/timeline";
export function HomeScreen() {
return (
<div className="relative w-full h-full">
<Timeline column={{ id: 1, kind: 1, title: "", content: "" }} />
<Timeline column={{ id: 1, title: "", content: "" }} />
</div>
);
}

View File

@ -11,9 +11,11 @@ export class Ark {
public async verify_signer() {
try {
const cmd: string = await invoke("verify_signer");
if (!cmd) return false;
this.account.pubkey = cmd;
return true;
if (cmd) {
this.account.pubkey = cmd;
return true;
}
return false;
} catch (e) {
console.error(String(e));
}

View File

@ -5,48 +5,33 @@ import {
RefreshIcon,
TrashIcon,
} from "@lume/icons";
import { useColumn } from "@lume/storage";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { InterestModal } from "./interestModal";
import { useColumnContext } from "./provider";
export function ColumnHeader({
id,
title,
queryKey,
}: {
id: number;
title: string;
queryKey?: string[];
}) {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { moveColumn, removeColumn } = useColumnContext();
const { move, remove } = useColumn();
const column = useColumnContext();
const queryClient = useQueryClient();
const refresh = async () => {
if (queryKey) await queryClient.refetchQueries({ queryKey });
};
const moveLeft = async () => {
moveColumn(id, "left");
};
const moveRight = async () => {
moveColumn(id, "right");
};
const deleteWidget = async () => {
await removeColumn(id);
};
return (
<DropdownMenu.Root>
<div className="flex items-center justify-center gap-2 px-3 w-full border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
<DropdownMenu.Trigger asChild>
<div className="inline-flex items-center gap-1.5">
<div className="text-[13px] font-medium">{title}</div>
<div className="text-[13px] font-medium">{column.title}</div>
<ChevronDownIcon className="size-5" />
</div>
</DropdownMenu.Trigger>
@ -65,18 +50,10 @@ export function ColumnHeader({
{t("global.refresh")}
</button>
</DropdownMenu.Item>
{queryKey?.[0] === "foryou-9998" ? (
<DropdownMenu.Item asChild>
<InterestModal
queryKey={queryKey}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
/>
</DropdownMenu.Item>
) : null}
<DropdownMenu.Item asChild>
<button
type="button"
onClick={moveLeft}
onClick={() => move(column.id, "left")}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveLeftIcon className="size-4" />
@ -86,7 +63,7 @@ export function ColumnHeader({
<DropdownMenu.Item asChild>
<button
type="button"
onClick={moveRight}
onClick={() => move(column.id, "right")}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveRightIcon className="size-4" />
@ -97,7 +74,7 @@ export function ColumnHeader({
<DropdownMenu.Item asChild>
<button
type="button"
onClick={deleteWidget}
onClick={() => remove(column.id)}
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
>
<TrashIcon className="size-4" />

View File

@ -2,9 +2,11 @@ import { Route } from "react-router-dom";
import { ColumnContent } from "./content";
import { ColumnHeader } from "./header";
import { ColumnLiveWidget } from "./live";
import { ColumnProvider } from "./provider";
import { ColumnRoot } from "./root";
export const Column = {
Provider: ColumnProvider,
Root: ColumnRoot,
Live: ColumnLiveWidget,
Header: ColumnHeader,

View File

@ -1,7 +1,5 @@
import { ArrowUpIcon } from "@lume/icons";
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
import { useEffect, useState } from "react";
import { useArk } from "../../hooks/useArk";
export function ColumnLiveWidget({
filter,

View File

@ -1,136 +1,14 @@
import { useStorage } from "@lume/storage";
import { IColumn } from "@lume/types";
import { COL_TYPES } from "@lume/utils";
import {
type MutableRefObject,
type ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { toast } from "sonner";
import { type VListHandle } from "virtua";
import { LumeColumn } from "@lume/types";
import { ReactNode, createContext, useContext } from "react";
type ColumnContext = {
columns: IColumn[];
vlistRef: MutableRefObject<VListHandle>;
addColumn: (column: IColumn) => Promise<void>;
removeColumn: (id: number) => Promise<void>;
moveColumn: (id: number, position: "left" | "right") => void;
updateColumn: (id: number, title: string, content: string) => Promise<void>;
loadAllColumns: () => Promise<IColumn[]>;
};
const ColumnContext = createContext<ColumnContext>(null);
export function ColumnProvider({ children }: { children: ReactNode }) {
const storage = useStorage();
const vlistRef = useRef<VListHandle>(null);
const [columns, setColumns] = useState<IColumn[]>([
{
id: 9999,
title: "Newsfeed",
content: "",
kind: COL_TYPES.newsfeed,
},
{
id: 9998,
title: "For You",
content: "",
kind: COL_TYPES.foryou,
},
]);
const loadAllColumns = useCallback(async () => {
return await storage.getColumns();
}, []);
const addColumn = useCallback(async (column: IColumn) => {
const result = await storage.createColumn(
column.kind,
column.title,
column.content,
);
if (result) {
setColumns((prev) => [...prev, result]);
vlistRef?.current.scrollToIndex(columns.length);
}
}, []);
const removeColumn = useCallback(async (id: number) => {
if (id === 9998 || id === 9999) {
toast.info("You cannot remove default column");
return;
}
await storage.removeColumn(id);
setColumns((prev) => prev.filter((t) => t.id !== id));
}, []);
const updateColumn = useCallback(
async (id: number, title: string, content: string) => {
const res = await storage.updateColumn(id, title, content);
if (res) {
const newCols = columns.map((col) => {
if (col.id === id) {
return { ...col, title, content };
}
return col;
});
setColumns(newCols);
}
},
[columns],
);
const moveColumn = useCallback(
(id: number, position: "left" | "right") => {
const newCols = [...columns];
const col = newCols.find((el) => el.id === id);
const colIndex = newCols.findIndex((el) => el.id === id);
newCols.splice(colIndex, 1);
if (position === "left") newCols.splice(colIndex - 1, 0, col);
if (position === "right") newCols.splice(colIndex + 1, 0, col);
setColumns(newCols);
},
[columns],
);
useEffect(() => {
let isMounted = true;
loadAllColumns().then((data) => {
if (isMounted) setColumns((prev) => [...prev, ...data]);
});
return () => {
isMounted = false;
};
}, []);
const ColumnContext = createContext<LumeColumn>(null);
export function ColumnProvider({
column,
children,
}: { column: LumeColumn; children: ReactNode }) {
return (
<ColumnContext.Provider
value={{
columns,
vlistRef,
addColumn,
removeColumn,
moveColumn,
updateColumn,
loadAllColumns,
}}
>
{children}
</ColumnContext.Provider>
<ColumnContext.Provider value={column}>{children}</ColumnContext.Provider>
);
}

View File

@ -1,6 +1,4 @@
import { NDKAppHandlerEvent } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query";
import { useArk } from "../../hooks/useArk";
export function AppHandler({ tag }: { tag: string[] }) {
const ark = useArk();

View File

@ -1,19 +1,7 @@
import { COL_TYPES } from "@lume/utils";
import { useColumnContext } from "../../column/provider";
export function Hashtag({ tag }: { tag: string }) {
const { addColumn } = useColumnContext();
return (
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.hashtag,
title: tag,
content: tag,
})
}
className="text-blue-500 break-all cursor-default hover:text-blue-600"
>
{tag}

View File

@ -1,11 +1,10 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES, NOSTR_MENTIONS } from "@lume/utils";
import { NOSTR_MENTIONS } from "@lume/utils";
import { ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import reactStringReplace from "react-string-replace";
import { useEvent } from "../../../hooks/useEvent";
import { useColumnContext } from "../../column/provider";
import { User } from "../../user";
import { Hashtag } from "./hashtag";
import { MentionUser } from "./user";
@ -15,7 +14,6 @@ export function MentionNote({
openable = true,
}: { eventId: string; openable?: boolean }) {
const { t } = useTranslation();
const { addColumn } = useColumnContext();
const { isLoading, isError, data } = useEvent(eventId);
const richContent = useMemo(() => {
@ -133,13 +131,6 @@ export function MentionNote({
</Link>
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.thread,
title: "Thread",
content: data.id,
})
}
className="inline-flex items-center justify-center rounded-md text-neutral-600 dark:text-neutral-400 size-6 bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
<PinIcon className="size-4" />

View File

@ -1,10 +1,9 @@
import { HorizontalDotsIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { useArk } from "../../hooks/useArk";
import { useArk } from "../../provider";
import { useNoteContext } from "./provider";
export function NoteMenu() {
@ -19,7 +18,7 @@ export function NoteMenu() {
};
const copyRaw = async () => {
await writeText(JSON.stringify(await event.toNostrEvent()));
await writeText(JSON.stringify(event));
};
const copyNpub = async () => {

View File

@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useArk } from "../../hooks/useArk";
import { useArk } from "../../provider";
import { AppHandler } from "./appHandler";
import { useNoteContext } from "./provider";

View File

@ -1,6 +1,6 @@
import { CheckCircleIcon, DownloadIcon } from "@lume/icons";
import { downloadDir } from "@tauri-apps/api/path";
import { Window } from "@tauri-apps/api/window";
import { WebviewWindow } from "@tauri-apps/api/webview";
import { download } from "@tauri-apps/plugin-upload";
import { SyntheticEvent, useState } from "react";
@ -23,7 +23,7 @@ export function ImagePreview({ url }: { url: string }) {
const open = async () => {
const name = new URL(url).pathname.split("/").pop();
return new Window("image-viewer", {
return new WebviewWindow("image-viewer", {
url,
title: name,
});

View File

@ -1,5 +1,5 @@
import { NavArrowDownIcon } from "@lume/icons";
import { NDKEventWithReplies } from "@lume/types";
import { EventWithReplies } from "@lume/types";
import { cn } from "@lume/utils";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
@ -10,7 +10,7 @@ import { ChildReply } from "./childReply";
export function Reply({
event,
}: {
event: NDKEventWithReplies;
event: EventWithReplies;
}) {
const [t] = useTranslation();
const [open, setOpen] = useState(false);

View File

@ -4,7 +4,7 @@ import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { Note } from "..";
import { useArk } from "../../../hooks/useArk";
import { useArk } from "../../../provider";
import { User } from "../../user";
export function RepostNote({

View File

@ -1,10 +1,9 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES, cn } from "@lume/utils";
import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Note } from ".";
import { useArk } from "../../hooks/useArk";
import { useColumnContext } from "../column/provider";
import { useArk } from "../../provider";
import { useNoteContext } from "./provider";
export function NoteThread({

View File

@ -2,14 +2,11 @@ import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useArk } from "../../hooks/useArk";
export function UserFollowButton({
target,
className,
}: { target: string; className?: string }) {
const ark = useArk();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [followed, setFollowed] = useState(false);

View File

@ -8,7 +8,7 @@ export function UserNip05({ className }: { className?: string }) {
const { isLoading, data: verified } = useQuery({
queryKey: ["nip05", user?.profile.nip05],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
queryFn: async () => {
if (!user) return false;
if (!user.profile.nip05) return false;
return false;

View File

@ -1,7 +1,7 @@
import { Metadata } from "@lume/types";
import { useQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { ReactNode, createContext, useContext } from "react";
import { useArk } from "../../hooks/useArk";
const UserContext = createContext<{ pubkey: string; profile: Metadata }>(null);
@ -10,13 +10,12 @@ export function UserProvider({
children,
embed,
}: { pubkey: string; children: ReactNode; embed?: string }) {
const ark = useArk();
const { data: profile } = useQuery({
queryKey: ["user", pubkey],
queryFn: async () => {
if (embed) return JSON.parse(embed) as Metadata;
const profile = await ark.get_profile(pubkey);
const profile: Metadata = await invoke("get_profile", { id: pubkey });
if (!profile)
throw new Error(

View File

@ -1,10 +0,0 @@
import { useContext } from "react";
import { ArkContext } from "../provider";
export const useArk = () => {
const context = useContext(ArkContext);
if (context === undefined) {
throw new Error("Please import Ark Provider to use useArk() hook");
}
return context;
};

View File

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useArk } from "./useArk";
import { useArk } from "../provider";
export function useEvent(id: string) {
const ark = useArk();

View File

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useArk } from "./useArk";
import { useArk } from "../provider";
export function useProfile(pubkey: string) {
const ark = useArk();

View File

@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useArk } from "./useArk";
import { useArk } from "../provider";
export function useRelaylist() {
const ark = useArk();

View File

@ -1,12 +1,9 @@
export * from "./ark";
export * from "./provider";
export * from "./hooks/useEvent";
export * from "./hooks/useArk";
export * from "./hooks/useProfile";
export * from "./hooks/useRelayList";
export * from "./components/user";
export * from "./components/column";
export * from "./components/column/provider";
export * from "./components/note";
export * from "./components/note/primitives/text";
export * from "./components/note/primitives/repost";

View File

@ -1,4 +1,4 @@
import { PropsWithChildren, createContext, useEffect, useMemo } from "react";
import { PropsWithChildren, createContext, useContext, useMemo } from "react";
import { Ark } from "./ark";
export const ArkContext = createContext<Ark>(undefined);
@ -7,3 +7,11 @@ export const ArkProvider = ({ children }: PropsWithChildren<object>) => {
const ark = useMemo(() => new Ark(), []);
return <ArkContext.Provider value={ark}>{children}</ArkContext.Provider>;
};
export const useArk = () => {
const context = useContext(ArkContext);
if (context === undefined) {
throw new Error("Ark Provider is not import");
}
return context;
};

View File

@ -1,10 +1,10 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { AntenasForm } from "./components/form";
import { HomeRoute } from "./home";
export function Antenas({ column }: { column: IColumn }) {
export function Antenas({ column }: { column: LumeColumn }) {
const colKey = `antenas-${column.id}`;
const created = !!column.content?.length;

View File

@ -1,8 +1,8 @@
import { Column, useColumnContext } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { COL_TYPES } from "@lume/utils";
export function Default({ column }: { column: IColumn }) {
export function Default({ column }: { column: LumeColumn }) {
const { addColumn } = useColumnContext();
return (

View File

@ -1,13 +1,13 @@
import { Column } from "@lume/ark";
import { useStorage } from "@lume/storage";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useQueryClient } from "@tanstack/react-query";
import { useRef } from "react";
import { HomeRoute } from "./home";
export function ForYou({ column }: { column: IColumn }) {
export function ForYou({ column }: { column: LumeColumn }) {
const colKey = `foryou-${column.id}`;
const storage = useStorage();
const queryClient = useQueryClient();

View File

@ -1,9 +1,9 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Global({ column }: { column: IColumn }) {
export function Global({ column }: { column: LumeColumn }) {
const colKey = `global-${column.id}`;
return (

View File

@ -1,10 +1,10 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { GroupForm } from "./components/form";
import { HomeRoute } from "./home";
export function Group({ column }: { column: IColumn }) {
export function Group({ column }: { column: LumeColumn }) {
const colKey = `group-${column.id}`;
const created = !!column.content?.length;

View File

@ -1,9 +1,9 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Hashtag({ column }: { column: IColumn }) {
export function Hashtag({ column }: { column: LumeColumn }) {
const colKey = `hashtag-${column.id}`;
const hashtag = column.content.replace("#", "");

View File

@ -1,9 +1,9 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { HomeRoute } from "./home";
import { EventRoute, UserRoute } from "@lume/ui";
export function Thread({ column }: { column: IColumn }) {
export function Thread({ column }: { column: LumeColumn }) {
return (
<Column.Root>
<Column.Header id={column.id} title={column.title} />

View File

@ -3,16 +3,15 @@ import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { Link } from "react-router-dom";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({ colKey }: { colKey: string }) {
export function HomeRoute({ queryKey }: { queryKey: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const cacheKey = `${queryKey}-vlist`;
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
@ -22,16 +21,14 @@ export function HomeRoute({ colKey }: { colKey: string }) {
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [colKey],
queryKey: [queryKey],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.get_text_events(FETCH_LIMIT);
const events = await ark.get_text_events(FETCH_LIMIT, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
@ -71,23 +68,6 @@ export function HomeRoute({ colKey }: { colKey: string }) {
};
}, []);
/*
if (!ark.account.contacts.length) {
return (
<div className="px-3 mt-3">
<EmptyFeed />
<Link
to="/suggest"
className="mt-3 w-full gap-2 inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 bg-blue-500 hover:bg-blue-600 text-white"
>
<SearchIcon className="size-5" />
Find accounts to follow
</Link>
</div>
);
}
*/
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">

View File

@ -1,51 +1,25 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, SuggestRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Timeline({ column }: { column: IColumn }) {
export function Timeline({ column }: { column: LumeColumn }) {
const colKey = `timeline-${column.id}`;
// const ark = useArk();
// const queryClient = useQueryClient();
// const since = useRef(Math.floor(Date.now() / 1000));
/*
const refresh = async (events: NDKEvent[]) => {
const uniqEvents = new Set(events);
await queryClient.setQueryData(
[colKey],
(prev: { pageParams: number; pages: Array<NDKEvent[]> }) => ({
...prev,
pages: [[...uniqEvents], ...prev.pages],
}),
);
};
*/
return (
<Column.Root>
{/*<Column.Header id={column.id} queryKey={[colKey]} title="Timeline" />*/}
{/*ark.account.contacts.length ? (
<Column.Live
filter={{
kinds: [NDKKind.Text, NDKKind.Repost],
authors: ark.account.contacts,
since: since.current,
}}
onClick={refresh}
/>
) : null*/}
<Column.Content>
<Column.Route path="/" element={<HomeRoute colKey={colKey} />} />
{/*
<Column.Provider column={column}>
<Column.Root>
<Column.Header queryKey={[colKey]} />
<Column.Content>
<Column.Route path="/" element={<HomeRoute queryKey={colKey} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
<Column.Route
path="/suggest"
element={<SuggestRoute queryKey={[colKey]} />}
element={<SuggestRoute queryKey={colKey} />}
/>
*/}
</Column.Content>
</Column.Root>
</Column.Content>
</Column.Root>
</Column.Provider>
);
}

View File

@ -1,9 +1,9 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function TrendingNotes({ column }: { column: IColumn }) {
export function TrendingNotes({ column }: { column: LumeColumn }) {
const colKey = `trending-notes-${column.id}`;
return (

View File

@ -1,9 +1,9 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function User({ column }: { column: IColumn }) {
export function User({ column }: { column: LumeColumn }) {
return (
<Column.Root>
<Column.Header id={column.id} title={column.title} />

View File

@ -1,8 +1,8 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { LumeColumn } from "@lume/types";
import { HomeRoute } from "./home";
export function Waifu({ column }: { column: IColumn }) {
export function Waifu({ column }: { column: LumeColumn }) {
const colKey = `waifu-${column.id}`;
return (

View File

@ -1,442 +0,0 @@
// inspired by NDK Cache Dexie
// source: https://github.com/nostr-dev-kit/ndk/tree/master/ndk-cache-dexie
import { LumeStorage } from "@lume/storage";
import {
Hexpubkey,
NDKCacheAdapter,
NDKEvent,
NDKFilter,
NDKRelay,
NDKSubscription,
NDKUserProfile,
profileFromEvent,
} from "@nostr-dev-kit/ndk";
import { LRUCache } from "lru-cache";
import { NostrEvent } from "nostr-fetch";
import { matchFilter } from "nostr-tools";
export class NDKCacheAdapterTauri implements NDKCacheAdapter {
#storage: LumeStorage;
private dirtyProfiles: Set<Hexpubkey> = new Set();
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
readonly locking: boolean;
constructor(storage: LumeStorage) {
this.#storage = storage;
this.locking = true;
this.profiles = new LRUCache({
max: 100000,
});
setInterval(() => {
this.dumpProfiles();
}, 1000 * 10);
}
public async query(subscription: NDKSubscription): Promise<void> {
Promise.allSettled(
subscription.filters.map((filter) =>
this.processFilter(filter, subscription),
),
);
}
public async fetchProfile(pubkey: Hexpubkey) {
if (!this.profiles) return null;
let profile = this.profiles.get(pubkey);
if (!profile) {
const user = await this.#storage.getCacheUser(pubkey);
if (user) {
profile = user.profile as NDKUserProfile;
this.profiles.set(pubkey, profile);
}
}
return profile;
}
public saveProfile(pubkey: Hexpubkey, profile: NDKUserProfile) {
if (!this.profiles) return;
this.profiles.set(pubkey, profile);
this.dirtyProfiles.add(pubkey);
}
private async processFilter(
filter: NDKFilter,
subscription: NDKSubscription,
): Promise<void> {
const _filter = { ...filter };
_filter.limit = undefined;
const filterKeys = Object.keys(_filter || {})
.sort()
.filter((e) => e !== "limit");
try {
await Promise.allSettled([
this.byKindAndAuthor(filterKeys, filter, subscription),
this.byAuthors(filterKeys, filter, subscription),
this.byKinds(filterKeys, filter, subscription),
this.byIdsQuery(filterKeys, filter, subscription),
this.byNip33Query(filterKeys, filter, subscription),
this.byTagsAndOptionallyKinds(filterKeys, filter, subscription),
]);
} catch (error) {
console.error(error);
}
}
public async setEvent(
event: NDKEvent,
filters: NDKFilter[],
relay?: NDKRelay,
): Promise<void> {
if (event.kind === 0) {
if (!this.profiles) return;
const profile: NDKUserProfile = profileFromEvent(event);
this.profiles.set(event.pubkey, profile);
} else {
let addEvent = true;
if (event.isParamReplaceable()) {
const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`;
const existingEvent = await this.#storage.getCacheEvent(replaceableId);
if (
existingEvent &&
event.created_at &&
existingEvent.createdAt > event.created_at
) {
addEvent = false;
}
}
if (addEvent) {
this.#storage.setCacheEvent({
id: event.tagId(),
pubkey: event.pubkey,
content: event.content,
// biome-ignore lint/style/noNonNullAssertion: <explanation>
kind: event.kind!,
// biome-ignore lint/style/noNonNullAssertion: <explanation>
createdAt: event.created_at!,
relay: relay?.url,
event: JSON.stringify(event.rawEvent()),
});
// Don't cache contact lists as tags since it's expensive
// and there is no use case for it
if (event.kind !== 3) {
for (const tag of event.tags) {
if (tag[0].length !== 1) return;
this.#storage.setCacheEventTag({
id: `${event.id}:${tag[0]}:${tag[1]}`,
eventId: event.id,
tag: tag[0],
value: tag[1],
tagValue: tag[0] + tag[1],
});
}
}
}
}
}
/**
* Searches by authors
*/
private async byAuthors(
filterKeys: string[],
filter: NDKFilter,
subscription: NDKSubscription,
): Promise<boolean> {
const f = ["authors"];
const hasAllKeys =
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
let foundEvents = false;
if (hasAllKeys && filter.authors) {
for (const pubkey of filter.authors) {
const events = await this.#storage.getCacheEventsByPubkey(pubkey);
for (const event of events) {
let rawEvent: NostrEvent;
try {
rawEvent = JSON.parse(event.event);
} catch (e) {
console.log("failed to parse event", e);
continue;
}
const ndkEvent = new NDKEvent(undefined, rawEvent);
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
subscription.eventReceived(ndkEvent, relay, true);
foundEvents = true;
}
}
}
return foundEvents;
}
/**
* Searches by kinds
*/
private async byKinds(
filterKeys: string[],
filter: NDKFilter,
subscription: NDKSubscription,
): Promise<boolean> {
const f = ["kinds"];
const hasAllKeys =
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
let foundEvents = false;
if (hasAllKeys && filter.kinds) {
for (const kind of filter.kinds) {
const events = await this.#storage.getCacheEventsByKind(kind);
for (const event of events) {
let rawEvent: NostrEvent;
try {
rawEvent = JSON.parse(event.event);
} catch (e) {
console.log("failed to parse event", e);
continue;
}
const ndkEvent = new NDKEvent(undefined, rawEvent);
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
subscription.eventReceived(ndkEvent, relay, true);
foundEvents = true;
}
}
}
return foundEvents;
}
/**
* Searches by ids
*/
private async byIdsQuery(
filterKeys: string[],
filter: NDKFilter,
subscription: NDKSubscription,
): Promise<boolean> {
const f = ["ids"];
const hasAllKeys =
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
if (hasAllKeys && filter.ids) {
for (const id of filter.ids) {
const event = await this.#storage.getCacheEvent(id);
if (!event) continue;
let rawEvent: NostrEvent;
try {
rawEvent = JSON.parse(event.event);
} catch (e) {
console.log("failed to parse event", e);
continue;
}
const ndkEvent = new NDKEvent(undefined, rawEvent);
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
subscription.eventReceived(ndkEvent, relay, true);
}
return true;
}
return false;
}
/**
* Searches by NIP-33
*/
private async byNip33Query(
filterKeys: string[],
filter: NDKFilter,
subscription: NDKSubscription,
): Promise<boolean> {
const f = ["#d", "authors", "kinds"];
const hasAllKeys =
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
if (hasAllKeys && filter.kinds && filter.authors) {
for (const kind of filter.kinds) {
const replaceableKind = kind >= 30000 && kind < 40000;
if (!replaceableKind) continue;
for (const author of filter.authors) {
for (const dTag of filter["#d"]) {
const replaceableId = `${kind}:${author}:${dTag}`;
const event = await this.#storage.getCacheEvent(replaceableId);
if (!event) continue;
let rawEvent: NostrEvent;
try {
rawEvent = JSON.parse(event.event);
} catch (e) {
console.log("failed to parse event", e);
continue;
}
const ndkEvent = new NDKEvent(undefined, rawEvent);
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
subscription.eventReceived(ndkEvent, relay, true);
}
}
}
return true;
}
return false;
}
/**
* Searches by kind & author
*/
private async byKindAndAuthor(
filterKeys: string[],
filter: NDKFilter,
subscription: NDKSubscription,
): Promise<boolean> {
const f = ["authors", "kinds"];
const hasAllKeys =
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
let foundEvents = false;
if (!hasAllKeys) return false;
if (filter.kinds && filter.authors) {
for (const kind of filter.kinds) {
for (const author of filter.authors) {
const events = await this.#storage.getCacheEventsByKindAndAuthor(
kind,
author,
);
for (const event of events) {
let rawEvent: NostrEvent;
try {
rawEvent = JSON.parse(event.event);
} catch (e) {
console.log("failed to parse event", e);
continue;
}
const ndkEvent = new NDKEvent(undefined, rawEvent);
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
subscription.eventReceived(ndkEvent, relay, true);
foundEvents = true;
}
}
}
}
return foundEvents;
}
/**
* Searches by tags and optionally filters by tags
*/
private async byTagsAndOptionallyKinds(
filterKeys: string[],
filter: NDKFilter,
subscription: NDKSubscription,
): Promise<boolean> {
for (const filterKey of filterKeys) {
const isKind = filterKey === "kinds";
const isTag = filterKey.startsWith("#") && filterKey.length === 2;
if (!isKind && !isTag) return false;
}
const events = await this.filterByTag(filterKeys, filter);
const kinds = filter.kinds as number[];
for (const event of events) {
// biome-ignore lint/style/noNonNullAssertion: <explanation>
if (!kinds?.includes(event.kind!)) continue;
subscription.eventReceived(event, undefined, true);
}
return false;
}
private async filterByTag(
filterKeys: string[],
filter: NDKFilter,
): Promise<NDKEvent[]> {
const retEvents: NDKEvent[] = [];
for (const filterKey of filterKeys) {
if (filterKey.length !== 2) continue;
const tag = filterKey.slice(1);
// const values = filter[filterKey] as string[];
const values: string[] = [];
for (const [key, value] of Object.entries(filter)) {
if (key === filterKey) values.push(value as string);
}
for (const value of values) {
const eventTags = await this.#storage.getCacheEventTagsByTagValue(
tag + value,
);
if (!eventTags.length) continue;
const eventIds = eventTags.map((t) => t.eventId);
const events = await this.#storage.getCacheEvents(eventIds);
for (const event of events) {
let rawEvent: NostrEvent;
try {
rawEvent = JSON.parse(event.event);
// Make sure all passed filters match the event
if (!matchFilter(filter, rawEvent)) continue;
} catch (e) {
console.log("failed to parse event", e);
continue;
}
const ndkEvent = new NDKEvent(undefined, rawEvent);
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
ndkEvent.relay = relay;
retEvents.push(ndkEvent);
}
}
}
return retEvents;
}
private async dumpProfiles(): Promise<void> {
const profiles = [];
if (!this.profiles) return;
for (const pubkey of this.dirtyProfiles) {
const profile = this.profiles.get(pubkey);
if (!profile) continue;
profiles.push({
pubkey,
profile: JSON.stringify(profile),
createdAt: Date.now(),
});
}
if (profiles.length) {
await this.#storage.setCacheProfiles(profiles);
}
this.dirtyProfiles.clear();
}
}

View File

@ -1,23 +0,0 @@
{
"name": "@lume/ndk-cache-tauri",
"version": "0.0.0",
"main": "./index.ts",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@lume/storage": "workspace:*",
"@nostr-dev-kit/ndk": "^2.4.0",
"lru-cache": "^10.2.0",
"nostr-fetch": "^0.15.0",
"nostr-tools": "1.17.0",
"react": "^18.2.0"
},
"devDependencies": {
"@lume/tsconfig": "workspace:*",
"@types/react": "^18.2.52",
"typescript": "^5.3.3"
}
}

View File

@ -1,7 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"exclude": ["node_modules", "dist"]
}

View File

@ -9,7 +9,11 @@
},
"dependencies": {
"@tauri-apps/plugin-store": "2.0.0-beta.0",
"react": "^18.2.0"
"react": "^18.2.0",
"scheduler": "^0.23.0",
"use-context-selector": "^1.4.1",
"virtua": "^0.23.3",
"zustand": "^4.5.0"
},
"devDependencies": {
"@lume/tsconfig": "workspace:*",

View File

@ -1,2 +1 @@
export * from "./storage";
export * from "./provider";

View File

@ -1,26 +1,118 @@
import { LumeColumn } from "@lume/types";
import { locale, platform } from "@tauri-apps/plugin-os";
import { Store } from "@tauri-apps/plugin-store";
import { PropsWithChildren, createContext, useContext } from "react";
import {
MutableRefObject,
PropsWithChildren,
useCallback,
useRef,
useState,
} from "react";
import { createContext, useContextSelector } from "use-context-selector";
import { type VListHandle } from "virtua";
import { LumeStorage } from "./storage";
const StorageContext = createContext<LumeStorage>(null);
const store = new Store("lume.data");
const platformName = await platform();
const osLocale = await locale();
const db = new LumeStorage(store, platformName, osLocale);
const store = new Store("lume.dat");
const storage = new LumeStorage(store, platformName, osLocale);
await storage.init();
type StorageContext = {
storage: LumeStorage;
column: {
columns: LumeColumn[];
vlistRef: MutableRefObject<VListHandle>;
create: (column: LumeColumn) => void;
remove: (id: number) => void;
move: (id: number, position: "left" | "right") => void;
update: (id: number, title: string, content: string) => void;
};
};
const StorageContext = createContext<StorageContext>(null);
export const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const vlistRef = useRef<VListHandle>(null);
const [columns, setColumns] = useState<LumeColumn[]>([
{
id: 1,
title: "Newsfeed",
content: "",
},
{
id: 2,
title: "For You",
content: "",
},
]);
const create = useCallback((column: LumeColumn) => {
setColumns((prev) => [...prev, column]);
vlistRef?.current.scrollToIndex(columns.length);
}, []);
const remove = useCallback((id: number) => {
setColumns((prev) => prev.filter((t) => t.id !== id));
}, []);
const update = useCallback(
(id: number, title: string, content: string) => {
const newCols = columns.map((col) => {
if (col.id === id) {
return { ...col, title, content };
}
return col;
});
setColumns(newCols);
},
[columns],
);
const move = useCallback(
(id: number, position: "left" | "right") => {
const newCols = [...columns];
const col = newCols.find((el) => el.id === id);
const colIndex = newCols.findIndex((el) => el.id === id);
newCols.splice(colIndex, 1);
if (position === "left") newCols.splice(colIndex - 1, 0, col);
if (position === "right") newCols.splice(colIndex + 1, 0, col);
setColumns(newCols);
},
[columns],
);
return (
<StorageContext.Provider value={db}>{children}</StorageContext.Provider>
<StorageContext.Provider
value={{
storage,
column: { columns, vlistRef, create, remove, move, update },
}}
>
{children}
</StorageContext.Provider>
);
};
export const useStorage = () => {
const context = useContext(StorageContext);
const context = useContextSelector(StorageContext, (state) => state.storage);
if (context === undefined) {
throw new Error("Please import Storage Provider to use useStorage() hook");
throw new Error("Storage Provider is required");
}
return context;
};
export const useColumn = () => {
const context = useContextSelector(StorageContext, (state) => state.column);
if (context === undefined) {
throw new Error("Storage Provider is required");
}
return context;
};

View File

@ -1,3 +1,4 @@
import { Settings } from "@lume/types";
import { Platform } from "@tauri-apps/plugin-os";
import { Store } from "@tauri-apps/plugin-store";
@ -5,17 +6,7 @@ export class LumeStorage {
#store: Store;
readonly platform: Platform;
readonly locale: string;
public settings: {
autoupdate: boolean;
nsecbunker: boolean;
media: boolean;
hashtag: boolean;
lowPower: boolean;
translation: boolean;
translateApiKey: string;
instantZap: boolean;
defaultZapAmount: number;
};
public settings: Settings;
constructor(store: Store, platform: Platform, locale: string) {
this.#store = store;
@ -34,8 +25,24 @@ export class LumeStorage {
};
}
public async createSetting(key: string, value: string | boolean) {
public async init() {
this.loadSettings();
}
public async loadSettings() {
const settings: Settings = JSON.parse(await this.#store.get("settings"));
for (const [key, value] of Object.entries(settings)) {
this.settings[key] = value;
}
}
public async createSetting(key: string, value: string | number | boolean) {
this.settings[key] = value;
await this.#store.set(this.settings[key], { value });
const settings: Settings = JSON.parse(await this.#store.get("settings"));
const newSettings = { ...settings, key: value };
await this.#store.set("settings", newSettings);
await this.#store.save();
}
}

View File

@ -1,4 +1,14 @@
import { type NDKEvent, type NDKUserProfile } from "@nostr-dev-kit/ndk";
export interface Settings {
autoupdate: boolean;
nsecbunker: boolean;
media: boolean;
hashtag: boolean;
lowPower: boolean;
translation: boolean;
translateApiKey: string;
instantZap: boolean;
defaultZapAmount: number;
}
export interface Keys {
npub: string;
@ -28,16 +38,20 @@ export interface Event {
sig: string;
}
export interface EventWithReplies extends Event {
replies: Array<Event>;
}
export interface Metadata {
name: Option<string>;
display_name: Option<string>;
about: Option<string>;
website: Option<string>;
picture: Option<string>;
banner: Option<string>;
nip05: Option<string>;
lud06: Option<string>;
lud16: Option<string>;
name?: string;
display_name?: string;
about?: string;
website?: string;
picture?: string;
banner?: string;
nip05?: string;
lud06?: string;
lud16?: string;
}
export interface CurrentAccount {
@ -61,9 +75,8 @@ export interface RichContent {
notes: string[];
}
export interface IColumn {
id?: number;
kind: number;
export interface LumeColumn {
id: number;
title: string;
content: string;
}
@ -75,10 +88,6 @@ export interface Opengraph {
image?: string;
}
export interface NDKEventWithReplies extends NDKEvent {
replies: Array<NDKEvent>;
}
export interface NostrBuildResponse {
ok: boolean;
data?: {
@ -99,34 +108,6 @@ export interface NostrBuildResponse {
};
}
export interface NDKCacheUser {
pubkey: string;
profile: string | NDKUserProfile;
createdAt: number;
}
export interface NDKCacheUserProfile extends NDKUserProfile {
npub: string;
}
export interface NDKCacheEvent {
id: string;
pubkey: string;
content: string;
kind: number;
createdAt: number;
relay: string;
event: string;
}
export interface NDKCacheEventTag {
id: string;
eventId: string;
tag: string;
value: string;
tagValue: string;
}
export interface NIP11 {
name: string;
description: string;

View File

@ -10,8 +10,5 @@
},
"devDependencies": {
"typescript": "^5.3.3"
},
"dependencies": {
"@nostr-dev-kit/ndk": "^2.4.0"
}
}

View File

@ -1,20 +1,11 @@
import { useStorage } from "@lume/storage";
import { cn } from "@lume/utils";
import { Outlet } from "react-router-dom";
import { Editor } from "../editor/column";
import { Navigation } from "../navigation";
import { SearchDialog } from "../search/dialog";
export function AppLayout() {
const storage = useStorage();
return (
<div
className={cn(
"flex h-screen w-screen flex-col",
storage.platform !== "macos" ? "bg-neutral-50 dark:bg-neutral-950" : "",
)}
>
<div className="flex h-screen w-screen flex-col bg-gradient-to-tl from-neutral-50 to-neutral-200 dark:from-neutral-950 dark:to-neutral-800">
<div data-tauri-drag-region className="h-9 shrink-0" />
<div className="flex w-full h-full min-h-0">
<Navigation />

View File

@ -25,7 +25,7 @@ const LUME_USERS = [
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445",
];
export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
export function SuggestRoute({ queryKey }: { queryKey: string }) {
const queryClient = useQueryClient();
const navigate = useNavigate();
@ -45,7 +45,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
const submit = async () => {
try {
await queryClient.refetchQueries({ queryKey });
await queryClient.refetchQueries({ queryKey: [queryKey] });
return navigate("/", { replace: true });
} catch (e) {
toast.error(String(e));

View File

@ -1061,6 +1061,18 @@ importers:
react:
specifier: ^18.2.0
version: 18.2.0
scheduler:
specifier: ^0.23.0
version: 0.23.0
use-context-selector:
specifier: ^1.4.1
version: 1.4.1(react@18.2.0)(scheduler@0.23.0)
virtua:
specifier: ^0.23.3
version: 0.23.3(react-dom@18.2.0)(react@18.2.0)
zustand:
specifier: ^4.5.0
version: 4.5.0(@types/react@18.2.52)(react@18.2.0)
devDependencies:
'@lume/tsconfig':
specifier: workspace:*
@ -1100,10 +1112,6 @@ importers:
packages/tsconfig: {}
packages/types:
dependencies:
'@nostr-dev-kit/ndk':
specifier: ^2.4.0
version: 2.4.0(typescript@5.3.3)
devDependencies:
typescript:
specifier: ^5.3.3
@ -9812,6 +9820,23 @@ packages:
tslib: 2.6.2
dev: false
/use-context-selector@1.4.1(react@18.2.0)(scheduler@0.23.0):
resolution: {integrity: sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==}
peerDependencies:
react: '>=16.8.0'
react-dom: '*'
react-native: '*'
scheduler: '>=0.19.0'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
react: 18.2.0
scheduler: 0.23.0
dev: false
/use-debounce@10.0.0(react@18.2.0):
resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==}
engines: {node: '>= 16.0.0'}
@ -9837,6 +9862,14 @@ packages:
tslib: 2.6.2
dev: false
/use-sync-external-store@1.2.0(react@18.2.0):
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/utf-8-validate@5.0.10:
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
engines: {node: '>=6.14.2'}
@ -10395,5 +10428,25 @@ packages:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: false
/zustand@4.5.0(@types/react@18.2.52)(react@18.2.0):
resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
dependencies:
'@types/react': 18.2.52
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}

View File

@ -19,6 +19,9 @@
"notification:default",
"os:allow-locale",
"os:allow-platform",
"updater:allow-check",
"updater:default",
"window:allow-start-dragging",
{
"identifier": "http:default",
"allow": [

View File

@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform",{"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","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","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",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}

View File

@ -1,19 +1,17 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"width": 1080,
"height": 800,
"minWidth": 1080,
"minHeight": 800,
"resizable": true,
"title": "Lume",
"center": true,
"fullscreen": false,
"fileDropEnabled": true,
"decorations": true
}
]
}
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"title": "Lume",
"label": "main",
"titleBarStyle": "Overlay",
"width": 1080,
"height": 800,
"minWidth": 1080,
"minHeight": 800,
"center": true
}
]
}
}

View File

@ -1,27 +1,19 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"width": 1080,
"height": 800,
"minWidth": 1080,
"minHeight": 800,
"resizable": true,
"title": "Lume",
"titleBarStyle": "Overlay",
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"fileDropEnabled": true,
"decorations": true,
"transparent": true,
"windowEffects": {
"effects": [
"sidebar"
]
}
}
]
}
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"title": "Lume",
"label": "main",
"titleBarStyle": "Overlay",
"width": 1080,
"height": 800,
"minWidth": 1080,
"minHeight": 800,
"center": true,
"hiddenTitle": true,
"decorations": true
}
]
}
}

View File

@ -1,20 +1,16 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"width": 1080,
"height": 800,
"minWidth": 1080,
"minHeight": 800,
"resizable": true,
"title": "Lume",
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"fileDropEnabled": true,
"decorations": false
}
]
}
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"title": "Lume",
"label": "main",
"width": 1080,
"height": 800,
"minWidth": 1080,
"minHeight": 800,
"center": true
}
]
}
}