refactor: simplify entry actions handling and improve context menu integration

- Removed the 'compact' parameter from several components to streamline the entry actions logic.
- Updated the CommandActionButton usage in ActionBar for better context menu interaction.
- Enhanced the ActionBar component to handle mouse position for context menu display.
- Cleaned up imports and adjusted related components to reflect the changes in props and structure.

These modifications aim to improve code clarity and maintainability while ensuring consistent functionality across entry actions.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-09-12 01:23:28 +08:00
parent 418d99aae9
commit a5dfea871c
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
7 changed files with 83 additions and 60 deletions

View File

@ -20,10 +20,10 @@ export const CommandActionButton = ({
return (
<ActionButton
ref={ref}
icon={icon}
{...rest}
tooltip={label.title}
tooltipDescription={label.description}
{...rest}
icon={icon}
/>
)
}

View File

@ -214,19 +214,8 @@ export const HIDE_ACTIONS_IN_ENTRY_CONTEXT_MENU = [
COMMAND_ID.settings.customizeToolbar,
COMMAND_ID.entry.readability,
COMMAND_ID.entry.exportAsPDF,
// Copy
COMMAND_ID.entry.copyTitle,
COMMAND_ID.entry.copyLink,
]
export const useEntryActions = ({
entryId,
view,
compact,
}: {
entryId: string
view: FeedViewType
compact?: boolean
}) => {
export const useEntryActions = ({ entryId, view }: { entryId: string; view: FeedViewType }) => {
const entry = useEntry(entryId, entrySelector)
const { isCollection, entryId: routeEntryId } = useRouteParams()
const isInCollection = useIsEntryStarred(entryId)
@ -428,7 +417,7 @@ export const useEntryActions = ({
new EntryActionMenuItem({
id: COMMAND_ID.entry.tts,
onClick: runCmdFn(COMMAND_ID.entry.tts, [{ entryId }]),
hide: !IN_ELECTRON || compact || !entry.hasContent,
hide: !IN_ELECTRON || !entry.hasContent,
shortcut: shortcuts[COMMAND_ID.entry.tts],
entryId,
}),
@ -437,7 +426,6 @@ export const useEntryActions = ({
onClick: runCmdFn(COMMAND_ID.entry.readability, [{ entryId, entryUrl: entry.url! }]),
hide:
!!entry.readability ||
compact ||
(view && views.find((v) => v.view === view)?.wideMode) ||
!entry.url,
active: isEntryInReadability,
@ -508,7 +496,6 @@ export const useEntryActions = ({
isShowAITranslationAuto,
isShowAITranslationOnce,
isCollection,
compact,
isEntryInReadability,
integrationSettings.customIntegration,
integrationSettings.enableCustomIntegration,
@ -520,13 +507,11 @@ export const useEntryActions = ({
export const useSortedEntryActions = ({
entryId,
view,
compact,
}: {
entryId: string
view: FeedViewType
compact?: boolean
}) => {
const entryActions = useEntryActions({ entryId, view, compact })
const entryActions = useEntryActions({ entryId, view })
const orderMap = useToolbarOrderMap()
const mainAction = useMemo(
() =>

View File

@ -1,11 +1,12 @@
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
import { Spring } from "@follow/components/constants/spring.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import { getMousePosition } from "@follow/components/hooks/useMouse.js"
import { ActionButton } from "@follow/components/ui/button/action-button.js"
import { FeedViewType, views } from "@follow/constants"
import { useEntry } from "@follow/store/entry/hooks"
import { unreadSyncService } from "@follow/store/unread/store"
import { cn } from "@follow/utils/utils"
import { AnimatePresence, m } from "motion/react"
import { AnimatePresence } from "motion/react"
import type { FC, MouseEvent, PropsWithChildren, TouchEvent } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
@ -20,12 +21,13 @@ import {
} from "~/atoms/context-menu"
import { useGeneralSettingKey } from "~/atoms/settings/general"
import { FocusablePresets } from "~/components/common/Focusable"
import { CommandActionButton } from "~/components/ui/button/CommandActionButton"
import { useEntryIsRead } from "~/hooks/biz/useAsRead"
import { useContextMenuActionShortCutTrigger } from "~/hooks/biz/useContextMenuActionShortCutTrigger"
import {
EntryActionMenuItem,
HIDE_ACTIONS_IN_ENTRY_CONTEXT_MENU,
useEntryActions,
useSortedEntryActions,
} from "~/hooks/biz/useEntryActions"
import { useFeature } from "~/hooks/biz/useFeature"
import { useFeedActions } from "~/hooks/biz/useFeedActions"
@ -34,8 +36,7 @@ import { getRouteParams, useRouteParams, useRouteParamsSelector } from "~/hooks/
import { useContextMenu } from "~/hooks/common/useContextMenu"
import { copyToClipboard } from "~/lib/clipboard"
import { COMMAND_ID } from "~/modules/command/commands/id"
import { EntryHeaderActions } from "~/modules/entry-content/actions/header-actions"
import { MoreActions } from "~/modules/entry-content/actions/more-actions"
import type { FollowCommandId } from "~/modules/command/types"
export const EntryItemWrapper: FC<
{
@ -231,30 +232,58 @@ export const EntryItemWrapper: FC<
{...(!isMobile ? { onTouchStart: handleClick } : {})}
>
{children}
<AnimatePresence>{showAction && isWide && <ActionBar entryId={entryId} />}</AnimatePresence>
<AnimatePresence>
{showAction && isWide && (
<ActionBar
openContextMenu={() => {
const { x, y } = getMousePosition()
const mouseEvent = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
})
contextMenuProps.onContextMenu?.(mouseEvent as unknown as MouseEvent<HTMLElement>)
}}
entryId={entryId}
/>
)}
</AnimatePresence>
</Link>
</div>
)
}
const ActionBar = ({ entryId }: { entryId: string }) => {
const { mainAction: entryActions } = useSortedEntryActions({
entryId,
view: FeedViewType.SocialMedia,
})
const SHOW_ACTION_BAR_ACTIONS_ORDER = [
// Copy
COMMAND_ID.entry.copyTitle,
COMMAND_ID.entry.copyLink,
COMMAND_ID.entry.star,
COMMAND_ID.entry.read,
COMMAND_ID.entry.openInBrowser,
COMMAND_ID.entry.tip,
COMMAND_ID.entry.share,
] as FollowCommandId[]
const SHOW_ACTION_BAR_ACTIONS = new Set<FollowCommandId>(SHOW_ACTION_BAR_ACTIONS_ORDER)
const ActionBar = ({
entryId,
openContextMenu,
}: {
entryId: string
openContextMenu: () => void
}) => {
const { view } = useRouteParams()
const entryActions = useEntryActions({ entryId, view })
if (entryActions.length === 0) return null
return (
<m.div
initial={{ opacity: 0, scale: 0.9, translateY: "-1/2" }}
animate={{ opacity: 1, scale: 1, translateY: "-1/2" }}
exit={{ opacity: 0, scale: 0.9, translateY: "-1/2" }}
transition={Spring.presets.smooth}
<div
className={cn(
"absolute -right-2 top-0 -translate-y-1/2 rounded-lg border border-gray-200 bg-white/90 p-1 shadow-sm backdrop-blur-sm dark:border-neutral-900 dark:bg-neutral-900",
view === FeedViewType.All && "top-1/2",
view === FeedViewType.All && "right-1 top-1/2",
)}
onClick={(e) => {
e.stopPropagation()
@ -262,9 +291,31 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
}}
>
<div className="flex items-center gap-1">
<EntryHeaderActions entryId={entryId} view={view} compact />
<MoreActions entryId={entryId} view={view} compact hideCustomizeToolbar />
{(
entryActions.filter(
(item) => item instanceof EntryActionMenuItem && SHOW_ACTION_BAR_ACTIONS.has(item.id),
) as EntryActionMenuItem[]
)
.sort(
(a, b) =>
SHOW_ACTION_BAR_ACTIONS_ORDER.indexOf(a.id) -
SHOW_ACTION_BAR_ACTIONS_ORDER.indexOf(b.id),
)
.map((item) => (
<CommandActionButton
key={item.id}
onClick={item.onClick}
size="xs"
commandId={item.id}
/>
))}
<ActionButton
onClick={openContextMenu}
size="xs"
icon={<i className="i-mingcute-more-1-fill" />}
/>
</div>
</m.div>
</div>
)
}

View File

@ -14,16 +14,8 @@ import { EntryActionDropdownItem, useSortedEntryActions } from "~/hooks/biz/useE
import { useCommand } from "~/modules/command/hooks/use-command"
import type { FollowCommandId } from "~/modules/command/types"
export const EntryHeaderActions = ({
entryId,
view,
compact,
}: {
entryId: string
view: FeedViewType
compact?: boolean
}) => {
const { mainAction: actionConfigs } = useSortedEntryActions({ entryId, view, compact })
export const EntryHeaderActions = ({ entryId, view }: { entryId: string; view: FeedViewType }) => {
const { mainAction: actionConfigs } = useSortedEntryActions({ entryId, view })
return actionConfigs
.filter((item) => item instanceof MenuItemText || item instanceof EntryActionDropdownItem)
@ -40,7 +32,6 @@ export const EntryHeaderActions = ({
clickableDisabled={config.disabled}
highlightMotion={config.notice}
id={`${config.entryId}/${config.id}`}
size={compact ? "xs" : "base"}
/>
)

View File

@ -26,12 +26,12 @@ import type { FollowCommandId } from "~/modules/command/types"
export const MoreActions = ({
entryId,
view,
compact,
hideCustomizeToolbar = false,
}: {
entryId: string
view: FeedViewType
compact?: boolean
hideCustomizeToolbar?: boolean
}) => {
const { moreAction } = useSortedEntryActions({ entryId, view })
@ -78,10 +78,7 @@ export const MoreActions = ({
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ActionButton
icon={<i className="i-mingcute-more-1-fill" />}
size={compact ? "xs" : "base"}
/>
<ActionButton icon={<i className="i-mingcute-more-1-fill" />} />
</DropdownMenuTrigger>
<RootPortal>
<DropdownMenuContent>

View File

@ -7,12 +7,12 @@ import { MoreActions } from "../../../actions/more-actions"
import { useEntryHeaderContext } from "./context"
function EntryHeaderActionsContainerImpl() {
const { entryId, compact } = useEntryHeaderContext()
const { entryId } = useEntryHeaderContext()
const { view } = useRouteParams()
return (
<div className="relative flex shrink-0 items-center justify-end gap-2">
<EntryHeaderActions entryId={entryId} view={view} compact={compact} />
<EntryHeaderActions entryId={entryId} view={view} />
<MoreActions entryId={entryId} view={view} />
</div>
)

View File

@ -11,7 +11,6 @@ import type { EntryHeaderProps } from "../types"
interface EntryHeaderContextValue {
entryId: string
compact?: boolean
}
const EntryHeaderContext = createContext<EntryHeaderContextValue | null>(null)