mirror of https://github.com/usememos/memos.git
refactor: optimize user fetching in MemoCommentMessage and MemoReactionListView components
This commit is contained in:
parent
115d1bacd7
commit
955ff0cad6
|
|
@ -8,10 +8,11 @@ import { activityServiceClient, memoServiceClient, userServiceClient } from "@/c
|
|||
import { activityNamePrefix } from "@/helpers/resource-names";
|
||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { useUser } from "@/hooks/useUserQueries";
|
||||
import { handleError } from "@/lib/error";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { User, UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
|
||||
import { UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -23,10 +24,12 @@ function MemoCommentMessage({ notification }: Props) {
|
|||
const navigateTo = useNavigateTo();
|
||||
const [relatedMemo, setRelatedMemo] = useState<Memo | undefined>(undefined);
|
||||
const [commentMemo, setCommentMemo] = useState<Memo | undefined>(undefined);
|
||||
const [sender, setSender] = useState<User | undefined>(undefined);
|
||||
const [senderName, setSenderName] = useState<string | undefined>(undefined);
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [hasError, setHasError] = useState<boolean>(false);
|
||||
|
||||
const { data: sender } = useUser(senderName || "", { enabled: !!senderName });
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (!notification.activityId) {
|
||||
return;
|
||||
|
|
@ -44,16 +47,12 @@ function MemoCommentMessage({ notification }: Props) {
|
|||
});
|
||||
setRelatedMemo(memo);
|
||||
|
||||
// Fetch the comment memo
|
||||
const comment = await memoServiceClient.getMemo({
|
||||
name: memoCommentPayload.memo,
|
||||
});
|
||||
setCommentMemo(comment);
|
||||
|
||||
const sender = await userServiceClient.getUser({
|
||||
name: notification.sender,
|
||||
});
|
||||
setSender(sender);
|
||||
setSenderName(notification.sender);
|
||||
setInitialized(true);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,30 @@
|
|||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { uniq } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { memoServiceClient, userServiceClient } from "@/connect";
|
||||
import { useMemo } from "react";
|
||||
import { memoServiceClient } from "@/connect";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { memoKeys } from "@/hooks/useMemoQueries";
|
||||
import { useUsersByNames } from "@/hooks/useUserQueries";
|
||||
import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import type { User } from "@/types/proto/api/v1/user_service_pb";
|
||||
|
||||
export type ReactionGroup = Map<string, User[]>;
|
||||
|
||||
export const useReactionGroups = (reactions: Reaction[]): ReactionGroup => {
|
||||
const [reactionGroup, setReactionGroup] = useState<ReactionGroup>(new Map());
|
||||
const creatorNames = useMemo(() => reactions.map((r) => r.creator), [reactions]);
|
||||
const { data: userMap } = useUsersByNames(creatorNames);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchReactionGroups = async () => {
|
||||
const newReactionGroup = new Map<string, User[]>();
|
||||
for (const reaction of reactions) {
|
||||
// Fetch user via gRPC directly since we need it within an effect
|
||||
const user = await userServiceClient.getUser({ name: reaction.creator });
|
||||
const users = newReactionGroup.get(reaction.reactionType) || [];
|
||||
users.push(user);
|
||||
newReactionGroup.set(reaction.reactionType, uniq(users));
|
||||
}
|
||||
setReactionGroup(newReactionGroup);
|
||||
};
|
||||
fetchReactionGroups();
|
||||
}, [reactions]);
|
||||
return useMemo(() => {
|
||||
const reactionGroup = new Map<string, User[]>();
|
||||
for (const reaction of reactions) {
|
||||
const user = userMap?.get(reaction.creator);
|
||||
if (!user) continue;
|
||||
|
||||
return reactionGroup;
|
||||
const users = reactionGroup.get(reaction.reactionType) || [];
|
||||
users.push(user);
|
||||
reactionGroup.set(reaction.reactionType, users);
|
||||
}
|
||||
return reactionGroup;
|
||||
}, [reactions, userMap]);
|
||||
};
|
||||
|
||||
interface UseReactionActionsOptions {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ArrowUpIcon, LoaderIcon } from "lucide-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowUpIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { matchPath } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -6,6 +7,7 @@ import { userServiceClient } from "@/connect";
|
|||
import { useView } from "@/contexts/ViewContext";
|
||||
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
|
||||
import { useInfiniteMemos } from "@/hooks/useMemoQueries";
|
||||
import { userKeys } from "@/hooks/useUserQueries";
|
||||
import { Routes } from "@/router";
|
||||
import { State } from "@/types/proto/api/v1/common_pb";
|
||||
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
|
|
@ -81,6 +83,7 @@ function useAutoFetchWhenNotScrollable({
|
|||
const PagedMemoList = (props: Props) => {
|
||||
const t = useTranslate();
|
||||
const { layout } = useView();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Show memo editor only on the root route
|
||||
const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname));
|
||||
|
|
@ -99,7 +102,7 @@ const PagedMemoList = (props: Props) => {
|
|||
// Apply custom sorting if provided, otherwise use memos directly
|
||||
const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);
|
||||
|
||||
// Batch-fetch creators when new data arrives to improve performance
|
||||
// Prefetch creators when new data arrives to improve performance
|
||||
useEffect(() => {
|
||||
if (!data?.pages || !props.showCreator) return;
|
||||
|
||||
|
|
@ -107,14 +110,17 @@ const PagedMemoList = (props: Props) => {
|
|||
if (!lastPage?.memos) return;
|
||||
|
||||
const uniqueCreators = Array.from(new Set(lastPage.memos.map((memo) => memo.creator)));
|
||||
void Promise.allSettled(
|
||||
uniqueCreators.map((creator) =>
|
||||
userServiceClient.getUser({ name: creator }).catch(() => {
|
||||
/* silently ignore errors */
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, [data?.pages, props.showCreator]);
|
||||
for (const creator of uniqueCreators) {
|
||||
void queryClient.prefetchQuery({
|
||||
queryKey: userKeys.detail(creator),
|
||||
queryFn: async () => {
|
||||
const user = await userServiceClient.getUser({ name: creator });
|
||||
return user;
|
||||
},
|
||||
staleTime: 1000 * 60 * 5,
|
||||
});
|
||||
}
|
||||
}, [data?.pages, props.showCreator, queryClient]);
|
||||
|
||||
// Auto-fetch hook: fetches more content when page isn't scrollable
|
||||
useAutoFetchWhenNotScrollable({
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const userKeys = {
|
|||
currentUser: () => [...userKeys.all, "current"] as const,
|
||||
shortcuts: () => [...userKeys.all, "shortcuts"] as const,
|
||||
notifications: () => [...userKeys.all, "notifications"] as const,
|
||||
byNames: (names: string[]) => [...userKeys.all, "byNames", ...names.sort()] as const,
|
||||
};
|
||||
|
||||
// NOTE: This hook is currently UNUSED in favor of the AuthContext-based
|
||||
|
|
@ -226,3 +227,33 @@ export function useUpdateUserGeneralSetting(currentUserName?: string) {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Hook to fetch multiple users by names (returns Map<name, User>)
|
||||
export function useUsersByNames(names: string[]) {
|
||||
const enabled = names.length > 0;
|
||||
const uniqueNames = Array.from(new Set(names));
|
||||
|
||||
return useQuery({
|
||||
queryKey: userKeys.byNames(uniqueNames),
|
||||
queryFn: async () => {
|
||||
const users = await Promise.all(
|
||||
uniqueNames.map(async (name) => {
|
||||
try {
|
||||
const user = await userServiceClient.getUser({ name });
|
||||
return { name, user };
|
||||
} catch {
|
||||
return { name, user: undefined };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const userMap = new Map<string, User | undefined>();
|
||||
for (const { name, user } of users) {
|
||||
userMap.set(name, user);
|
||||
}
|
||||
return userMap;
|
||||
},
|
||||
enabled,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes - user profiles don't change often
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue