diff --git a/icons/mgc/question_cute_re.svg b/icons/mgc/question_cute_re.svg new file mode 100644 index 000000000..c769c5cd4 --- /dev/null +++ b/icons/mgc/question_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/components/ui/accordion.tsx b/src/renderer/src/components/ui/accordion.tsx index 35c9dfdb0..aad68fa02 100644 --- a/src/renderer/src/components/ui/accordion.tsx +++ b/src/renderer/src/components/ui/accordion.tsx @@ -2,6 +2,9 @@ import * as AccordionPrimitive from "@radix-ui/react-accordion" import { cn } from "@renderer/lib/utils" import * as React from "react" +/** + * @deprecated please use `src/renderer/src/components/ui/collapse/Collapse.tsx` + */ const Accordion = AccordionPrimitive.Root const AccordionItem = React.forwardRef< diff --git a/src/renderer/src/components/ui/collapse/Collapse.tsx b/src/renderer/src/components/ui/collapse/Collapse.tsx index 71e2901ef..941a7fdbe 100644 --- a/src/renderer/src/components/ui/collapse/Collapse.tsx +++ b/src/renderer/src/components/ui/collapse/Collapse.tsx @@ -1,15 +1,32 @@ import { cn } from "@renderer/lib/utils" +import { createContextState } from "foxact/context-state" import type { Variants } from "framer-motion" import { AnimatePresence, m } from "framer-motion" import * as React from "react" -import { microReboundPreset } from "../constants/spring" - interface CollapseProps { title: React.ReactNode + hideArrow?: boolean } +const [CollapseStateProvider, useCurrentCollapseId, useSetCurrentCollapseId] = + createContextState(null) +export const CollapseGroup: Component = ({ children }) => ( + {children} +) export const Collapse: Component = (props) => { const [isOpened, setIsOpened] = React.useState(false) + const id = React.useId() + const setCurrentId = useSetCurrentCollapseId() + const currentId = useCurrentCollapseId() + + React.useEffect(() => { + if (isOpened) { + setCurrentId(id) + } + }, [id, isOpened, setCurrentId]) + React.useEffect(() => { + setIsOpened(currentId === id) + }, [currentId, id]) return ( props.onOpenChange(!props.isOpened)} > {props.title} -
- -
+ {!props.hideArrow && ( +
+ +
+ )} {props.children} @@ -57,7 +76,11 @@ export const CollapseContent: Component<{ open: { opacity: 1, height: "auto", - transition: microReboundPreset, + + transition: { + type: "spring", + mass: 0.2, + }, }, collapsed: { opacity: 0, diff --git a/src/renderer/src/components/ui/modal/stacked/constants.ts b/src/renderer/src/components/ui/modal/stacked/constants.ts index 631a9b304..5833da914 100644 --- a/src/renderer/src/components/ui/modal/stacked/constants.ts +++ b/src/renderer/src/components/ui/modal/stacked/constants.ts @@ -18,4 +18,4 @@ export const modalMontionConfig = { transition: microReboundPreset, } -export const MODAL_STACK_Z_INDEX = 100 +export const MODAL_STACK_Z_INDEX = 40 diff --git a/src/renderer/src/modules/entry-column/EntryListHeader.tsx b/src/renderer/src/modules/entry-column/EntryListHeader.tsx new file mode 100644 index 000000000..f4bfd1637 --- /dev/null +++ b/src/renderer/src/modules/entry-column/EntryListHeader.tsx @@ -0,0 +1,192 @@ +import { + setGeneralSetting, + useGeneralSettingKey, +} from "@renderer/atoms/settings/general" +import { useWhoami } from "@renderer/atoms/user" +import { ActionButton } from "@renderer/components/ui/button" +import { DividerVertical } from "@renderer/components/ui/divider" +import { useModalStack } from "@renderer/components/ui/modal" +import { EllipsisHorizontalTextWithTooltip } from "@renderer/components/ui/typography" +import { + FEED_COLLECTION_LIST, + ROUTE_ENTRY_PENDING, + views, +} from "@renderer/constants" +import { shortcuts } from "@renderer/constants/shortcuts" +import { + useRouteParms, +} from "@renderer/hooks/biz/useRouteParams" +import { useIsOnline } from "@renderer/hooks/common" +import { FeedViewType } from "@renderer/lib/enum" +import { cn, getOS, isBizId } from "@renderer/lib/utils" +import { useRefreshFeedMutation } from "@renderer/queries/feed" +import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed" +import type { FC } from "react" + +import { Daily } from "../entry-content/daily" +import { EntryHeader } from "../entry-content/header" +import { MarkAllButton } from "./mark-all-button" + +export const EntryListHeader: FC<{ + totalCount: number + refetch: () => void + isRefreshing: boolean + hasUpdate: boolean +}> = ({ totalCount, refetch, isRefreshing, hasUpdate }) => { + const routerParams = useRouteParms() + + const unreadOnly = useGeneralSettingKey("unreadOnly") + + const { feedId, entryId, view } = routerParams + + const headerTitle = useFeedHeaderTitle() + const os = getOS() + + const titleAtBottom = window.electron && os === "macOS" + const isInCollectionList = feedId === FEED_COLLECTION_LIST + + const titleInfo = !!headerTitle && ( +
+
+ + {headerTitle} + +
+
+ {totalCount || 0} + {" "} + {unreadOnly && !isInCollectionList ? "Unread" : ""} + {" "} + Items +
+
+ ) + const { mutateAsync: refreshFeed, isPending } = useRefreshFeedMutation( + routerParams.feedId, + ) + + const user = useWhoami() + const isOnline = useIsOnline() + + const feed = useFeedById(routerParams.feedId) + + const titleStyleBasedView = [ + "pl-12", + "pl-7", + "pl-7", + "pl-7", + "px-5", + "pl-12", + ] + + return ( +
+
+ {!titleAtBottom && titleInfo} + +
e.stopPropagation()} + > + {views[view].wideMode && + entryId && + entryId !== ROUTE_ENTRY_PENDING && ( + <> + + + + )} + + {view === FeedViewType.SocialMedia && } + + {isOnline ? ( + feed?.ownerUserId === user?.id && isBizId(routerParams.feedId!) ? + ( + { + refreshFeed() + }} + > + + + ) : + ( + { + refetch() + }} + > + + + ) + ) : null} + setGeneralSetting("unreadOnly", !unreadOnly)} + > + {unreadOnly ? ( + + ) : ( + + )} + + +
+
+ {titleAtBottom && titleInfo} +
+ ) +} + +const DailyReportButton: FC = () => { + const { present } = useModalStack() + + return ( + { + present({ + title: "Daily Report", + content: () => ( + + ), + }) + }} + tooltip="Daily Report" + > + + + ) +} diff --git a/src/renderer/src/modules/entry-column/index.tsx b/src/renderer/src/modules/entry-column/index.tsx index 6a2a0e967..bc080cd1b 100644 --- a/src/renderer/src/modules/entry-column/index.tsx +++ b/src/renderer/src/modules/entry-column/index.tsx @@ -1,36 +1,22 @@ -import { - setGeneralSetting, - useGeneralSettingKey, -} from "@renderer/atoms/settings/general" -import { useWhoami } from "@renderer/atoms/user" import { m } from "@renderer/components/common/Motion" import { FeedFoundCanBeFollowError } from "@renderer/components/errors/FeedFoundCanBeFollowErrorFallback" import { FeedNotFound } from "@renderer/components/errors/FeedNotFound" import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height" -import { ActionButton } from "@renderer/components/ui/button" -import { DividerVertical } from "@renderer/components/ui/divider" import { LoadingCircle } from "@renderer/components/ui/loading" import { ScrollArea } from "@renderer/components/ui/scroll-area" -import { EllipsisHorizontalTextWithTooltip } from "@renderer/components/ui/typography" import { FEED_COLLECTION_LIST, - ROUTE_ENTRY_PENDING, ROUTE_FEED_PENDING, views, } from "@renderer/constants" -import { shortcuts } from "@renderer/constants/shortcuts" import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" import { useRouteParamsSelector, useRouteParms, } from "@renderer/hooks/biz/useRouteParams" -import { useIsOnline } from "@renderer/hooks/common/useIsOnline" -import { cn, getOS, isBizId } from "@renderer/lib/utils" -import { EntryHeader } from "@renderer/modules/entry-content/header" -import { useFeed, useRefreshFeedMutation } from "@renderer/queries/feed" +import { cn, isBizId } from "@renderer/lib/utils" +import { useFeed } from "@renderer/queries/feed" import { entryActions, useEntry } from "@renderer/store/entry" -import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed" -import type { FC } from "react" import { useCallback, useEffect, useRef } from "react" import type { ScrollSeekConfiguration, @@ -39,6 +25,7 @@ import type { } from "react-virtuoso" import { VirtuosoGrid } from "react-virtuoso" +import { EntryListHeader } from "./EntryListHeader" import { useEntriesByView, useEntryMarkReadHandler } from "./hooks" import { EntryItem, @@ -46,7 +33,6 @@ import { EntryItemSkeletonWithDelayShow, } from "./item" import { EntryEmptyList, EntryList, EntryListContent } from "./lists" -import { MarkAllButton } from "./mark-all-button" import { girdClassNames } from "./styles" const scrollSeekConfiguration: ScrollSeekConfiguration = { @@ -147,7 +133,7 @@ export function EntryColumn() { !entries.isLoading && !entries.error && } - { throw new FeedFoundCanBeFollowError(feedQuery.data.feed) } - -const ListHeader: FC<{ - totalCount: number - refetch: () => void - isRefreshing: boolean - hasUpdate: boolean -}> = ({ totalCount, refetch, isRefreshing, hasUpdate }) => { - const routerParams = useRouteParms() - - const unreadOnly = useGeneralSettingKey("unreadOnly") - - const { feedId, entryId, view } = routerParams - - const headerTitle = useFeedHeaderTitle() - const os = getOS() - - const titleAtBottom = window.electron && os === "macOS" - const isInCollectionList = feedId === FEED_COLLECTION_LIST - - const titleInfo = !!headerTitle && ( -
-
- - {headerTitle} - -
-
- {totalCount || 0} - {" "} - {unreadOnly && !isInCollectionList ? "Unread" : ""} - {" "} - Items -
-
- ) - const { mutateAsync: refreshFeed, isPending } = useRefreshFeedMutation( - routerParams.feedId, - ) - - const user = useWhoami() - const isOnline = useIsOnline() - - const feed = useFeedById(routerParams.feedId) - - const titleStyleBasedView = [ - "pl-12", - "pl-7", - "pl-7", - "pl-7", - "px-5", - "pl-12", - ] - - return ( -
-
- {!titleAtBottom && titleInfo} - -
e.stopPropagation()} - > - {views[view].wideMode && - entryId && - entryId !== ROUTE_ENTRY_PENDING && ( - <> - - - - )} - {isOnline ? ( - feed?.ownerUserId === user?.id && isBizId(routerParams.feedId!) ? - ( - { - refreshFeed() - }} - > - - - ) : - ( - { - refetch() - }} - > - - - ) - ) : null} - setGeneralSetting("unreadOnly", !unreadOnly)} - > - {unreadOnly ? ( - - ) : ( - - )} - - -
-
- {titleAtBottom && titleInfo} -
- ) -} diff --git a/src/renderer/src/modules/entry-column/mark-all-button.tsx b/src/renderer/src/modules/entry-column/mark-all-button.tsx index f187b490f..54ae634ed 100644 --- a/src/renderer/src/modules/entry-column/mark-all-button.tsx +++ b/src/renderer/src/modules/entry-column/mark-all-button.tsx @@ -1,3 +1,4 @@ +import { PopoverPortal } from "@radix-ui/react-popover" import { ActionButton, StyledButton } from "@renderer/components/ui/button" import { Popover, @@ -7,19 +8,22 @@ import { } from "@renderer/components/ui/popover" import { shortcuts } from "@renderer/constants/shortcuts" import { useRouteParms } from "@renderer/hooks/biz/useRouteParams" -import { subscriptionActions, useFolderFeedsByFeedId } from "@renderer/store/subscription" -import { useCallback, useState } from "react" +import { + subscriptionActions, + useFolderFeedsByFeedId, +} from "@renderer/store/subscription" +import { forwardRef, useCallback, useState } from "react" -export const MarkAllButton = ({ - filter, - className, -}: { - filter?: { - startTime: number - endTime: number +export const MarkAllButton = forwardRef< + HTMLButtonElement, + { + filter?: { + startTime: number + endTime: number + } + className?: string } - className?: string -}) => { +>(({ filter, className }, ref) => { const [markPopoverOpen, setMarkPopoverOpen] = useState(false) const routerParams = useRouteParms() @@ -35,13 +39,13 @@ export const MarkAllButton = ({ if (typeof routerParams.feedId === "number" || routerParams.isAllFeeds) { subscriptionActions.markReadByView(view, filter) } else if (folderIds) { + subscriptionActions.markReadByFeedIds(view, folderIds, filter) + } else if (routerParams.feedId) { subscriptionActions.markReadByFeedIds( view, - folderIds, + routerParams.feedId?.split(","), filter, ) - } else if (routerParams.feedId) { - subscriptionActions.markReadByFeedIds(view, routerParams.feedId?.split(","), filter) } }, [feedId, folderIds, filter, routerParams]) @@ -52,26 +56,30 @@ export const MarkAllButton = ({ shortcut={shortcuts.entries.markAllAsRead.key} tooltip="Mark All as Read" className={className} + ref={ref} > - -
Mark all as read?
-
- - Cancel - - {/* TODO */} - { - handleMarkAllAsRead() - setMarkPopoverOpen(false) - }} - > - Confirm - -
-
+ + +
Mark all as read?
+
+ + Cancel + + {/* TODO */} + { + handleMarkAllAsRead() + setMarkPopoverOpen(false) + }} + > + Confirm + +
+
+
) -} +}) diff --git a/src/renderer/src/modules/entry-column/social-media-item.tsx b/src/renderer/src/modules/entry-column/social-media-item.tsx index df87edc91..8f994a8af 100644 --- a/src/renderer/src/modules/entry-column/social-media-item.tsx +++ b/src/renderer/src/modules/entry-column/social-media-item.tsx @@ -5,6 +5,7 @@ import { RelativeTime } from "@renderer/components/ui/datetime" import { Media } from "@renderer/components/ui/media" import { usePreviewMedia } from "@renderer/components/ui/media/hooks" import { Skeleton } from "@renderer/components/ui/skeleton" +import { views } from "@renderer/constants" import { useAsRead } from "@renderer/hooks/biz/useAsRead" import { useEntryActions } from "@renderer/hooks/biz/useEntryActions" import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams" @@ -177,9 +178,10 @@ export const SocialMediaDateItem = ({ const dateString = dateObj.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" }) const startOfDay = new Date(dateObj.setHours(0, 0, 0, 0)).getTime() const endOfDay = new Date(dateObj.setHours(23, 59, 59, 999)).getTime() + const view = useRouteParamsSelector((s) => s.view) return ( -
+
{ +type DailyView = Extract< + FeedViewType, + FeedViewType.Articles | FeedViewType.SocialMedia +> +enum DayOf { + Today, + Yesterday, +} +interface DailyItemProps { + view: DailyView + day: DayOf +} + +const DailyItem = ({ view, day }: DailyItemProps) => { const dateObj = new Date() const nowHour = dateObj.getHours() @@ -40,9 +46,10 @@ export const DailyItem = ({ dateObj.setDate(dateObj.getDate() - 1) const dayBeforeYesterday8PM = dateObj.setHours(20, 0, 0, 0) + const isToday = day === DayOf.Today // For index 0, get the last past 8 AM or 8 PM; for index 1, get the second last past 8 AM or 8 PM. if (nowHour >= 20) { - if (index === 0) { + if (isToday) { endDate = today8PM - 1 startDate = today8AM title = "Today" @@ -52,7 +59,7 @@ export const DailyItem = ({ title = "Last Night" } } else if (nowHour >= 8) { - if (index === 0) { + if (isToday) { endDate = today8AM - 1 startDate = yesterday8PM title = "Last Night" @@ -62,7 +69,7 @@ export const DailyItem = ({ title = "Yesterday" } } else { - if (index === 0) { + if (isToday) { endDate = yesterday8PM - 1 startDate = yesterday8AM title = "Yesterday" @@ -73,6 +80,59 @@ export const DailyItem = ({ } } + return ( + + +
+ {title} + 's Top News +
+ + + + + +
    +
  • + Here is news selected by AI from your timeline + {" ("} + {new Date(startDate).toLocaleTimeString("en-US", { + weekday: "short", + hour: "numeric", + minute: "numeric", + })} + {" "} + - + {" "} + {new Date(endDate + 1).toLocaleTimeString("en-US", { + weekday: "short", + hour: "numeric", + minute: "numeric", + })} + {") "} + that may be important to you. +
  • +
  • Update daily at 8 AM and 8 PM.
  • +
+
+
+
+ )} + className="mx-auto w-full max-w-lg border-b pb-6 last:border-b-0" + > + + + ) +} + +const DailyReportContent: FC<{ + view: DailyView + startDate: number + endDate: number +}> = ({ endDate, startDate, view }) => { const content = useAuthQuery( defineQuery(["daily", view, startDate, endDate], async () => { const res = await apiClient.ai.daily.$get({ @@ -84,72 +144,54 @@ export const DailyItem = ({ return res.data }), { - enabled: value === `${index}`, + meta: { + persist: true, + }, }, ) + const [markAllButtonRef, setMarkAllButtonRef] = + useState(null) + return ( - - - - -
- -
- {title} - 's Top News -
- - - - - -
    -
  • - Here is news selected by AI from your timeline - {" ("} - {new Date(startDate).toLocaleTimeString("en-US", { weekday: "short", hour: "numeric", minute: "numeric" })} - {" "} - - - {" "} - {new Date(endDate + 1).toLocaleTimeString("en-US", { weekday: "short", hour: "numeric", minute: "numeric" })} - {") "} - that may be important to you. -
  • -
  • Update daily at 8 AM and 8 PM.
  • -
-
-
-
-
- - - {content.isLoading ? ( - - ) : ( - content.data && ( - {content.data} - ) - )} - -
- - Mark all as read -
-
-
-
-
+ + + + {content.isLoading ? ( + + ) : ( + content.data && ( + + {content.data} + + ) + )} + + {content.data && ( + + )} + + ) } @@ -157,17 +199,14 @@ export const Daily = ({ view, className, }: { - view: 0 | 1 + view: DailyView className?: string -}) => { - const [value, setValue] = useState() +}) => ( +
+ + - return ( -
- - - - -
- ) -} + +
+
+)