From 1d99dad435593a67aa3b846bdf2fcb2bb6d9e15e Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 9 May 2024 07:56:00 +0800 Subject: [PATCH] feat: update timeline page --- web/src/components/ActivityCalendar.tsx | 15 +- .../components/ChangeMemoCreatedTsDialog.tsx | 105 ------------- .../components/HomeSidebar/TagsSection.tsx | 8 +- web/src/components/MemoView.tsx | 11 +- web/src/pages/Archived.tsx | 15 +- web/src/pages/Explore.tsx | 15 +- web/src/pages/Home.tsx | 15 +- web/src/pages/Timeline.tsx | 139 ++++++++---------- web/src/pages/UserProfile.tsx | 15 +- web/src/store/v1/tag.ts | 9 -- 10 files changed, 118 insertions(+), 229 deletions(-) delete mode 100644 web/src/components/ChangeMemoCreatedTsDialog.tsx diff --git a/web/src/components/ActivityCalendar.tsx b/web/src/components/ActivityCalendar.tsx index b0005a21..1fde4d84 100644 --- a/web/src/components/ActivityCalendar.tsx +++ b/web/src/components/ActivityCalendar.tsx @@ -6,6 +6,7 @@ import { useTranslate } from "@/utils/i18n"; interface Props { // Format: 2021-1 month: string; + selectedDate: string; data: Record; onClick?: (date: string) => void; } @@ -28,8 +29,8 @@ const getCellAdditionalStyles = (count: number, maxCount: number) => { const ActivityCalendar = (props: Props) => { const t = useTranslate(); const { month: monthStr, data, onClick } = props; - const year = new Date(monthStr).getUTCFullYear(); - const month = new Date(monthStr).getUTCMonth() + 1; + const year = new Date(monthStr).getFullYear(); + const month = new Date(monthStr).getMonth() + 1; const dayInMonth = new Date(year, month, 0).getDate(); const firstDay = new Date(year, month - 1, 1).getDay(); const lastDay = new Date(year, month - 1, dayInMonth).getDay(); @@ -55,15 +56,19 @@ const ActivityCalendar = (props: Props) => { const count = data[date] || 0; const isToday = new Date().toDateString() === new Date(date).toDateString(); const tooltipText = count ? t("memo.count-memos-in-date", { count: count, date: date }) : date; + const isSelected = new Date(props.selectedDate).toDateString() === new Date(date).toDateString(); return day ? (
0 && "cursor-pointer", )} - onClick={() => count && onClick && onClick(date)} + onClick={() => count && onClick && onClick(new Date(date).toDateString())} > {day}
diff --git a/web/src/components/ChangeMemoCreatedTsDialog.tsx b/web/src/components/ChangeMemoCreatedTsDialog.tsx deleted file mode 100644 index 6da85153..00000000 --- a/web/src/components/ChangeMemoCreatedTsDialog.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Button, IconButton, Input } from "@mui/joy"; -import { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; -import { getNormalizedTimeString } from "@/helpers/datetime"; -import { MemoNamePrefix, useMemoStore } from "@/store/v1"; -import { useTranslate } from "@/utils/i18n"; -import { generateDialog } from "./Dialog"; -import Icon from "./Icon"; - -interface Props extends DialogProps { - memoId: number; -} - -const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { - const t = useTranslate(); - const { destroy, memoId } = props; - const memoStore = useMemoStore(); - const [createdAt, setCreatedAt] = useState(""); - const maxDatetimeValue = getNormalizedTimeString(); - - useEffect(() => { - memoStore.getOrFetchMemoByName(`${MemoNamePrefix}${memoId}`).then((memo) => { - if (memo) { - const datetime = getNormalizedTimeString(memo.createTime); - setCreatedAt(datetime); - } else { - toast.error(t("message.memo-not-found")); - destroy(); - } - }); - }, []); - - const handleCloseBtnClick = () => { - destroy(); - }; - - const handleDatetimeInputChange = (e: React.ChangeEvent) => { - const datetime = e.target.value as string; - setCreatedAt(datetime); - }; - - const handleSaveBtnClick = async () => { - try { - await memoStore.updateMemo( - { - name: `${MemoNamePrefix}${memoId}`, - createTime: new Date(createdAt), - }, - ["created_ts"], - ); - toast.success("Updated memo created time successfully."); - handleCloseBtnClick(); - } catch (error: any) { - console.error(error); - toast.error(error.response.data.message); - } - }; - - return ( - <> -
-

{t("message.change-memo-created-time")}

- - - -
-
- -
- - -
-
- - ); -}; - -function showChangeMemoCreatedTsDialog(memoId: number) { - generateDialog( - { - className: "change-memo-created-ts-dialog", - dialogName: "change-memo-created-ts-dialog", - }, - ChangeMemoCreatedTsDialog, - { - memoId, - }, - ); -} - -export default showChangeMemoCreatedTsDialog; diff --git a/web/src/components/HomeSidebar/TagsSection.tsx b/web/src/components/HomeSidebar/TagsSection.tsx index 28dc4cf9..d8f3b8ee 100644 --- a/web/src/components/HomeSidebar/TagsSection.tsx +++ b/web/src/components/HomeSidebar/TagsSection.tsx @@ -94,8 +94,12 @@ const TagContainer: React.FC = (props: TagContainerProps) => style: "danger", dialogName: "delete-tag-dialog", onConfirm: async () => { - await tagStore.deleteTag(tag); - tagStore.fetchTags({ skipCache: true }); + await memoServiceClient.deleteMemoTag({ + parent: "memos/-", + tag: tag, + }); + await tagStore.fetchTags({ skipCache: true }); + toast.success(t("message.deleted-successfully")); }, }); }; diff --git a/web/src/components/MemoView.tsx b/web/src/components/MemoView.tsx index 2c91a122..79dd7693 100644 --- a/web/src/components/MemoView.tsx +++ b/web/src/components/MemoView.tsx @@ -4,12 +4,11 @@ import { memo, useCallback, useEffect, useRef, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import useCurrentUser from "@/hooks/useCurrentUser"; import useNavigateTo from "@/hooks/useNavigateTo"; -import { extractMemoIdFromName, useUserStore } from "@/store/v1"; +import { useUserStore } from "@/store/v1"; import { MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service"; import { Memo, Visibility } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; import { convertVisibilityToString } from "@/utils/memo"; -import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; import Icon from "./Icon"; import MemoActionMenu from "./MemoActionMenu"; import MemoContent from "./MemoContent"; @@ -56,12 +55,8 @@ const MemoView: React.FC = (props: Props) => { })(); }, []); - const handleGotoMemoDetailPage = (event: React.MouseEvent) => { - if (event.altKey) { - showChangeMemoCreatedTsDialog(extractMemoIdFromName(memo.name)); - } else { - navigateTo(`/m/${memo.uid}`); - } + const handleGotoMemoDetailPage = () => { + navigateTo(`/m/${memo.uid}`); }; const handleMemoContentClick = useCallback(async (e: React.MouseEvent) => { diff --git a/web/src/pages/Archived.tsx b/web/src/pages/Archived.tsx index 1c9a76cb..e68fac5d 100644 --- a/web/src/pages/Archived.tsx +++ b/web/src/pages/Archived.tsx @@ -31,9 +31,14 @@ const Archived = () => { .sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); useEffect(() => { + setIsRequesting(true); nextPageTokenRef.current = undefined; - memoList.reset(); - fetchMemos(); + setTimeout(async () => { + memoList.reset(); + const nextPageToken = await fetchMemos(); + nextPageTokenRef.current = nextPageToken; + setIsRequesting(false); + }); }, [tagQuery, textQuery]); const fetchMemos = async () => { @@ -48,14 +53,12 @@ const Archived = () => { if (tagQuery) { filters.push(`tag == "${tagQuery}"`); } - setIsRequesting(true); - const data = await memoStore.fetchMemos({ + const { nextPageToken } = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), pageToken: nextPageTokenRef.current, }); - setIsRequesting(false); - nextPageTokenRef.current = data.nextPageToken; + return nextPageToken; }; const handleDeleteMemoClick = async (memo: Memo) => { diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 64950749..c55d521e 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -27,9 +27,14 @@ const Explore = () => { const sortedMemos = memoList.value.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); useEffect(() => { + setIsRequesting(true); nextPageTokenRef.current = undefined; - memoList.reset(); - fetchMemos(); + setTimeout(async () => { + memoList.reset(); + const nextPageToken = await fetchMemos(); + nextPageTokenRef.current = nextPageToken; + setIsRequesting(false); + }); }, [tagQuery, textQuery]); const fetchMemos = async () => { @@ -44,14 +49,12 @@ const Explore = () => { if (tagQuery) { filters.push(`tag == "${tagQuery}"`); } - setIsRequesting(true); - const data = await memoStore.fetchMemos({ + const { nextPageToken } = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), pageToken: nextPageTokenRef.current, }); - setIsRequesting(false); - nextPageTokenRef.current = data.nextPageToken; + return nextPageToken; }; return ( diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 8f0dea75..b1e92822 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -33,9 +33,14 @@ const Home = () => { .sort((a, b) => Number(b.pinned) - Number(a.pinned)); useEffect(() => { + setIsRequesting(true); nextPageTokenRef.current = undefined; - memoList.reset(); - fetchMemos(); + setTimeout(async () => { + memoList.reset(); + const nextPageToken = await fetchMemos(); + nextPageTokenRef.current = nextPageToken; + setIsRequesting(false); + }); }, [tagQuery, textQuery]); const fetchMemos = async () => { @@ -50,14 +55,12 @@ const Home = () => { if (tagQuery) { filters.push(`tag == "${tagQuery}"`); } - setIsRequesting(true); - const data = await memoStore.fetchMemos({ + const { nextPageToken } = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), pageToken: nextPageTokenRef.current, }); - setIsRequesting(false); - nextPageTokenRef.current = data.nextPageToken; + return nextPageToken; }; const handleEditPrevious = useCallback(() => { diff --git a/web/src/pages/Timeline.tsx b/web/src/pages/Timeline.tsx index fb95ff64..568950a7 100644 --- a/web/src/pages/Timeline.tsx +++ b/web/src/pages/Timeline.tsx @@ -1,6 +1,6 @@ -import { Button, Divider, IconButton } from "@mui/joy"; +import { Button, IconButton } from "@mui/joy"; import clsx from "clsx"; -import { Fragment, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import ActivityCalendar from "@/components/ActivityCalendar"; import Empty from "@/components/Empty"; import Icon from "@/components/Icon"; @@ -11,58 +11,38 @@ import MobileHeader from "@/components/MobileHeader"; import { TimelineSidebar, TimelineSidebarDrawer } from "@/components/TimelineSidebar"; import { memoServiceClient } from "@/grpcweb"; import { DAILY_TIMESTAMP, DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; -import { getNormalizedTimeString, getTimeStampByDate } from "@/helpers/datetime"; +import { getTimeStampByDate } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; import useFilterWithUrlParams from "@/hooks/useFilterWithUrlParams"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import i18n from "@/i18n"; import { useMemoList, useMemoStore } from "@/store/v1"; -import { Memo } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; -interface GroupedByMonthItem { - // Format: 2021-1 - month: string; - data: Record; - memos: Memo[]; -} - -const groupByMonth = (dateCountMap: Record, memos: Memo[]): GroupedByMonthItem[] => { - const groupedByMonth: GroupedByMonthItem[] = []; - - Object.entries(dateCountMap).forEach(([date, count]) => { - const month = date.split("-").slice(0, 2).join("-"); - const existingMonth = groupedByMonth.find((group) => group.month === month); - if (existingMonth) { - existingMonth.data[date] = count; - } else { - const monthMemos = memos.filter((memo) => getNormalizedTimeString(memo.displayTime).startsWith(month)); - groupedByMonth.push({ month, data: { [date]: count }, memos: monthMemos }); - } - }); - - return groupedByMonth.filter((group) => group.memos.length > 0).sort((a, b) => getTimeStampByDate(b.month) - getTimeStampByDate(a.month)); -}; - const Timeline = () => { const t = useTranslate(); const { md } = useResponsiveWidth(); const user = useCurrentUser(); const memoStore = useMemoStore(); const memoList = useMemoList(); + const { tag: tagQuery, text: textQuery } = useFilterWithUrlParams(); const [activityStats, setActivityStats] = useState>({}); - const [selectedDay, setSelectedDay] = useState(); + const [selectedDateString, setSelectedDateString] = useState(new Date().toDateString()); const [isRequesting, setIsRequesting] = useState(true); const nextPageTokenRef = useRef(undefined); - const { tag: tagQuery, text: textQuery } = useFilterWithUrlParams(); - const sortedMemos = memoList.value.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); - const groupedByMonth = groupByMonth(activityStats, sortedMemos); + const sortedMemos = memoList.value.sort((a, b) => getTimeStampByDate(a.displayTime) - getTimeStampByDate(b.displayTime)); + const monthString = new Date(selectedDateString).getFullYear() + "-" + (new Date(selectedDateString).getMonth() + 1); useEffect(() => { + setIsRequesting(true); nextPageTokenRef.current = undefined; - memoList.reset(); - fetchMemos(); - }, [selectedDay, tagQuery, textQuery]); + setTimeout(async () => { + memoList.reset(); + const nextPageToken = await fetchMemos(); + nextPageTokenRef.current = nextPageToken; + setIsRequesting(false); + }); + }, [selectedDateString, tagQuery, textQuery]); useEffect(() => { (async () => { @@ -82,7 +62,15 @@ const Timeline = () => { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, filter: filters.join(" && "), }); - setActivityStats(stats); + + setActivityStats( + Object.fromEntries( + Object.entries(stats).filter(([date]) => { + const d = new Date(date); + return `${d.getFullYear()}-${d.getMonth() + 1}` === monthString; + }), + ), + ); })(); }, [sortedMemos.length]); @@ -98,20 +86,18 @@ const Timeline = () => { if (tagQuery) { filters.push(`tag == "${tagQuery}"`); } - if (selectedDay) { - const selectedDateStamp = getTimeStampByDate(selectedDay) + new Date().getTimezoneOffset() * 60 * 1000; + if (selectedDateString) { + const selectedDateStamp = getTimeStampByDate(selectedDateString); filters.push( ...[`display_time_after == ${selectedDateStamp / 1000}`, `display_time_before == ${(selectedDateStamp + DAILY_TIMESTAMP) / 1000}`], ); } - setIsRequesting(true); - const data = await memoStore.fetchMemos({ + const { nextPageToken } = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), pageToken: nextPageTokenRef.current, }); - setIsRequesting(false); - nextPageTokenRef.current = data.nextPageToken; + return nextPageToken; }; const handleNewMemo = () => { @@ -132,7 +118,7 @@ const Timeline = () => {
setSelectedDay(undefined)} + onClick={() => setSelectedDateString(new Date().toDateString())} > {t("timeline.title")} @@ -145,43 +131,44 @@ const Timeline = () => {
- + - {groupedByMonth.map((group, index) => ( - -
-
-
- - {new Date(group.month).toLocaleString(i18n.language, { month: "short", timeZone: "UTC" })} - - {new Date(group.month).getUTCFullYear()} -
- setSelectedDay(date)} /> -
+
+
+
+ + {new Date(selectedDateString).toLocaleDateString(i18n.language, { month: "short", day: "numeric" })} + + {new Date(monthString).getFullYear()} +
+ setSelectedDateString(date)} + /> +
-
- {group.memos.map((memo, index) => ( -
- -
- {index !== group.memos.length - 1 && ( -
- )} -
- -
-
+
+ {sortedMemos.map((memo, index) => ( +
+ +
+ {index !== sortedMemos.length - 1 && ( +
+ )} +
+
- ))} +
-
- {index !== groupedByMonth.length - 1 && } - - ))} + ))} +
+
+ {isRequesting ? (
diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index f0648137..89dcea33 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -59,9 +59,14 @@ const UserProfile = () => { return; } + setIsRequesting(true); nextPageTokenRef.current = undefined; - memoList.reset(); - fetchMemos(); + setTimeout(async () => { + memoList.reset(); + const nextPageToken = await fetchMemos(); + nextPageTokenRef.current = nextPageToken; + setIsRequesting(false); + }); }, [user, tagQuery, textQuery]); const fetchMemos = async () => { @@ -80,14 +85,12 @@ const UserProfile = () => { if (tagQuery) { filters.push(`tag == "${tagQuery}"`); } - setIsRequesting(true); - const data = await memoStore.fetchMemos({ + const { nextPageToken } = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), pageToken: nextPageTokenRef.current, }); - setIsRequesting(false); - nextPageTokenRef.current = data.nextPageToken; + return nextPageToken; }; const handleCopyProfileLink = () => { diff --git a/web/src/store/v1/tag.ts b/web/src/store/v1/tag.ts index bf7142a0..c05e076a 100644 --- a/web/src/store/v1/tag.ts +++ b/web/src/store/v1/tag.ts @@ -28,14 +28,5 @@ export const useTagStore = create( const { tagAmounts } = await memoServiceClient.listMemoTags({ parent: "memos/-" }); set({ tagAmounts }); }, - deleteTag: async (tagName: string) => { - await memoServiceClient.deleteMemoTag({ - parent: "memos/-", - tag: tagName, - }); - const { tagAmounts } = get(); - delete tagAmounts[tagName]; - set({ tagAmounts }); - }, })), );