Merge pull request #148 from luminous-devs/feat/improve-perf

Improve overall performance
This commit is contained in:
Ren Amamiya
2024-01-27 20:01:55 +07:00
committed by GitHub
14 changed files with 168 additions and 261 deletions

View File

@@ -14,7 +14,7 @@
} }
.shadow-toolbar { .shadow-toolbar {
box-shadow: 0 0 #0000,0 0 #0000,0 8px 24px 0 rgba(0,0,0,.2),0 2px 8px 0 rgba(0,0,0,.08),inset 0 0 0 1px rgba(0,0,0,.2),inset 0 0 0 2px hsla(0,0%,100%,.14) box-shadow: 0 0 #0000, 0 0 #0000, 0 8px 24px 0 rgba(0, 0, 0, .2), 0 2px 8px 0 rgba(0, 0, 0, .08), inset 0 0 0 1px rgba(0, 0, 0, .2), inset 0 0 0 2px hsla(0, 0%, 100%, .14)
} }
} }
@@ -42,3 +42,7 @@ input::-ms-clear {
.border { .border {
background-clip: padding-box; background-clip: padding-box;
} }
media-controller {
@apply w-full;
}

View File

@@ -145,7 +145,6 @@ export class Ark {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
}); });
if (!profile) return null;
return profile; return profile;
} catch { } catch {
throw new Error("user not found"); throw new Error("user not found");
@@ -167,8 +166,9 @@ export class Ark {
(user) => user.pubkey, (user) => user.pubkey,
); );
if (!pubkey || pubkey === this.account.pubkey) if (!pubkey || pubkey === this.account.pubkey) {
this.account.contacts = contacts; this.account.contacts = contacts;
}
return contacts; return contacts;
} catch (e) { } catch (e) {

View File

@@ -5,7 +5,6 @@ import {
MediaPlayButton, MediaPlayButton,
MediaTimeDisplay, MediaTimeDisplay,
MediaTimeRange, MediaTimeRange,
MediaVolumeRange,
} from "media-chrome/dist/react"; } from "media-chrome/dist/react";
export function VideoPreview({ url }: { url: string }) { export function VideoPreview({ url }: { url: string }) {
@@ -24,7 +23,6 @@ export function VideoPreview({ url }: { url: string }) {
<MediaTimeRange /> <MediaTimeRange />
<MediaTimeDisplay showDuration /> <MediaTimeDisplay showDuration />
<MediaMuteButton /> <MediaMuteButton />
<MediaVolumeRange />
</MediaControlBar> </MediaControlBar>
</MediaController> </MediaController>
</div> </div>

View File

@@ -39,7 +39,10 @@ export function UserAvatar({ className }: { className?: string }) {
alt={user.pubkey} alt={user.pubkey}
loading="eager" loading="eager"
decoding="async" decoding="async"
className={cn("bg-black dark:bg-white", className)} className={cn(
"bg-black dark:bg-white ring-1 ring-black/5 dark:ring-white/5",
className,
)}
/> />
) : ( ) : (
<Avatar.Image <Avatar.Image
@@ -47,7 +50,7 @@ export function UserAvatar({ className }: { className?: string }) {
alt={user.pubkey} alt={user.pubkey}
loading="eager" loading="eager"
decoding="async" decoding="async"
className={className} className={cn("ring-1 ring-black/5 dark:ring-white/5", className)}
/> />
)} )}
<Avatar.Fallback delayMs={120}> <Avatar.Fallback delayMs={120}>

View File

@@ -13,6 +13,7 @@ export function UserFollowButton({
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
const toggleFollow = async () => { const toggleFollow = async () => {
setLoading(true);
if (!followed) { if (!followed) {
const add = await ark.createContact(target); const add = await ark.createContact(target);
if (add) setFollowed(true); if (add) setFollowed(true);
@@ -20,6 +21,7 @@ export function UserFollowButton({
const remove = await ark.deleteContact(target); const remove = await ark.deleteContact(target);
if (remove) setFollowed(false); if (remove) setFollowed(false);
} }
setLoading(false);
}; };
useEffect(() => { useEffect(() => {
@@ -37,7 +39,12 @@ export function UserFollowButton({
}, []); }, []);
return ( return (
<button type="button" onClick={toggleFollow} className={cn("", className)}> <button
type="button"
disabled={loading}
onClick={toggleFollow}
className={cn("", className)}
>
{loading ? ( {loading ? (
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />
) : followed ? ( ) : followed ? (

View File

@@ -8,7 +8,7 @@ export function UserName({ className }: { className?: string }) {
return ( return (
<div <div
className={cn( className={cn(
"h-4 w-20 bg-black/20 dark:bg-white/20 rounded animate-pulse", "h-4 w-20 self-center bg-black/20 dark:bg-white/20 rounded animate-pulse",
className, className,
)} )}
/> />

View File

@@ -1,8 +1,10 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useArk } from "./useArk"; import { useArk } from "./useArk";
export function useProfile(pubkey: string) { export function useProfile(pubkey: string) {
const ark = useArk(); const ark = useArk();
const queryClient = useQueryClient();
const { const {
isLoading, isLoading,
isError, isError,
@@ -17,6 +19,9 @@ export function useProfile(pubkey: string) {
); );
return profile; return profile;
}, },
initialData: () => {
return queryClient.getQueryData(["user", pubkey]);
},
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,

View File

@@ -23,6 +23,7 @@ import { useSetAtom } from "jotai";
import Linkify from "linkify-react"; import Linkify from "linkify-react";
import { normalizeRelayUrlSet } from "nostr-fetch"; import { normalizeRelayUrlSet } from "nostr-fetch";
import { PropsWithChildren, useEffect, useState } from "react"; import { PropsWithChildren, useEffect, useState } from "react";
import { toast } from "sonner";
import { Ark } from "./ark"; import { Ark } from "./ark";
import { LumeContext } from "./context"; import { LumeContext } from "./context";
@@ -80,24 +81,29 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
return new NDKPrivateKeySigner(userPrivkey); return new NDKPrivateKeySigner(userPrivkey);
} catch (e) { } catch (e) {
console.error(e); toast.error(String(e));
return null; return null;
} }
} }
async function initNDK() { async function initNDK() {
try {
const explicitRelayUrls = normalizeRelayUrlSet([ const explicitRelayUrls = normalizeRelayUrlSet([
"wss://nostr.mutinywallet.com/", "wss://nostr.mutinywallet.com/",
"wss://bostr.nokotaro.com/", "wss://bostr.nokotaro.com/",
"wss://purplepag.es/",
]); ]);
const outboxRelayUrls = normalizeRelayUrlSet(["wss://purplepag.es/"]);
const tauriCache = new NDKCacheAdapterTauri(storage); const tauriCache = new NDKCacheAdapterTauri(storage);
const ndk = new NDK({ const ndk = new NDK({
cacheAdapter: tauriCache, cacheAdapter: tauriCache,
explicitRelayUrls, explicitRelayUrls,
outboxRelayUrls,
enableOutboxModel: !storage.settings.lowPower, enableOutboxModel: !storage.settings.lowPower,
autoConnectUserRelays: !storage.settings.lowPower, autoConnectUserRelays: !storage.settings.lowPower,
autoFetchUserMutelist: !storage.settings.lowPower, autoFetchUserMutelist: false, // #TODO: add support mute list
clientName: "Lume", clientName: "Lume",
}); });
@@ -115,7 +121,10 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
await ndk.connect(3000); await ndk.connect(3000);
// auth // auth
ndk.relayAuthDefaultPolicy = async (relay: NDKRelay, challenge: string) => { ndk.relayAuthDefaultPolicy = async (
relay: NDKRelay,
challenge: string,
) => {
const signIn = NDKRelayAuthPolicies.signIn({ ndk }); const signIn = NDKRelayAuthPolicies.signIn({ ndk });
const event = await signIn(relay, challenge).catch((e) => const event = await signIn(relay, challenge).catch((e) =>
console.log("auth failed", e), console.log("auth failed", e),
@@ -129,6 +138,9 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
}; };
setNDK(ndk); setNDK(ndk);
} catch (e) {
toast.error(String(e));
}
} }
async function initArk() { async function initArk() {
@@ -137,12 +149,25 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
// ark utils // ark utils
const ark = new Ark({ ndk, account: storage.currentUser }); const ark = new Ark({ ndk, account: storage.currentUser });
try {
if (ndk && storage.currentUser) { if (ndk && storage.currentUser) {
const user = new NDKUser({ pubkey: storage.currentUser.pubkey }); const user = new NDKUser({ pubkey: storage.currentUser.pubkey });
ndk.activeUser = user; ndk.activeUser = user;
// update contacts // update contacts
await ark.getUserContacts(); const contacts = await ark.getUserContacts();
if (contacts?.length) {
console.log("total contacts: ", contacts.length);
for (const pubkey of ark.account.contacts) {
await queryClient.prefetchQuery({
queryKey: ["user", pubkey],
queryFn: async () => {
return await ark.getUserProfile(pubkey);
},
});
}
}
// subscribe for new activity // subscribe for new activity
const activitySub = ndk.subscribe( const activitySub = ndk.subscribe(
@@ -183,56 +208,9 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
break; break;
} }
}); });
}
// prefetch activty } catch (e) {
await queryClient.prefetchInfiniteQuery({ toast.error(String(e));
queryKey: ["activity"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap],
"#p": [ark.account.pubkey],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
});
// prefetch timeline
await queryClient.prefetchInfiniteQuery({
queryKey: ["timeline-9999"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
});
} }
setArk(ark); setArk(ark);
@@ -250,7 +228,7 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
className="relative flex items-center justify-center w-screen h-screen bg-neutral-50 dark:bg-neutral-950" className="relative flex items-center justify-center w-screen h-screen"
> >
<div className="flex flex-col items-start max-w-2xl gap-1"> <div className="flex flex-col items-start max-w-2xl gap-1">
<h5 className="font-semibold uppercase">TIP:</h5> <h5 className="font-semibold uppercase">TIP:</h5>

View File

@@ -50,18 +50,7 @@ export function HomeRoute({ colKey }: { colKey: string }) {
if (!lastEvent) return; if (!lastEvent) return;
return lastEvent.created_at - 1; return lastEvent.created_at - 1;
}, },
initialData: () => {
const queryCacheData = queryClient.getQueryState([colKey])
?.data as NDKEvent[];
if (queryCacheData) {
return {
pageParams: [undefined, 1],
pages: [queryCacheData],
};
}
},
select: (data) => data?.pages.flatMap((page) => page), select: (data) => data?.pages.flatMap((page) => page),
staleTime: 120 * 1000,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
}); });
@@ -115,6 +104,17 @@ export function HomeRoute({ colKey }: { colKey: string }) {
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5"> <div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" /> <LoaderIcon className="size-5 animate-spin" />
</div> </div>
) : !data.length ? (
<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>
) : ( ) : (
data.map((item) => renderItem(item)) data.map((item) => renderItem(item))
)} )}

View File

@@ -356,7 +356,7 @@ export function EditorForm() {
<Portal> <Portal>
<div <div
ref={ref} ref={ref}
className="top-[-9999px] left-[-9999px] absolute z-10 w-[250px] p-1 bg-white border border-neutral-50 dark:border-neutral-900 dark:bg-neutral-950 rounded-lg shadow-lg" className="top-[-9999px] left-[-9999px] absolute z-10 w-[250px] p-2 bg-white border border-neutral-50 dark:border-neutral-900 dark:bg-neutral-950 rounded-xl shadow-lg"
> >
{filters.map((contact, i) => ( {filters.map((contact, i) => (
<button <button
@@ -367,13 +367,13 @@ export function EditorForm() {
insertMention(editor, contact); insertMention(editor, contact);
setTarget(null); setTarget(null);
}} }}
className="px-2 py-2 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-900" className="p-2 flex flex-col w-full rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-900"
> >
<User.Provider pubkey={contact.npub}> <User.Provider pubkey={contact.npub}>
<User.Root className="flex items-center gap-2.5"> <User.Root className="w-full flex items-center gap-2.5">
<User.Avatar className="size-10 rounded-lg object-cover shrink-0" /> <User.Avatar className="size-8 rounded-lg object-cover shrink-0" />
<div className="flex w-full flex-col items-start"> <div className="flex w-full flex-col items-start">
<User.Name className="max-w-[15rem] truncate font-semibold" /> <User.Name className="max-w-[8rem] truncate text-sm font-medium" />
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>

View File

@@ -1,5 +1,5 @@
import { useArk } from "@lume/ark";
import { CheckIcon, LoaderIcon } from "@lume/icons"; import { CheckIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils"; import { onboardingAtom } from "@lume/utils";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@@ -7,6 +7,7 @@ import { useSetAtom } from "jotai";
import { useState } from "react"; import { useState } from "react";
export function OnboardingFinishScreen() { export function OnboardingFinishScreen() {
const storage = useStorage();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setOnboarding = useSetAtom(onboardingAtom); const setOnboarding = useSetAtom(onboardingAtom);
@@ -15,8 +16,9 @@ export function OnboardingFinishScreen() {
const finish = async () => { const finish = async () => {
setLoading(true); setLoading(true);
await queryClient.refetchQueries({ queryKey: ["timeline-9999"] }); if (storage.interests) {
await queryClient.refetchQueries({ queryKey: ["foryou-9998"] }); await queryClient.invalidateQueries({ queryKey: ["foryou-9998"] });
}
setLoading(false); setLoading(false);
setOnboarding({ open: false, newUser: false }); setOnboarding({ open: false, newUser: false });

View File

@@ -35,9 +35,10 @@ export function OnboardingInterestScreen() {
JSON.stringify({ hashtags }), JSON.stringify({ hashtags }),
); );
setLoading(false); if (save) {
storage.interests = { hashtags, users: [], words: [] };
if (save) return navigate("/finish"); return navigate("/finish");
}
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(String(e)); toast.error(String(e));

View File

@@ -1,15 +1,6 @@
import { User, useArk } from "@lume/ark"; import { User } from "@lume/ark";
import { import { ArrowLeftIcon, ArrowRightIcon, LoaderIcon } from "@lume/icons";
ArrowLeftIcon,
ArrowRightIcon,
CancelIcon,
LoaderIcon,
PlusIcon,
} from "@lume/icons";
import { cn } from "@lume/utils";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { WindowVirtualizer } from "virtua"; import { WindowVirtualizer } from "virtua";
@@ -34,7 +25,6 @@ const LUME_USERS = [
]; ];
export function SuggestRoute({ queryKey }: { queryKey: string[] }) { export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
const ark = useArk();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -51,40 +41,11 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
}, },
}); });
const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState<string[]>([]);
// toggle follow state
const toggleFollow = (pubkey: string) => {
const arr = follows.includes(pubkey)
? follows.filter((i) => i !== pubkey)
: [...follows, pubkey];
setFollows(arr);
};
const submit = async () => { const submit = async () => {
try { try {
setLoading(true); await queryClient.refetchQueries({ queryKey });
return navigate("/", { replace: true });
if (!follows.length) return navigate("/");
const publish = await ark.newContactList({
tags: follows.map((item) => {
if (item.startsWith("npub1"))
return ["p", nip19.decode(item).data as string];
return ["p", item];
}),
});
if (publish) {
await queryClient.refetchQueries({ queryKey: ["timeline-9999"] });
}
setLoading(false);
return navigate("/");
} catch (e) { } catch (e) {
setLoading(false);
toast.error(String(e)); toast.error(String(e));
} }
}; };
@@ -135,30 +96,12 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
<User.Avatar className="size-10 shrink-0 rounded-lg" /> <User.Avatar className="size-10 shrink-0 rounded-lg" />
<User.Name className="max-w-[15rem] truncate font-semibold leadning-tight" /> <User.Name className="max-w-[15rem] truncate font-semibold leadning-tight" />
</div> </div>
<button <User.Button
type="button" target={item.pubkey}
onClick={() => toggleFollow(item.pubkey)} className="w-20 h-8 text-sm font-medium bg-neutral-100 dark:bg-neutral-900 hover:bg-neutral-200 dark:hover:bg-neutral-800 rounded-lg inline-flex items-center justify-center"
className={cn( />
"inline-flex h-8 shrink-0 pl-2 pr-2.5 items-center justify-center gap-1 rounded-lg text-sm font-medium",
follows.includes(item.pubkey)
? "text-red-500 bg-red-100 hover:text-white hover:bg-red-500"
: "text-blue-500 bg-blue-100 hover:text-white hover:bg-blue-500",
)}
>
{follows.includes(item.pubkey) ? (
<>
<CancelIcon className="size-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="size-4" />
Follow
</>
)}
</button>
</div> </div>
<User.About className="break-p text-neutral-800 dark:text-neutral-400 max-w-none select-text whitespace-pre-line" /> <User.About className="mt-1 line-clamp-3 text-neutral-800 dark:text-neutral-400 max-w-none select-text" />
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>
@@ -170,10 +113,9 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
<button <button
type="button" type="button"
onClick={submit} onClick={submit}
disabled={loading} className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl dark:shadow-none shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-44 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
> >
Save & Go Back Save & Go back
</button> </button>
</div> </div>
</div> </div>

View File

@@ -26,7 +26,7 @@ export const NOSTR_EVENTS = [
"Nostr:nevent1", "Nostr:nevent1",
]; ];
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q']; export const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
export const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]; export const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
@@ -45,37 +45,6 @@ export const VIDEOS = [
export const AUDIOS = ["mp3", "ogg", "wav"]; export const AUDIOS = ["mp3", "ogg", "wav"];
export const HASHTAGS = [
{ hashtag: "#food" },
{ hashtag: "#gaming" },
{ hashtag: "#nsfw" },
{ hashtag: "#bitcoin" },
{ hashtag: "#nostr" },
{ hashtag: "#nostrdesign" },
{ hashtag: "#security" },
{ hashtag: "#zap" },
{ hashtag: "#LFG" },
{ hashtag: "#zapchain" },
{ hashtag: "#shitcoin" },
{ hashtag: "#plebchain" },
{ hashtag: "#nodes" },
{ hashtag: "#hodl" },
{ hashtag: "#stacksats" },
{ hashtag: "#nokyc" },
{ hashtag: "#meme" },
{ hashtag: "#memes" },
{ hashtag: "#memestr" },
{ hashtag: "#nostriches" },
{ hashtag: "#dev" },
{ hashtag: "#anime" },
{ hashtag: "#waifu" },
{ hashtag: "#manga" },
{ hashtag: "#lume" },
{ hashtag: "#snort" },
{ hashtag: "#damus" },
{ hashtag: "#primal" },
];
export const COL_TYPES = { export const COL_TYPES = {
default: 0, default: 0,
user: 1, user: 1,
@@ -173,7 +142,6 @@ export const TOPICS = [
"#pcgaming", "#pcgaming",
"#nintendo", "#nintendo",
"#switch", "#switch",
"#pubg",
"#esports", "#esports",
"#gameoftheyear", "#gameoftheyear",
"#darksoul", "#darksoul",
@@ -326,7 +294,6 @@ export const TOPICS = [
"#fashion", "#fashion",
"#travel", "#travel",
"#photoshoot", "#photoshoot",
"#nature",
"#naturephotography", "#naturephotography",
"#smile", "#smile",
"#style", "#style",