chore: clean duplicated requests

pull/2209/head
Steven 2 years ago
parent ca336af4fa
commit 866937787c

@ -5,8 +5,8 @@ import Icon from "./Icon";
interface Props { interface Props {
value: Locale; value: Locale;
onChange: (locale: Locale) => void;
className?: string; className?: string;
onChange: (locale: Locale) => void;
} }
const LocaleSelect: FC<Props> = (props: Props) => { const LocaleSelect: FC<Props> = (props: Props) => {

@ -229,7 +229,7 @@ const Memo: React.FC<Props> = (props: Props) => {
<> <>
<Link className="flex flex-row justify-start items-center" to={`/u/${memo.creatorUsername}`}> <Link className="flex flex-row justify-start items-center" to={`/u/${memo.creatorUsername}`}>
<UserAvatar className="!w-5 !h-auto mr-1" avatarUrl={creator.avatarUrl} /> <UserAvatar className="!w-5 !h-auto mr-1" avatarUrl={creator.avatarUrl} />
<span className="text-sm text-gray-600 max-w-[8em] truncate dark:text-zinc-300">{creator.nickname}</span> <span className="text-sm text-gray-600 max-w-[8em] truncate dark:text-gray-400">{creator.nickname}</span>
</Link> </Link>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" /> <Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
</> </>

@ -15,7 +15,8 @@ const MemoList: React.FC = () => {
const userStore = useUserStore(); const userStore = useUserStore();
const filterStore = useFilterStore(); const filterStore = useFilterStore();
const filter = filterStore.state; const filter = filterStore.state;
const { memos, isFetching } = memoStore.state; const { memos } = memoStore.state;
const [isFetching, setIsFetching] = useState<boolean>(true);
const [isComplete, setIsComplete] = useState<boolean>(false); const [isComplete, setIsComplete] = useState<boolean>(false);
const currentUsername = userStore.getCurrentUsername(); const currentUsername = userStore.getCurrentUsername();
@ -82,6 +83,7 @@ const MemoList: React.FC = () => {
} else { } else {
setIsComplete(false); setIsComplete(false);
} }
setIsFetching(false);
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
@ -122,12 +124,14 @@ const MemoList: React.FC = () => {
const handleFetchMoreClick = async () => { const handleFetchMoreClick = async () => {
try { try {
setIsFetching(true);
const fetchedMemos = await memoStore.fetchMemos(DEFAULT_MEMO_LIMIT, memos.length); const fetchedMemos = await memoStore.fetchMemos(DEFAULT_MEMO_LIMIT, memos.length);
if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) { if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true); setIsComplete(true);
} else { } else {
setIsComplete(false); setIsComplete(false);
} }
setIsFetching(false);
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
toast.error(error.response.data.message); toast.error(error.response.data.message);

@ -136,17 +136,17 @@ const PreferencesSection = () => {
<div className="inline-block min-w-full align-middle"> <div className="inline-block min-w-full align-middle">
<table className="min-w-full divide-y divide-gray-300"> <table className="min-w-full divide-y divide-gray-300">
<thead> <thead>
<tr> <tr className="text-sm font-semibold text-left text-gray-900 dark:text-gray-300">
<th scope="col" className="py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900"> <th scope="col" className="py-2 pl-4 pr-3">
ID ID
</th> </th>
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-gray-900"> <th scope="col" className="px-3 py-2">
{t("common.username")} {t("common.username")}
</th> </th>
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-gray-900"> <th scope="col" className="px-3 py-2">
{t("common.nickname")} {t("common.nickname")}
</th> </th>
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-gray-900"> <th scope="col" className="px-3 py-2">
{t("common.email")} {t("common.email")}
</th> </th>
<th scope="col" className="relative py-2 pl-3 pr-4"></th> <th scope="col" className="relative py-2 pl-3 pr-4"></th>
@ -155,13 +155,13 @@ const PreferencesSection = () => {
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
{userList.map((user) => ( {userList.map((user) => (
<tr key={user.id}> <tr key={user.id}>
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900">{user.id}</td> <td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900 dark:text-gray-300">{user.id}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-300">
{user.username} {user.username}
<span className="ml-1 italic">{user.rowStatus === "ARCHIVED" && "(Archived)"}</span> <span className="ml-1 italic">{user.rowStatus === "ARCHIVED" && "(Archived)"}</span>
</td> </td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.nickname}</td> <td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-300">{user.nickname}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.email}</td> <td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-300">{user.email}</td>
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end"> <td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end">
{currentUser?.id === user.id ? ( {currentUser?.id === user.id ? (
<span>{t("common.yourself")}</span> <span>{t("common.yourself")}</span>

@ -3,6 +3,7 @@ import { getMemoStats } from "@/helpers/api";
import { DAILY_TIMESTAMP } from "@/helpers/consts"; import { DAILY_TIMESTAMP } from "@/helpers/consts";
import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers/datetime"; import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers/datetime";
import * as utils from "@/helpers/utils"; import * as utils from "@/helpers/utils";
import { useUserV1Store } from "@/store/v1";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { useFilterStore, useMemoStore, useUserStore } from "../store/module"; import { useFilterStore, useMemoStore, useUserStore } from "../store/module";
import "@/less/usage-heat-map.less"; import "@/less/usage-heat-map.less";
@ -32,6 +33,7 @@ const UsageHeatMap = () => {
const t = useTranslate(); const t = useTranslate();
const filterStore = useFilterStore(); const filterStore = useFilterStore();
const userStore = useUserStore(); const userStore = useUserStore();
const userV1Store = useUserV1Store();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const todayTimeStamp = getDateStampByDate(Date.now()); const todayTimeStamp = getDateStampByDate(Date.now());
const todayDay = new Date(todayTimeStamp).getDay() + 1; const todayDay = new Date(todayTimeStamp).getDay() + 1;
@ -47,7 +49,7 @@ const UsageHeatMap = () => {
const currentUsername = userStore.getCurrentUsername(); const currentUsername = userStore.getCurrentUsername();
useEffect(() => { useEffect(() => {
userStore.getUserByUsername(currentUsername).then((user) => { userV1Store.getOrFetchUserByUsername(currentUsername).then((user) => {
if (!user) { if (!user) {
return; return;
} }
@ -56,6 +58,10 @@ const UsageHeatMap = () => {
}, [currentUsername]); }, [currentUsername]);
useEffect(() => { useEffect(() => {
if (memos.length === 0) {
return;
}
getMemoStats(currentUsername) getMemoStats(currentUsername)
.then(({ data }) => { .then(({ data }) => {
setMemoAmount(data.length); setMemoAmount(data.length);

@ -9,7 +9,11 @@ const UserAvatar = (props: Props) => {
const { avatarUrl, className } = props; const { avatarUrl, className } = props;
return ( return (
<div className={classNames(`w-8 h-auto overflow-clip rounded-full`, className)}> <div className={classNames(`w-8 h-auto overflow-clip rounded-full`, className)}>
<img className="w-full h-auto rounded-full min-w-full min-h-full object-cover" src={avatarUrl || "/logo.webp"} alt="" /> <img
className="w-full h-auto rounded-full min-w-full min-h-full object-cover dark:opacity-80"
src={avatarUrl || "/logo.webp"}
alt=""
/>
</div> </div>
); );
}; };

@ -1,9 +1,6 @@
html, html,
body { body {
@apply text-base w-full h-full overflow-hidden dark:bg-zinc-800; @apply text-base w-full h-full overflow-hidden dark:bg-zinc-800;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Noto Sans",
"Noto Sans CJK SC", "Microsoft YaHei UI", "Microsoft YaHei", "WenQuanYi Micro Hei", "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji", sans-serif;
} }
#root { #root {

@ -143,7 +143,7 @@ const Auth = () => {
className="w-full" className="w-full"
size="lg" size="lg"
type="text" type="text"
disabled={actionBtnLoadingState.isLoading} readOnly={actionBtnLoadingState.isLoading}
placeholder={t("common.username")} placeholder={t("common.username")}
value={username} value={username}
onChange={handleUsernameInputChanged} onChange={handleUsernameInputChanged}
@ -153,7 +153,7 @@ const Auth = () => {
className="w-full" className="w-full"
size="lg" size="lg"
type="password" type="password"
disabled={actionBtnLoadingState.isLoading} readOnly={actionBtnLoadingState.isLoading}
placeholder={t("common.password")} placeholder={t("common.password")}
value={password} value={password}
onChange={handlePasswordInputChanged} onChange={handlePasswordInputChanged}

@ -6,17 +6,19 @@ import MemoFilter from "@/components/MemoFilter";
import MemoList from "@/components/MemoList"; import MemoList from "@/components/MemoList";
import MobileHeader from "@/components/MobileHeader"; import MobileHeader from "@/components/MobileHeader";
import { useGlobalStore, useUserStore } from "@/store/module"; import { useGlobalStore, useUserStore } from "@/store/module";
import { useUserV1Store } from "@/store/v1";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
const Home = () => { const Home = () => {
const t = useTranslate(); const t = useTranslate();
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const userStore = useUserStore(); const userStore = useUserStore();
const userV1Store = useUserV1Store();
const user = userStore.state.user; const user = userStore.state.user;
useEffect(() => { useEffect(() => {
const currentUsername = userStore.getCurrentUsername(); const currentUsername = userStore.getCurrentUsername();
userStore.getUserByUsername(currentUsername).catch((error) => { userV1Store.getOrFetchUserByUsername(currentUsername).catch((error) => {
console.error(error); console.error(error);
toast.error(t("message.user-not-found")); toast.error(t("message.user-not-found"));
}); });

@ -5,12 +5,13 @@ import FloatingNavButton from "@/components/FloatingNavButton";
import Memo from "@/components/Memo"; import Memo from "@/components/Memo";
import UserAvatar from "@/components/UserAvatar"; import UserAvatar from "@/components/UserAvatar";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
import { useMemoStore, useUserStore } from "@/store/module"; import { useMemoStore } from "@/store/module";
import { useUserV1Store } from "@/store/v1";
const MemoDetail = () => { const MemoDetail = () => {
const params = useParams(); const params = useParams();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const userStore = useUserStore(); const userV1Store = useUserV1Store();
const loadingState = useLoading(); const loadingState = useLoading();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
const memoId = Number(params.memoId); const memoId = Number(params.memoId);
@ -21,7 +22,7 @@ const MemoDetail = () => {
memoStore memoStore
.fetchMemoById(memoId) .fetchMemoById(memoId)
.then(async (memo) => { .then(async (memo) => {
const user = await userStore.getUserByUsername(memo.creatorUsername); const user = await userV1Store.getOrFetchUserByUsername(memo.creatorUsername);
setUser(user); setUser(user);
loadingState.setFinish(); loadingState.setFinish();
}) })

@ -6,18 +6,20 @@ import MemoList from "@/components/MemoList";
import UserAvatar from "@/components/UserAvatar"; import UserAvatar from "@/components/UserAvatar";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
import { useUserStore } from "@/store/module"; import { useUserStore } from "@/store/module";
import { useUserV1Store } from "@/store/v1";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
const UserProfile = () => { const UserProfile = () => {
const t = useTranslate(); const t = useTranslate();
const userStore = useUserStore(); const userStore = useUserStore();
const userV1Store = useUserV1Store();
const loadingState = useLoading(); const loadingState = useLoading();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
useEffect(() => { useEffect(() => {
const currentUsername = userStore.getCurrentUsername(); const currentUsername = userStore.getCurrentUsername();
userStore userV1Store
.getUserByUsername(currentUsername) .getOrFetchUserByUsername(currentUsername)
.then((user) => { .then((user) => {
setUser(user); setUser(user);
loadingState.setFinish(); loadingState.setFinish();

@ -2,7 +2,7 @@ import { omit } from "lodash-es";
import * as api from "@/helpers/api"; import * as api from "@/helpers/api";
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
import store, { useAppSelector } from "../"; import store, { useAppSelector } from "../";
import { createMemo, deleteMemo, patchMemo, setIsFetching, upsertMemos } from "../reducer/memo"; import { createMemo, deleteMemo, patchMemo, upsertMemos } from "../reducer/memo";
import { useMemoCacheStore } from "../v1"; import { useMemoCacheStore } from "../v1";
import { useUserStore } from "./"; import { useUserStore } from "./";
@ -34,7 +34,6 @@ export const useMemoStore = () => {
return store.getState().memo; return store.getState().memo;
}, },
fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => { fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => {
store.dispatch(setIsFetching(true));
const memoFind: MemoFind = { const memoFind: MemoFind = {
rowStatus: "NORMAL", rowStatus: "NORMAL",
limit, limit,
@ -46,26 +45,20 @@ export const useMemoStore = () => {
const { data } = await api.getMemoList(memoFind); const { data } = await api.getMemoList(memoFind);
const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
store.dispatch(upsertMemos(fetchedMemos)); store.dispatch(upsertMemos(fetchedMemos));
store.dispatch(setIsFetching(false));
for (const m of fetchedMemos) { for (const m of fetchedMemos) {
memoCacheStore.setMemoCache(m); memoCacheStore.setMemoCache(m);
} }
return fetchedMemos; return fetchedMemos;
}, },
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => { fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => {
store.dispatch(setIsFetching(true));
const memoFind: MemoFind = { const memoFind: MemoFind = {
rowStatus: "NORMAL", rowStatus: "NORMAL",
limit, limit,
offset, offset,
}; };
const { data } = await api.getAllMemos(memoFind); const { data } = await api.getAllMemos(memoFind);
const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
store.dispatch(upsertMemos(fetchedMemos)); store.dispatch(upsertMemos(fetchedMemos));
store.dispatch(setIsFetching(false));
for (const m of fetchedMemos) { for (const m of fetchedMemos) {
memoCacheStore.setMemoCache(m); memoCacheStore.setMemoCache(m);

@ -5,7 +5,7 @@ import storage from "@/helpers/storage";
import { getSystemColorScheme } from "@/helpers/utils"; import { getSystemColorScheme } from "@/helpers/utils";
import store, { useAppSelector } from ".."; import store, { useAppSelector } from "..";
import { setAppearance, setLocale } from "../reducer/global"; import { setAppearance, setLocale } from "../reducer/global";
import { patchUser, setHost, setUser, setUserById } from "../reducer/user"; import { patchUser, setHost, setUser } from "../reducer/user";
const defaultSetting: Setting = { const defaultSetting: Setting = {
locale: "en", locale: "en",
@ -118,16 +118,6 @@ export const useUserStore = () => {
return state.user?.username || UNKNOWN_USERNAME; return state.user?.username || UNKNOWN_USERNAME;
} }
}, },
getUserByUsername: async (username: string) => {
const { data } = await api.getUserByUsername(username);
if (data) {
const user = convertResponseModelUser(data);
store.dispatch(setUserById(user));
return user;
} else {
return undefined;
}
},
upsertUserSetting: async (key: string, value: any) => { upsertUserSetting: async (key: string, value: any) => {
await api.upsertUserSetting({ await api.upsertUserSetting({
key: key as any, key: key as any,

@ -3,15 +3,12 @@ import { uniqBy } from "lodash-es";
interface State { interface State {
memos: Memo[]; memos: Memo[];
isFetching: boolean;
} }
const memoSlice = createSlice({ const memoSlice = createSlice({
name: "memo", name: "memo",
initialState: { initialState: {
memos: [], memos: [],
// isFetching flag should starts with true.
isFetching: true,
} as State, } as State,
reducers: { reducers: {
upsertMemos: (state, action: PayloadAction<Memo[]>) => { upsertMemos: (state, action: PayloadAction<Memo[]>) => {
@ -51,15 +48,9 @@ const memoSlice = createSlice({
}), }),
}; };
}, },
setIsFetching: (state, action: PayloadAction<boolean>) => {
return {
...state,
isFetching: action.payload,
};
},
}, },
}); });
export const { upsertMemos, createMemo, patchMemo, deleteMemo, setIsFetching } = memoSlice.actions; export const { upsertMemos, createMemo, patchMemo, deleteMemo } = memoSlice.actions;
export default memoSlice.reducer; export default memoSlice.reducer;

@ -1,19 +1,15 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { cloneDeep } from "lodash-es";
interface State { interface State {
// host is the user who hist the system // host is the user who hist the system
host?: User; host?: User;
// user is the user who is currently logged in // user is the user who is currently logged in
user?: User; user?: User;
userById: { [key: UserId]: User };
} }
const userSlice = createSlice({ const userSlice = createSlice({
name: "user", name: "user",
initialState: { initialState: {} as State,
userById: {},
} as State,
reducers: { reducers: {
setHost: (state, action: PayloadAction<User | undefined>) => { setHost: (state, action: PayloadAction<User | undefined>) => {
return { return {
@ -27,14 +23,6 @@ const userSlice = createSlice({
user: action.payload, user: action.payload,
}; };
}, },
setUserById: (state, action: PayloadAction<User>) => {
const userById = cloneDeep(state.userById);
userById[action.payload.id] = action.payload;
return {
...state,
userById: userById,
};
},
patchUser: (state, action: PayloadAction<Partial<User>>) => { patchUser: (state, action: PayloadAction<Partial<User>>) => {
return { return {
...state, ...state,
@ -47,6 +35,6 @@ const userSlice = createSlice({
}, },
}); });
export const { setHost, setUser, setUserById, patchUser } = userSlice.actions; export const { setHost, setUser, patchUser } = userSlice.actions;
export default userSlice.reducer; export default userSlice.reducer;

@ -8,6 +8,9 @@ interface UserV1Store {
getUserByUsername: (username: string) => User; getUserByUsername: (username: string) => User;
} }
// Request cache is used to prevent multiple requests.
const requestCache = new Map<string, Promise<any>>();
const useUserV1Store = create<UserV1Store>()((set, get) => ({ const useUserV1Store = create<UserV1Store>()((set, get) => ({
userMapByUsername: {}, userMapByUsername: {},
getOrFetchUserByUsername: async (username: string) => { getOrFetchUserByUsername: async (username: string) => {
@ -15,8 +18,14 @@ const useUserV1Store = create<UserV1Store>()((set, get) => ({
if (userMap[username]) { if (userMap[username]) {
return userMap[username] as User; return userMap[username] as User;
} }
if (requestCache.has(username)) {
return await requestCache.get(username);
}
const { data } = await api.getUserByUsername(username); const promise = api.getUserByUsername(username);
requestCache.set(username, promise);
const { data } = await promise;
requestCache.delete(username);
const user = convertResponseModelUser(data); const user = convertResponseModelUser(data);
userMap[username] = user; userMap[username] = user;
set(userMap); set(userMap);

Loading…
Cancel
Save