feat: improve performance (#234)

* feat: use negentropy as much as possible

* update

* update
This commit is contained in:
雨宮蓮
2024-09-29 16:53:39 +07:00
committed by GitHub
parent afa9327bb7
commit f0fc89724d
26 changed files with 566 additions and 373 deletions

View File

@@ -1,27 +1,24 @@
import {
type PersistedQuery,
experimental_createPersister,
} from "@tanstack/query-persist-client-core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { type } from "@tauri-apps/plugin-os";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { newQueryStorage } from "./commons";
import { routeTree } from "./routes.gen"; // auto generated file
import type { LumeEvent } from "./system";
import "./app.css";
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
import { Store } from "@tauri-apps/plugin-store";
import { newQueryStorage } from "./commons";
const platform = type();
const tauriStore = new Store(".lume.dat");
const store = new Store(".cache");
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 30,
persister: experimental_createPersister<PersistedQuery>({
storage: newQueryStorage(tauriStore),
maxAge: 1000 * 60 * 60 * 24, // 24 hours,
persister: experimental_createPersister({
storage: newQueryStorage(store),
maxAge: 1000 * 60 * 60 * 12, // 12 hours
}),
},
},

View File

@@ -96,6 +96,9 @@ async resetPassword(key: string, password: string) : Promise<Result<null, string
else return { status: "error", error: e as any };
}
},
async isAccountSync(id: string) : Promise<boolean> {
return await TAURI_INVOKE("is_account_sync", { id });
},
async login(account: string, password: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) };
@@ -136,14 +139,6 @@ async setContactList(publicKeys: string[]) : Promise<Result<boolean, string>> {
else return { status: "error", error: e as any };
}
},
async isContactListEmpty() : Promise<Result<boolean, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("is_contact_list_empty") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async checkContact(hex: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("check_contact", { hex }) };
@@ -320,9 +315,9 @@ async getEventsBy(publicKey: string, limit: number) : Promise<Result<RichEvent[]
else return { status: "error", error: e as any };
}
},
async getEventsFromContacts(until: string | null) : Promise<Result<RichEvent[], string>> {
async getLocalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_events_from_contacts", { until }) };
return { status: "ok", data: await TAURI_INVOKE("get_local_events", { until }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -475,7 +470,7 @@ subscription: "subscription"
export type Column = { label: string; url: string; x: number; y: number; width: number; height: number }
export type Mention = { pubkey: string; avatar: string; display_name: string; name: string }
export type Meta = { content: string; images: string[]; videos: string[]; events: string[]; mentions: string[]; hashtags: string[] }
export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] }
export type NewSettings = Settings
export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null }
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }

View File

@@ -1,5 +1,5 @@
import { LumeWindow } from "@/system";
import { FrameCorners } from "@phosphor-icons/react";
import { ListPlus } from "@phosphor-icons/react";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useNoteContext } from "../provider";
@@ -15,7 +15,7 @@ export function NoteOpenThread() {
onClick={() => LumeWindow.openEvent(event)}
className="group inline-flex h-7 w-14 bg-neutral-100 dark:bg-white/10 rounded-full items-center justify-center text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
>
<FrameCorners className="shrink-0 size-4" />
<ListPlus className="shrink-0 size-4" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>

View File

@@ -7,7 +7,6 @@ import { Hashtag } from "./mentions/hashtag";
import { MentionNote } from "./mentions/note";
import { MentionUser } from "./mentions/user";
import { Images } from "./preview/images";
import { Videos } from "./preview/videos";
import { useNoteContext } from "./provider";
export function NoteContent({
@@ -102,14 +101,9 @@ export function NoteContent({
{content}
</div>
{visible ? (
<>
{event.meta?.images.length ? (
<Images urls={event.meta.images} />
) : null}
{event.meta?.videos.length ? (
<Videos urls={event.meta.videos} />
) : null}
</>
event.meta?.images.length ? (
<Images urls={event.meta.images} />
) : null
) : null}
</div>
);

View File

@@ -6,7 +6,6 @@ import { Hashtag } from "./mentions/hashtag";
import { MentionNote } from "./mentions/note";
import { MentionUser } from "./mentions/user";
import { ImagePreview } from "./preview/image";
import { VideoPreview } from "./preview/video";
import { useNoteContext } from "./provider";
export function NoteContentLarge({
@@ -18,7 +17,7 @@ export function NoteContentLarge({
const content = useMemo(() => {
try {
// Get parsed meta
const { images, videos, hashtags, events, mentions } = event.meta;
const { images, hashtags, events, mentions } = event.meta;
// Define rich content
let richContent: ReactNode[] | string = event.content;
@@ -48,12 +47,6 @@ export function NoteContentLarge({
));
}
for (const video of videos) {
richContent = reactStringReplace(richContent, video, (match, i) => (
<VideoPreview key={match + i} url={match} />
));
}
richContent = reactStringReplace(
richContent,
/(https?:\/\/\S+)/gi,
@@ -75,8 +68,7 @@ export function NoteContentLarge({
));
return richContent;
} catch (e) {
console.log("[parser]: ", e);
} catch {
return event.content;
}
}, [event.content]);

View File

@@ -15,8 +15,7 @@ export function UserAvatar({ className }: { className?: string }) {
const picture = useMemo(() => {
if (service?.length && user.profile?.picture?.length) {
const url = `${service}?url=${user.profile?.picture}&w=100&h=100&default=1&n=-1`;
return url;
return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`;
} else {
return user.profile?.picture;
}

View File

@@ -59,10 +59,10 @@ export function UserFollowButton({ className }: { className?: string }) {
type="button"
disabled={isPending}
onClick={() => toggleFollow()}
className={cn("w-max", className)}
className={cn("w-max gap-1", className)}
>
{isError ? "Error" : null}
{isPending || isLoading ? <Spinner className="size-4" /> : null}
{isPending || isLoading ? <Spinner className="size-3" /> : null}
{isFollow ? "Unfollow" : "Follow"}
</button>
);

View File

@@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as LoadingImport } from './routes/loading'
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
import { Route as IndexImport } from './routes/index'
import { Route as EditorIndexImport } from './routes/editor/index'
@@ -95,6 +96,11 @@ const NewLazyRoute = NewLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
const LoadingRoute = LoadingImport.update({
path: '/loading',
getParentRoute: () => rootRoute,
} as any)
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
path: '/bootstrap-relays',
getParentRoute: () => rootRoute,
@@ -338,6 +344,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof BootstrapRelaysImport
parentRoute: typeof rootRoute
}
'/loading': {
id: '/loading'
path: '/loading'
fullPath: '/loading'
preLoaderRoute: typeof LoadingImport
parentRoute: typeof rootRoute
}
'/new': {
id: '/new'
path: '/new'
@@ -713,6 +726,7 @@ const ColumnsRouteWithChildren =
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/loading': typeof LoadingRoute
'/new': typeof NewLazyRoute
'/reset': typeof ResetLazyRoute
'/$account': typeof AccountSettingsLazyRouteWithChildren
@@ -751,6 +765,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/loading': typeof LoadingRoute
'/new': typeof NewLazyRoute
'/reset': typeof ResetLazyRoute
'/$account': typeof AccountSettingsLazyRouteWithChildren
@@ -790,6 +805,7 @@ export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/loading': typeof LoadingRoute
'/new': typeof NewLazyRoute
'/reset': typeof ResetLazyRoute
'/$account': typeof AccountRouteWithChildren
@@ -833,6 +849,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/bootstrap-relays'
| '/loading'
| '/new'
| '/reset'
| '/$account'
@@ -870,6 +887,7 @@ export interface FileRouteTypes {
to:
| '/'
| '/bootstrap-relays'
| '/loading'
| '/new'
| '/reset'
| '/$account'
@@ -907,6 +925,7 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/bootstrap-relays'
| '/loading'
| '/new'
| '/reset'
| '/$account'
@@ -949,6 +968,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
LoadingRoute: typeof LoadingRoute
NewLazyRoute: typeof NewLazyRoute
ResetLazyRoute: typeof ResetLazyRoute
AccountRoute: typeof AccountRouteWithChildren
@@ -963,6 +983,7 @@ export interface RootRouteChildren {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BootstrapRelaysRoute: BootstrapRelaysRoute,
LoadingRoute: LoadingRoute,
NewLazyRoute: NewLazyRoute,
ResetLazyRoute: ResetLazyRoute,
AccountRoute: AccountRouteWithChildren,
@@ -988,6 +1009,7 @@ export const routeTree = rootRoute
"children": [
"/",
"/bootstrap-relays",
"/loading",
"/new",
"/reset",
"/$account",
@@ -1005,6 +1027,9 @@ export const routeTree = rootRoute
"/bootstrap-relays": {
"filePath": "bootstrap-relays.tsx"
},
"/loading": {
"filePath": "loading.tsx"
},
"/new": {
"filePath": "new.lazy.tsx"
},

View File

@@ -224,7 +224,7 @@ function ReplyList() {
{isLoading ? (
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<Spinner className="size-4" />
<span className="text-sm font-medium">Getting replies...</span>
</div>
</div>

View File

@@ -48,7 +48,7 @@ export function Screen() {
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const until = pageParam > 0 ? pageParam.toString() : undefined;
const res = await commands.getEventsFromContacts(until);
const res = await commands.getLocalEvents(until);
if (res.status === "error") {
throw new Error(res.error);

View File

@@ -11,7 +11,7 @@ export const Route = createLazyFileRoute("/columns/_layout/trending")({
component: Screen,
});
export function Screen() {
function Screen() {
const { isLoading, isError, data } = useQuery({
queryKey: ["trending-notes"],
queryFn: async ({ signal }) => {

View File

@@ -64,11 +64,21 @@ function Screen() {
appSettings.setState(() => settings.data);
}
navigate({
to: "/$account/home",
params: { account: res.data },
replace: true,
});
const status = await commands.isAccountSync(res.data);
if (status) {
navigate({
to: "/$account/home",
params: { account: res.data },
replace: true,
});
} else {
navigate({
to: "/loading",
search: { account: res.data },
replace: true,
});
}
} else {
await message(res.error, { title: "Login", kind: "error" });
return;

47
src/routes/loading.tsx Normal file
View File

@@ -0,0 +1,47 @@
import { Spinner } from "@/components";
import { createFileRoute } from "@tanstack/react-router";
import { listen } from "@tauri-apps/api/event";
import { useEffect } from "react";
type RouteSearch = {
account: string;
};
export const Route = createFileRoute("/loading")({
validateSearch: (search: Record<string, string>): RouteSearch => {
return {
account: search.account,
};
},
component: Screen,
});
function Screen() {
const navigate = Route.useNavigate();
const search = Route.useSearch();
useEffect(() => {
const unlisten = listen("synchronized", () => {
navigate({
to: "/$account/home",
params: { account: search.account },
replace: true,
});
});
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<div className="size-full flex items-center justify-center">
<div className="flex flex-col gap-2 items-center justify-center text-center">
<Spinner />
<p className="text-sm">
Fetching necessary data for the first time login...
</p>
</div>
</div>
);
}

View File

@@ -54,7 +54,7 @@ export function useEvent(id: string) {
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: false,
retry: 2,
});
return { isLoading, isError, error, data };

View File

@@ -38,7 +38,7 @@ export function useProfile(pubkey: string, embed?: string) {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: false,
retry: 2,
});
return { isLoading, isError, profile };

View File

@@ -58,7 +58,7 @@ export const LumeWindow = {
eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1];
const url = `/columns/events/${root ?? reply ?? event.id}`;
const label = `event-${root ?? reply ?? event.id}`;
const label = `event-${root?.substring(0, 6) ?? reply?.substring(0, 6) ?? event.id.substring(0, 6)}`;
LumeWindow.openColumn({ label, url, name: "Thread" });
},

View File

@@ -13,7 +13,6 @@ export enum Kind {
export interface Meta {
content: string;
images: string[];
videos: string[];
events: string[];
mentions: string[];
hashtags: string[];