mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
refactor: user stats state
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { last } from "lodash-es";
|
||||
import { Globe2Icon, HomeIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { matchPath, NavLink, useLocation } from "react-router-dom";
|
||||
import useDebounce from "react-use/lib/useDebounce";
|
||||
import SearchBar from "@/components/SearchBar";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { Routes } from "@/router";
|
||||
import { useMemoList, useUserStatsStore } from "@/store/v1";
|
||||
import { useMemoList } from "@/store/v1";
|
||||
import { userStore } from "@/store/v2";
|
||||
import { cn } from "@/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
@@ -25,12 +26,11 @@ interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const HomeSidebar = (props: Props) => {
|
||||
const HomeSidebar = observer((props: Props) => {
|
||||
const t = useTranslate();
|
||||
const location = useLocation();
|
||||
const currentUser = useCurrentUser();
|
||||
const memoList = useMemoList();
|
||||
const userStatsStore = useUserStatsStore();
|
||||
|
||||
const homeNavLink: NavLinkItem = {
|
||||
id: "header-home",
|
||||
@@ -55,13 +55,13 @@ const HomeSidebar = (props: Props) => {
|
||||
}
|
||||
if (matchPath("/u/:username", location.pathname) !== null) {
|
||||
const username = last(location.pathname.split("/"));
|
||||
const user = await userStore.fetchUserByUsername(username || "");
|
||||
const user = await userStore.getOrFetchUserByUsername(username || "");
|
||||
parent = user.name;
|
||||
}
|
||||
await userStatsStore.listUserStats(parent);
|
||||
await userStore.fetchUserStats(parent);
|
||||
},
|
||||
300,
|
||||
[memoList.size(), userStatsStore.stateId, location.pathname],
|
||||
[memoList.size(), userStore.state.statsStateId, location.pathname],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -93,6 +93,6 @@ const HomeSidebar = (props: Props) => {
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default HomeSidebar;
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { Dropdown, Menu, MenuButton, MenuItem, Switch } from "@mui/joy";
|
||||
import { Edit3Icon, HashIcon, MoreVerticalIcon, TagsIcon, TrashIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import toast from "react-hot-toast";
|
||||
import useLocalStorage from "react-use/lib/useLocalStorage";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import { useMemoFilterStore, useUserStatsStore, useUserStatsTags } from "@/store/v1";
|
||||
import { useMemoFilterStore } from "@/store/v1";
|
||||
import { userStore } from "@/store/v2";
|
||||
import { cn } from "@/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import showRenameTagDialog from "../RenameTagDialog";
|
||||
@@ -14,12 +16,11 @@ interface Props {
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
const TagsSection = (props: Props) => {
|
||||
const TagsSection = observer((props: Props) => {
|
||||
const t = useTranslate();
|
||||
const memoFilterStore = useMemoFilterStore();
|
||||
const userStatsStore = useUserStatsStore();
|
||||
const [treeMode, setTreeMode] = useLocalStorage<boolean>("tag-view-as-tree", false);
|
||||
const tags = Object.entries(useUserStatsTags())
|
||||
const tags = Object.entries(userStore.state.tagCount)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
@@ -42,7 +43,6 @@ const TagsSection = (props: Props) => {
|
||||
parent: "memos/-",
|
||||
tag: tag,
|
||||
});
|
||||
userStatsStore.setStateId();
|
||||
toast.success(t("message.deleted-successfully"));
|
||||
}
|
||||
};
|
||||
@@ -114,6 +114,6 @@ const TagsSection = (props: Props) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default TagsSection;
|
||||
|
@@ -15,7 +15,8 @@ import toast from "react-hot-toast";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { markdownServiceClient } from "@/grpcweb";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { useMemoStore, useUserStatsStore } from "@/store/v1";
|
||||
import { useMemoStore } from "@/store/v1";
|
||||
import { userStore } from "@/store/v2";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { NodeType } from "@/types/proto/api/v1/markdown_service";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
@@ -48,14 +49,13 @@ const MemoActionMenu = (props: Props) => {
|
||||
const location = useLocation();
|
||||
const navigateTo = useNavigateTo();
|
||||
const memoStore = useMemoStore();
|
||||
const userStatsStore = useUserStatsStore();
|
||||
const isArchived = memo.state === State.ARCHIVED;
|
||||
const hasCompletedTaskList = checkHasCompletedTaskList(memo);
|
||||
const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`);
|
||||
|
||||
const memoUpdatedCallback = () => {
|
||||
// Refresh user stats.
|
||||
userStatsStore.setStateId();
|
||||
userStore.setStatsStateId();
|
||||
};
|
||||
|
||||
const handleTogglePinMemoBtnClick = async () => {
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { Dropdown, Menu, MenuButton } from "@mui/joy";
|
||||
import { Button } from "@usememos/mui";
|
||||
import { HashIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRef, useState } from "react";
|
||||
import useClickAway from "react-use/lib/useClickAway";
|
||||
import OverflowTip from "@/components/kit/OverflowTip";
|
||||
import { useUserStatsTags } from "@/store/v1";
|
||||
import { userStore } from "@/store/v2";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { EditorRefActions } from "../Editor";
|
||||
|
||||
@@ -12,12 +13,12 @@ interface Props {
|
||||
editorRef: React.RefObject<EditorRefActions>;
|
||||
}
|
||||
|
||||
const TagSelector = (props: Props) => {
|
||||
const TagSelector = observer((props: Props) => {
|
||||
const t = useTranslate();
|
||||
const { editorRef } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const tags = Object.entries(useUserStatsTags())
|
||||
const tags = Object.entries(userStore.state.tagCount)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([tag]) => tag);
|
||||
@@ -71,6 +72,6 @@ const TagSelector = (props: Props) => {
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default TagSelector;
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import getCaretCoordinates from "textarea-caret";
|
||||
import OverflowTip from "@/components/kit/OverflowTip";
|
||||
import { useUserStatsTags } from "@/store/v1";
|
||||
import { userStore } from "@/store/v2";
|
||||
import { cn } from "@/utils";
|
||||
import { EditorRefActions } from ".";
|
||||
|
||||
@@ -13,12 +14,12 @@ type Props = {
|
||||
|
||||
type Position = { left: number; top: number; height: number };
|
||||
|
||||
const TagSuggestions = ({ editorRef, editorActions }: Props) => {
|
||||
const TagSuggestions = observer(({ editorRef, editorActions }: Props) => {
|
||||
const [position, setPosition] = useState<Position | null>(null);
|
||||
const [selected, select] = useState(0);
|
||||
const selectedRef = useRef(selected);
|
||||
selectedRef.current = selected;
|
||||
const tags = Object.entries(useUserStatsTags())
|
||||
const tags = Object.entries(userStore.state.tagCount)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([tag]) => tag);
|
||||
@@ -120,6 +121,6 @@ const TagSuggestions = ({ editorRef, editorActions }: Props) => {
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default TagSuggestions;
|
||||
|
@@ -5,7 +5,7 @@ import { Link, useLocation } from "react-router-dom";
|
||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { useMemoStore, useUserStatsStore } from "@/store/v1";
|
||||
import { useMemoStore } from "@/store/v1";
|
||||
import { userStore, workspaceStore } from "@/store/v2";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service";
|
||||
@@ -45,7 +45,6 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
const currentUser = useCurrentUser();
|
||||
const user = useCurrentUser();
|
||||
const memoStore = useMemoStore();
|
||||
const userStatsStore = useUserStatsStore();
|
||||
const [showEditor, setShowEditor] = useState<boolean>(false);
|
||||
const [creator, setCreator] = useState(userStore.getUserByName(memo.creator));
|
||||
const [showNSFWContent, setShowNSFWContent] = useState(props.showNsfwContent);
|
||||
@@ -102,7 +101,7 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
|
||||
const onEditorConfirm = () => {
|
||||
setShowEditor(false);
|
||||
userStatsStore.setStateId();
|
||||
userStore.setStatsStateId();
|
||||
};
|
||||
|
||||
const onPinIconClick = async () => {
|
||||
|
@@ -5,7 +5,6 @@ import React, { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { useUserStatsStore } from "@/store/v1";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
@@ -16,7 +15,6 @@ interface Props extends DialogProps {
|
||||
const RenameTagDialog: React.FC<Props> = (props: Props) => {
|
||||
const { tag, destroy } = props;
|
||||
const t = useTranslate();
|
||||
const userStatsStore = useUserStatsStore();
|
||||
const [newName, setNewName] = useState(tag);
|
||||
const requestState = useLoading(false);
|
||||
|
||||
@@ -41,7 +39,6 @@ const RenameTagDialog: React.FC<Props> = (props: Props) => {
|
||||
newTag: newName,
|
||||
});
|
||||
toast.success("Rename tag successfully");
|
||||
userStatsStore.setStateId();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
|
@@ -2,21 +2,22 @@ import { Tooltip } from "@mui/joy";
|
||||
import dayjs from "dayjs";
|
||||
import { countBy } from "lodash-es";
|
||||
import { CheckCircleIcon, ChevronRightIcon, ChevronLeftIcon, Code2Icon, LinkIcon, ListTodoIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||
import i18n from "@/i18n";
|
||||
import { useMemoFilterStore, useUserStatsStore } from "@/store/v1";
|
||||
import { useMemoFilterStore } from "@/store/v1";
|
||||
import { userStore } from "@/store/v2";
|
||||
import { UserStats_MemoTypeStats } from "@/types/proto/api/v1/user_service";
|
||||
import { cn } from "@/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import ActivityCalendar from "./ActivityCalendar";
|
||||
|
||||
const StatisticsView = () => {
|
||||
const StatisticsView = observer(() => {
|
||||
const t = useTranslate();
|
||||
const memoFilterStore = useMemoFilterStore();
|
||||
const userStatsStore = useUserStatsStore();
|
||||
const [memoTypeStats, setMemoTypeStats] = useState<UserStats_MemoTypeStats>(UserStats_MemoTypeStats.fromPartial({}));
|
||||
const [activityStats, setActivityStats] = useState<Record<string, number>>({});
|
||||
const [selectedDate] = useState(new Date());
|
||||
@@ -25,7 +26,7 @@ const StatisticsView = () => {
|
||||
useAsyncEffect(async () => {
|
||||
const memoTypeStats = UserStats_MemoTypeStats.fromPartial({});
|
||||
const displayTimeList: Date[] = [];
|
||||
for (const stats of Object.values(userStatsStore.userStatsByName)) {
|
||||
for (const stats of Object.values(userStore.state.userStatsByName)) {
|
||||
displayTimeList.push(...stats.memoDisplayTimestamps);
|
||||
if (stats.memoTypeStats) {
|
||||
memoTypeStats.codeCount += stats.memoTypeStats.codeCount;
|
||||
@@ -36,7 +37,7 @@ const StatisticsView = () => {
|
||||
}
|
||||
setMemoTypeStats(memoTypeStats);
|
||||
setActivityStats(countBy(displayTimeList.map((date) => dayjs(date).format("YYYY-MM-DD"))));
|
||||
}, [userStatsStore.userStatsByName, userStatsStore.stateId]);
|
||||
}, [userStore.state.userStatsByName]);
|
||||
|
||||
const onCalendarClick = (date: string) => {
|
||||
memoFilterStore.removeFilter((f) => f.factor === "displayTime");
|
||||
@@ -135,6 +136,6 @@ const StatisticsView = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default StatisticsView;
|
||||
|
@@ -31,7 +31,7 @@ const UserProfile = () => {
|
||||
}
|
||||
|
||||
userStore
|
||||
.fetchUserByUsername(username)
|
||||
.getOrFetchUserByUsername(username)
|
||||
.then((user) => {
|
||||
setUser(user);
|
||||
loadingState.setFinish();
|
||||
|
@@ -2,4 +2,3 @@ export * from "./memo";
|
||||
export * from "./resourceName";
|
||||
export * from "./resource";
|
||||
export * from "./memoFilter";
|
||||
export * from "./userStats";
|
||||
|
@@ -1,51 +0,0 @@
|
||||
import { uniqueId } from "lodash-es";
|
||||
import { create } from "zustand";
|
||||
import { combine } from "zustand/middleware";
|
||||
import { userServiceClient } from "@/grpcweb";
|
||||
import { UserStats } from "@/types/proto/api/v1/user_service";
|
||||
|
||||
interface State {
|
||||
// stateId is used to identify the store instance state.
|
||||
// It should be update when any state change.
|
||||
stateId: string;
|
||||
userStatsByName: Record<string, UserStats>;
|
||||
}
|
||||
|
||||
const getDefaultState = (): State => ({
|
||||
stateId: uniqueId(),
|
||||
userStatsByName: {},
|
||||
});
|
||||
|
||||
export const useUserStatsStore = create(
|
||||
combine(getDefaultState(), (set, get) => ({
|
||||
setState: (state: State) => set(state),
|
||||
getState: () => get(),
|
||||
listUserStats: async (user?: string) => {
|
||||
const userStatsByName: Record<string, UserStats> = {};
|
||||
if (!user) {
|
||||
const { userStats } = await userServiceClient.listAllUserStats({});
|
||||
for (const stats of userStats) {
|
||||
userStatsByName[stats.name] = stats;
|
||||
}
|
||||
} else {
|
||||
const userStats = await userServiceClient.getUserStats({ name: user });
|
||||
userStatsByName[user] = userStats;
|
||||
}
|
||||
set({ ...get(), userStatsByName });
|
||||
},
|
||||
setStateId: (id = uniqueId()) => {
|
||||
set({ ...get(), stateId: id });
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
export const useUserStatsTags = () => {
|
||||
const userStatsStore = useUserStatsStore();
|
||||
const tagAmounts: Record<string, number> = {};
|
||||
for (const userStats of Object.values(userStatsStore.getState().userStatsByName)) {
|
||||
for (const tag of Object.keys(userStats.tagCount)) {
|
||||
tagAmounts[tag] = (tagAmounts[tag] || 0) + userStats.tagCount[tag];
|
||||
}
|
||||
}
|
||||
return tagAmounts;
|
||||
};
|
@@ -1,7 +1,8 @@
|
||||
import { uniqueId } from "lodash-es";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { authServiceClient, inboxServiceClient, userServiceClient } from "@/grpcweb";
|
||||
import { Inbox } from "@/types/proto/api/v1/inbox_service";
|
||||
import { Shortcut, User, UserSetting } from "@/types/proto/api/v1/user_service";
|
||||
import { Shortcut, User, UserSetting, UserStats } from "@/types/proto/api/v1/user_service";
|
||||
import workspaceStore from "./workspace";
|
||||
|
||||
class LocalState {
|
||||
@@ -10,6 +11,20 @@ class LocalState {
|
||||
shortcuts: Shortcut[] = [];
|
||||
inboxes: Inbox[] = [];
|
||||
userMapByName: Record<string, User> = {};
|
||||
userStatsByName: Record<string, UserStats> = {};
|
||||
|
||||
// The state id of user stats map.
|
||||
statsStateId = uniqueId();
|
||||
|
||||
get tagCount() {
|
||||
const tagCount: Record<string, number> = {};
|
||||
for (const stats of Object.values(this.userStatsByName)) {
|
||||
for (const tag of Object.keys(stats.tagCount)) {
|
||||
tagCount[tag] = (tagCount[tag] || 0) + stats.tagCount[tag];
|
||||
}
|
||||
}
|
||||
return tagCount;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
@@ -40,13 +55,19 @@ const userStore = (() => {
|
||||
return user;
|
||||
};
|
||||
|
||||
const fetchUserByUsername = async (username: string) => {
|
||||
const getOrFetchUserByUsername = async (username: string) => {
|
||||
const userMap = state.userMapByName;
|
||||
for (const name in userMap) {
|
||||
if (userMap[name].username === username) {
|
||||
return userMap[name];
|
||||
}
|
||||
}
|
||||
const user = await userServiceClient.getUserByUsername({
|
||||
username,
|
||||
});
|
||||
state.setPartial({
|
||||
userMapByName: {
|
||||
...state.userMapByName,
|
||||
...userMap,
|
||||
[user.name]: user,
|
||||
},
|
||||
});
|
||||
@@ -138,10 +159,30 @@ const userStore = (() => {
|
||||
return updatedInbox;
|
||||
};
|
||||
|
||||
const fetchUserStats = async (user?: string) => {
|
||||
const userStatsByName: Record<string, UserStats> = {};
|
||||
if (!user) {
|
||||
const { userStats } = await userServiceClient.listAllUserStats({});
|
||||
for (const stats of userStats) {
|
||||
userStatsByName[stats.name] = stats;
|
||||
}
|
||||
} else {
|
||||
const userStats = await userServiceClient.getUserStats({ name: user });
|
||||
userStatsByName[user] = userStats;
|
||||
}
|
||||
state.setPartial({
|
||||
userStatsByName,
|
||||
});
|
||||
};
|
||||
|
||||
const setStatsStateId = (id = uniqueId()) => {
|
||||
state.statsStateId = id;
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
getOrFetchUserByName,
|
||||
fetchUserByUsername,
|
||||
getOrFetchUserByUsername,
|
||||
getUserByName,
|
||||
fetchUsers,
|
||||
updateUser,
|
||||
@@ -150,6 +191,8 @@ const userStore = (() => {
|
||||
fetchShortcuts,
|
||||
fetchInboxes,
|
||||
updateInbox,
|
||||
fetchUserStats,
|
||||
setStatsStateId,
|
||||
};
|
||||
})();
|
||||
|
||||
|
Reference in New Issue
Block a user