chore: update detail styles (#1964)

This commit is contained in:
boojack 2023-07-15 22:57:57 +08:00 committed by GitHub
parent 49dd90578b
commit 01f4780655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 133 additions and 160 deletions

View File

@ -14,6 +14,7 @@
"@mui/joy": "^5.0.0-alpha.75",
"@reduxjs/toolkit": "^1.8.1",
"axios": "^0.27.2",
"classnames": "^2.3.2",
"copy-to-clipboard": "^3.3.2",
"highlight.js": "^11.6.0",
"i18next": "^21.9.2",

View File

@ -1,4 +1,4 @@
lockfileVersion: '6.0'
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
@ -23,6 +23,9 @@ dependencies:
axios:
specifier: ^0.27.2
version: 0.27.2
classnames:
specifier: ^2.3.2
version: 2.3.2
copy-to-clipboard:
specifier: ^3.3.2
version: 3.3.2
@ -1346,6 +1349,10 @@ packages:
fsevents: 2.3.2
dev: false
/classnames@2.3.2:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
/clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}

View File

@ -1,7 +1,8 @@
import classNames from "classnames";
import { useEffect } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useTranslate } from "@/utils/i18n";
import { useLayoutStore, useUserStore } from "@/store/module";
import { useTranslate } from "@/utils/i18n";
import { resolution } from "@/utils/layout";
import Icon from "./Icon";
import UserBanner from "./UserBanner";
@ -53,9 +54,10 @@ const Header = () => {
to="/"
id="header-home"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600"
} px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>
@ -66,9 +68,10 @@ const Header = () => {
to="/review"
id="header-review"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600"
} px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>
@ -79,9 +82,10 @@ const Header = () => {
to="/resources"
id="header-resources"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600"
} px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>
@ -94,9 +98,10 @@ const Header = () => {
to="/explore"
id="header-explore"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600"
} px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>
@ -110,9 +115,10 @@ const Header = () => {
to="/memo-chat"
id="header-memo-chat"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 shadow"
} px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>
@ -123,9 +129,10 @@ const Header = () => {
to="/archived"
id="header-archived"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600"
} px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>
@ -136,9 +143,10 @@ const Header = () => {
to="/setting"
id="header-setting"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600"
} px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>
@ -154,9 +162,10 @@ const Header = () => {
to="/auth"
id="header-auth"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600"
} px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700`
classNames(
"px-4 pr-5 py-2 rounded-full border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}
>
<>

View File

@ -2,20 +2,15 @@ import Icon from "@/components/Icon";
import Textarea from "@mui/joy/Textarea/Textarea";
import { useTranslation } from "react-i18next";
interface MemosChatInputProps {
interface Props {
question: string;
handleQuestionTextareaChange: any;
setIsInIME: any;
handleKeyDown: any;
handleSendQuestionButtonClick: any;
}
const MemosChatInput = ({
question,
handleQuestionTextareaChange,
setIsInIME,
handleKeyDown,
handleSendQuestionButtonClick,
}: MemosChatInputProps) => {
const ChatInput = ({ question, handleQuestionTextareaChange, setIsInIME, handleKeyDown, handleSendQuestionButtonClick }: Props) => {
const { t } = useTranslation();
return (
@ -39,4 +34,4 @@ const MemosChatInput = ({
);
};
export default MemosChatInput;
export default ChatInput;

View File

@ -7,7 +7,7 @@ interface MessageProps {
message: Message;
}
const MemosChatMessage = ({ index, message }: MessageProps) => {
const ChatMessage = ({ index, message }: MessageProps) => {
return (
<div key={index} className="w-full flex flex-col justify-start items-start space-y-2">
{message.role === "user" ? (
@ -28,4 +28,4 @@ const MemosChatMessage = ({ index, message }: MessageProps) => {
);
};
export default MemosChatMessage;
export default ChatMessage;

View File

@ -1,3 +1,4 @@
import { Button } from "@mui/joy";
import { isNumber, last, uniq } from "lodash-es";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "react-hot-toast";
@ -421,9 +422,9 @@ const MemoEditor = (props: Props) => {
<div className="editor-footer-container">
<MemoVisibilitySelector value={state.memoVisibility} onChange={handleMemoVisibilityChange} />
<div className="buttons-container">
<button className="action-btn confirm-btn" disabled={!allowSave} onClick={handleSaveBtnClick}>
<Button disabled={!allowSave} onClick={handleSaveBtnClick}>
{t("editor.save")}
</button>
</Button>
</div>
</div>
</div>

View File

@ -76,7 +76,7 @@ const MemoList = () => {
return shouldShow;
})
: memos
).filter((memo) => memo.creatorId === currentUserId);
).filter((memo) => memo.creatorId === currentUserId && memo.rowStatus === "NORMAL");
const pinnedMemos = shownMemos.filter((m) => m.pinned);
const unpinnedMemos = shownMemos.filter((m) => !m.pinned);

View File

@ -326,6 +326,14 @@
"and": "And",
"or": "Or"
},
"amount-text": {
"memo_one": "MEMO",
"memo_other": "MEMOS",
"tag_one": "TAG",
"tag_other": "TAGS",
"day_one": "TAG",
"day_other": "TAGE"
},
"message": {
"no-data": "Maybe no data was found, maybe it should be another option.",
"memos-ready": "all memos are ready 🎉",

View File

@ -1,4 +1,4 @@
import { Button, Divider } from "@mui/joy";
import { Button, Divider, Input } from "@mui/joy";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslate } from "@/utils/i18n";
@ -131,71 +131,44 @@ const Auth = () => {
<div className="flex flex-row justify-center items-center w-full h-full dark:bg-zinc-800">
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-start items-center">
<div className="w-full py-4 grow flex flex-col justify-center items-center">
<div className="flex flex-col justify-start items-start w-full mb-4">
<div className="w-full flex flex-row justify-start items-center mb-2">
<img className="h-12 w-auto rounded-lg mr-1" src={systemStatus.customizedProfile.logoUrl} alt="" />
<p className="text-6xl tracking-wide text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
</div>
<p className="text-sm text-gray-700 dark:text-gray-300">
{systemStatus.customizedProfile.description || t("common.memos-slogan")}
</p>
<div className="w-full flex flex-col justify-center items-center mb-2">
<img className="h-20 w-auto rounded-full shadow mr-1" src={systemStatus.customizedProfile.logoUrl} alt="" />
<p className="text-3xl text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
</div>
<form className="w-full" onSubmit={handleFormSubmit}>
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading && "opacity-80"}`}>
<div className="flex flex-col justify-start items-start relative w-full text-base mt-2 py-2">
<span
className={`absolute top-3 left-3 px-1 leading-10 shrink-0 text-base cursor-text text-gray-400 transition-all select-none pointer-events-none ${
username ? "!text-sm !top-0 !z-10 !leading-4 bg-white dark:bg-zinc-800 rounded" : ""
}`}
>
{t("common.username")}
</span>
<input
className="input-text w-full py-3 px-3 text-base rounded-lg dark:bg-zinc-800"
type="text"
value={username}
onChange={handleUsernameInputChanged}
required
/>
</div>
<div className="flex flex-col justify-start items-start relative w-full text-base mt-2 py-2">
<span
className={`absolute top-3 left-3 px-1 leading-10 shrink-0 text-base cursor-text text-gray-400 transition-all select-none pointer-events-none ${
password ? "!text-sm !top-0 !z-10 !leading-4 bg-white dark:bg-zinc-800 rounded" : ""
}`}
>
{t("common.password")}
</span>
<input
className="input-text w-full py-3 px-3 text-base rounded-lg dark:bg-zinc-800"
type="password"
value={password}
onChange={handlePasswordInputChanged}
required
/>
</div>
<form className="w-full mt-4" onSubmit={handleFormSubmit}>
<div className="flex flex-col justify-start items-start w-full gap-4">
<Input
className="w-full"
size="lg"
type="text"
placeholder={t("common.username")}
value={username}
onChange={handleUsernameInputChanged}
required
/>
<Input
className="w-full"
size="lg"
type="password"
placeholder={t("common.password")}
value={password}
onChange={handlePasswordInputChanged}
required
/>
</div>
<div className="flex flex-row justify-end items-center w-full mt-2">
<div className="flex flex-row justify-end items-center w-full mt-6">
{actionBtnLoadingState.isLoading && <Icon.Loader className="w-4 h-auto mr-2 animate-spin dark:text-gray-300" />}
{systemStatus?.allowSignUp && (
<>
<button
type="button"
className={`btn-text ${actionBtnLoadingState.isLoading ? "cursor-wait opacity-80" : ""}`}
onClick={handleSignUpButtonClick}
>
<Button variant={"plain"} loading={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>
{t("common.sign-up")}
</button>
</Button>
<span className="mr-2 font-mono text-gray-200">/</span>
</>
)}
<button
type="submit"
className={`btn-primary ${actionBtnLoadingState.isLoading ? "cursor-wait opacity-80" : ""}`}
onClick={handleSignInButtonClick}
>
<Button type="submit" loading={actionBtnLoadingState.isLoading} onClick={handleSignInButtonClick}>
{t("common.sign-in")}
</button>
</Button>
</div>
</form>
{identityProviderList.length > 0 && (

View File

@ -11,19 +11,13 @@ import Memo from "@/components/Memo";
import MobileHeader from "@/components/MobileHeader";
import Empty from "@/components/Empty";
interface State {
memos: Memo[];
}
const Explore = () => {
const t = useTranslate();
const location = useLocation();
const filterStore = useFilterStore();
const memoStore = useMemoStore();
const filter = filterStore.state;
const [state, setState] = useState<State>({
memos: [],
});
const memos = memoStore.state.memos;
const [isComplete, setIsComplete] = useState<boolean>(false);
const loadingState = useLoading();
@ -32,9 +26,6 @@ const Explore = () => {
if (memos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true);
}
setState({
memos,
});
loadingState.setFinish();
});
}, [location]);
@ -43,7 +34,7 @@ const Explore = () => {
const showMemoFilter = Boolean(tagQuery || textQuery);
const shownMemos = showMemoFilter
? state.memos.filter((memo) => {
? memos.filter((memo) => {
let shouldShow = true;
if (tagQuery) {
@ -64,21 +55,17 @@ const Explore = () => {
}
return shouldShow;
})
: state.memos;
const sortedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL");
: memos;
const sortedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL" && m.visibility !== "PRIVATE");
const handleFetchMoreClick = async () => {
try {
const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length);
const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, memos.length);
if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true);
} else {
setIsComplete(false);
}
setState({
memos: state.memos.concat(fetchedMemos),
});
} catch (error: any) {
console.error(error);
toast.error(error.response.data.message);
@ -95,7 +82,7 @@ const Explore = () => {
return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} showCreator />;
})}
{isComplete ? (
state.memos.length === 0 && (
memos.length === 0 && (
<div className="w-full mt-16 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>

View File

@ -1,4 +1,5 @@
import { Button, Stack } from "@mui/joy";
import { head } from "lodash-es";
import React, { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
@ -9,27 +10,23 @@ import { Conversation, useConversationStore } from "@/store/zustand/conversation
import Icon from "@/components/Icon";
import { generateUUID } from "@/utils/uuid";
import MobileHeader from "@/components/MobileHeader";
import MemosChatMessage from "@/components/MemosChat/MemosChatMessage";
import MemosChatInput from "@/components/MemosChat/MemosChatInput";
import head from "lodash-es/head";
import ConversationTab from "@/components/MemosChat/ConversationTab";
import ChatMessage from "@/components/MemoChat/ChatMessage";
import ChatInput from "@/components/MemoChat/ChatInput";
import ConversationTab from "@/components/MemoChat/ConversationTab";
import Empty from "@/components/Empty";
const MemosChat = () => {
const MemoChat = () => {
const { t } = useTranslation();
const fetchingState = useLoading(false);
const [isEnabled, setIsEnabled] = useState<boolean>(true);
const [isInIME, setIsInIME] = useState(false);
const [question, setQuestion] = useState<string>("");
const conversationStore = useConversationStore();
const conversationList = conversationStore.conversationList;
const [selectedConversationId, setSelectedConversationId] = useState<string>(head(conversationList)?.messageStorageId || "");
const messageStore = useMessageStore(selectedConversationId)();
const messageList = messageStore.messageList;
// the state didn't show in component, just for trigger re-render
// The state didn't show in component, just for trigger re-render
const [message, setMessage] = useState<string>("");
useEffect(() => {
@ -170,7 +167,7 @@ const MemosChat = () => {
</div>
)}
{messageList.map((message, index) => (
<MemosChatMessage key={index} message={message} index={index} />
<ChatMessage key={index} message={message} index={index} />
))}
</Stack>
{fetchingState.isLoading && (
@ -185,7 +182,7 @@ const MemosChat = () => {
</div>
)}
<MemosChatInput
<ChatInput
question={question}
handleQuestionTextareaChange={handleQuestionTextareaChange}
setIsInIME={setIsInIME}
@ -198,4 +195,4 @@ const MemosChat = () => {
);
};
export default MemosChat;
export default MemoChat;

View File

@ -1,40 +1,28 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { useTranslate } from "@/utils/i18n";
import { Link, useLocation, useParams } from "react-router-dom";
import { UNKNOWN_ID } from "@/helpers/consts";
import { useGlobalStore, useMemoStore } from "@/store/module";
import useLoading from "@/hooks/useLoading";
import Icon from "@/components/Icon";
import Memo from "@/components/Memo";
interface State {
memo: Memo;
}
const MemoDetail = () => {
const t = useTranslate();
const params = useParams();
const location = useLocation();
const globalStore = useGlobalStore();
const memoStore = useMemoStore();
const [state, setState] = useState<State>({
memo: {
id: UNKNOWN_ID,
} as Memo,
});
const loadingState = useLoading();
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
const memoId = Number(params.memoId);
const memo = memoStore.state.memos.find((memo) => memo.id === memoId);
useEffect(() => {
const memoId = Number(params.memoId);
if (memoId && !isNaN(memoId)) {
memoStore
.fetchMemoById(memoId)
.then((memo) => {
setState({
memo,
});
.then(() => {
loadingState.setFinish();
})
.catch((error) => {
@ -53,21 +41,26 @@ const MemoDetail = () => {
<p className="detail-name text-4xl tracking-wide text-black dark:text-white">{customizedProfile.name}</p>
</div>
</div>
{!loadingState.isLoading && (
<>
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<Memo memo={state.memo} showCreator showRelatedMemos />
</main>
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
<Link
to="/"
className="flex flex-row justify-center items-center text-gray-600 dark:text-gray-300 text-sm px-3 hover:opacity-80 hover:underline"
>
<Icon.Home className="w-4 h-auto mr-1 -mt-0.5" /> {t("router.back-to-home")}
</Link>
</div>
</>
)}
{!loadingState.isLoading &&
(memo ? (
<>
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<Memo memo={memo} showCreator showRelatedMemos />
</main>
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
<Link
to="/"
className="flex flex-row justify-center items-center text-gray-600 dark:text-gray-300 text-sm px-3 hover:opacity-80 hover:underline"
>
<Icon.Home className="w-4 h-auto mr-1 -mt-0.5" /> {t("router.back-to-home")}
</Link>
</div>
</>
) : (
<>
<p>Not found</p>
</>
))}
</div>
</section>
);

View File

@ -16,7 +16,7 @@ const Home = lazy(() => import("@/pages/Home"));
const MemoDetail = lazy(() => import("@/pages/MemoDetail"));
const EmbedMemo = lazy(() => import("@/pages/EmbedMemo"));
const NotFound = lazy(() => import("@/pages/NotFound"));
const MemosChat = lazy(() => import("@/pages/MemosChat"));
const MemoChat = lazy(() => import("@/pages/MemoChat"));
const initialGlobalStateLoader = (() => {
let done = false;
@ -150,7 +150,7 @@ const router = createBrowserRouter([
},
{
path: "memo-chat",
element: <MemosChat />,
element: <MemoChat />,
loader: async () => {
await initialGlobalStateLoader();

View File

@ -23,6 +23,7 @@ export const useMemoStore = () => {
const fetchMemoById = async (memoId: MemoId) => {
const { data } = await api.getMemoById(memoId);
const memo = convertResponseModelMemo(data);
store.dispatch(upsertMemos([memo]));
return memo;
};
@ -62,6 +63,7 @@ export const useMemoStore = () => {
const { data } = await api.getAllMemos(memoFind);
const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
store.dispatch(upsertMemos(fetchedMemos));
for (const m of fetchedMemos) {
memoCacheStore.setMemoCache(m);