feat(ui): enhance context menu with hover effects, disable option, and quick list creation (#787)
* feat: disabled when exceed invite limitation * feat: add item hover * fix: create list minimum value is 0 * feat: add actions settings loading * feat: hide menu when enabled is false * feat: change opacity when menu is disabled * feat: add quick access for add feed to list * fix: unify list item hover style Signed-off-by: Innei <i@innei.in> * refactor: create list modal Signed-off-by: Innei <i@innei.in> * update Signed-off-by: Innei <i@innei.in> * unify style Signed-off-by: Innei <i@innei.in> * native app style Signed-off-by: Innei <i@innei.in> --------- Signed-off-by: Innei <i@innei.in> Co-authored-by: Innei <i@innei.in>
This commit is contained in:
parent
ce72b7a57f
commit
d2b5cc36e9
|
|
@ -28,6 +28,7 @@ const ContextMenuSubTrigger = React.forwardRef<
|
|||
inset && "pl-8",
|
||||
"center gap-2",
|
||||
className,
|
||||
props.disabled && "cursor-not-allowed opacity-30",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { useFeedClaimModal } from "~/modules/claim"
|
|||
import { FeedForm } from "~/modules/discover/feed-form"
|
||||
import { InboxForm } from "~/modules/discover/inbox-form"
|
||||
import { ListForm } from "~/modules/discover/list-form"
|
||||
import { ListCreationModalContent } from "~/modules/settings/tabs/lists/modals"
|
||||
import {
|
||||
getFeedById,
|
||||
useAddFeedToFeedList,
|
||||
|
|
@ -37,7 +38,6 @@ export const useFeedActions = ({
|
|||
const { t } = useTranslation()
|
||||
const feed = useFeedById(feedId)
|
||||
const subscription = useSubscriptionByFeedId(feedId)
|
||||
|
||||
const { present } = useModalStack()
|
||||
const deleteSubscription = useDeleteSubscription({})
|
||||
const claimFeed = useFeedClaimModal({
|
||||
|
|
@ -92,28 +92,43 @@ export const useFeedActions = ({
|
|||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_column.context_menu.add_feeds_to_list"),
|
||||
enabled: !!listByView?.length,
|
||||
submenu: listByView?.map((list) => {
|
||||
const isIncluded = list.feedIds.includes(feedId)
|
||||
return {
|
||||
label: list.title || "",
|
||||
submenu: [
|
||||
...listByView.map((list) => {
|
||||
const isIncluded = list.feedIds.includes(feedId)
|
||||
return {
|
||||
label: list.title || "",
|
||||
type: "text" as const,
|
||||
checked: isIncluded,
|
||||
click() {
|
||||
if (!isIncluded) {
|
||||
addFeedToListMutation({
|
||||
feedId,
|
||||
listId: list.id,
|
||||
})
|
||||
} else {
|
||||
removeFeedFromListMutation({
|
||||
feedId,
|
||||
listId: list.id,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: t("sidebar.feed_actions.create_list"),
|
||||
type: "text" as const,
|
||||
checked: isIncluded,
|
||||
icon: <i className="i-mgc-add-cute-re" />,
|
||||
click() {
|
||||
if (!isIncluded) {
|
||||
addFeedToListMutation({
|
||||
feedId,
|
||||
listId: list.id,
|
||||
})
|
||||
} else {
|
||||
removeFeedFromListMutation({
|
||||
feedId,
|
||||
listId: list.id,
|
||||
})
|
||||
}
|
||||
present({
|
||||
title: t("sidebar.feed_actions.create_list"),
|
||||
content: () => <ListCreationModalContent />,
|
||||
})
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator" as const,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export type NativeMenuItem = (
|
|||
type: "text"
|
||||
label: string
|
||||
click?: () => void
|
||||
enabled?: boolean
|
||||
/** only work in web app */
|
||||
icon?: React.ReactNode
|
||||
shortcut?: string
|
||||
|
|
@ -130,7 +129,7 @@ export const showNativeMenu = async (
|
|||
return {
|
||||
...item,
|
||||
icon: undefined,
|
||||
enabled: item.enabled ?? (item.click !== undefined || !!item.submenu),
|
||||
enabled: item.click !== undefined || (!!item.submenu && item.submenu.length > 0),
|
||||
click: undefined,
|
||||
submenu: item.submenu ? transformMenuItems(item.submenu) : undefined,
|
||||
shortcut: item.shortcut?.replace("Meta", "CmdOrCtrl"),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { LoadingCircle } from "~/components/ui/loading"
|
|||
import { views } from "~/constants"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import { FeedViewType as FeedViewTypeEnum } from "~/lib/enum"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { Queries } from "~/queries"
|
||||
import type { FlatEntryModel } from "~/store/entry"
|
||||
|
|
@ -39,17 +38,9 @@ function EntryItemImpl({ entry, view }: { entry: FlatEntryModel; view?: number }
|
|||
)
|
||||
|
||||
const Item: EntryListItemFC = getItemComponentByView(view as FeedViewType)
|
||||
const overlayItemClassName =
|
||||
view === FeedViewTypeEnum.Pictures || view === FeedViewTypeEnum.Videos ? "top-0" : ""
|
||||
|
||||
return (
|
||||
<EntryItemWrapper
|
||||
itemClassName={Item.wrapperClassName}
|
||||
entry={entry}
|
||||
view={view}
|
||||
overlay
|
||||
overlayItemClassName={overlayItemClassName}
|
||||
>
|
||||
<EntryItemWrapper itemClassName={Item.wrapperClassName} entry={entry} view={view}>
|
||||
<Item entryId={entry.entries.id} translation={translation.data} />
|
||||
</EntryItemWrapper>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import { useTranslation } from "react-i18next"
|
|||
import { useDebounceCallback } from "usehooks-ts"
|
||||
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
import { ListItemHoverOverlay } from "~/components/ui/list-item-hover-overlay"
|
||||
import { views } from "~/constants/tabs"
|
||||
import { useAsRead } from "~/hooks/biz/useAsRead"
|
||||
import { useEntryActions } from "~/hooks/biz/useEntryActions"
|
||||
import { useFeedActions } from "~/hooks/biz/useFeedActions"
|
||||
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { useAnyPointDown } from "~/hooks/common"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import { showNativeMenu } from "~/lib/native-menu"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { FlatEntryModel } from "~/store/entry"
|
||||
|
|
@ -21,11 +22,9 @@ export const EntryItemWrapper: FC<
|
|||
entry: FlatEntryModel
|
||||
view?: number
|
||||
itemClassName?: string
|
||||
overlayItemClassName?: string
|
||||
overlay?: boolean
|
||||
style?: React.CSSProperties
|
||||
} & PropsWithChildren
|
||||
> = ({ entry, view, overlay, children, itemClassName, overlayItemClassName, style }) => {
|
||||
> = ({ entry, view, children, itemClassName, style }) => {
|
||||
const { items } = useEntryActions({
|
||||
view,
|
||||
entry,
|
||||
|
|
@ -118,7 +117,9 @@ export const EntryItemWrapper: FC<
|
|||
className={cn(
|
||||
"relative",
|
||||
asRead ? "text-zinc-700 dark:text-neutral-400" : "text-zinc-900 dark:text-neutral-300",
|
||||
|
||||
views[view as FeedViewType]?.wideMode ? "rounded-md" : "-mx-2 px-2",
|
||||
"duration-200 hover:bg-theme-item-hover",
|
||||
(isActive || isContextMenuOpen) && "!bg-theme-item-active",
|
||||
itemClassName,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
|
|
@ -127,16 +128,7 @@ export const EntryItemWrapper: FC<
|
|||
onDoubleClick={handleDoubleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{overlay ? (
|
||||
<ListItemHoverOverlay
|
||||
isActive={isActive || isContextMenuOpen}
|
||||
className={overlayItemClassName}
|
||||
>
|
||||
{children}
|
||||
</ListItemHoverOverlay>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ const WideModeButton = () => {
|
|||
: t("entry_list_header.switch_to_normalmode")
|
||||
}
|
||||
>
|
||||
<i className={cn(isWideMode ? "i-mgc-align-justify-cute-re" : "i-mgc-layout-4-cute-re")} />
|
||||
<i className={cn(!isWideMode ? "i-mgc-align-justify-cute-re" : "i-mgc-layout-4-cute-re")} />
|
||||
</ActionButton>
|
||||
</ImpressionView>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscripti
|
|||
import { useFeedUnreadStore } from "~/store/unread"
|
||||
|
||||
import { useModalStack } from "../../components/ui/modal/stacked/hooks"
|
||||
import { ListCreationModalContent } from "../settings/tabs/lists/modals"
|
||||
import { useFeedListSortSelector } from "./atom"
|
||||
import { CategoryRemoveDialogContent } from "./category-remove-dialog"
|
||||
import { FeedItem } from "./item"
|
||||
import { feedColumnStyles } from "./styles"
|
||||
import { UnreadNumber } from "./unread-number"
|
||||
|
||||
type FeedId = string
|
||||
|
|
@ -135,9 +137,10 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
|
|||
<div tabIndex={-1} onClick={stopPropagation}>
|
||||
{!!showCollapse && (
|
||||
<div
|
||||
data-active={isActive || isContextMenuOpen}
|
||||
className={cn(
|
||||
"flex w-full cursor-menu items-center justify-between rounded-md px-2.5 transition-colors",
|
||||
(isActive || isContextMenuOpen) && "bg-native-active",
|
||||
"flex w-full cursor-menu items-center justify-between rounded-md px-2.5",
|
||||
feedColumnStyles.item,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -162,22 +165,37 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
|
|||
{
|
||||
type: "text",
|
||||
label: t("sidebar.feed_column.context_menu.add_feeds_to_list"),
|
||||
enabled: !!listList?.length,
|
||||
submenu: listList?.map((list) => ({
|
||||
label: list.title || "",
|
||||
type: "text",
|
||||
click() {
|
||||
return addMutation.mutate({
|
||||
feedIds: ids,
|
||||
listId: list.id,
|
||||
})
|
||||
},
|
||||
})),
|
||||
// @ts-expect-error
|
||||
submenu: listList
|
||||
?.map((list) => ({
|
||||
label: list.title || "",
|
||||
type: "text",
|
||||
click() {
|
||||
return addMutation.mutate({
|
||||
feedIds: ids,
|
||||
listId: list.id,
|
||||
})
|
||||
},
|
||||
}))
|
||||
.concat([
|
||||
// @ts-expect-error
|
||||
{ type: "separator" as const },
|
||||
{
|
||||
label: t("sidebar.feed_actions.create_list"),
|
||||
type: "text" as const,
|
||||
|
||||
click() {
|
||||
present({
|
||||
title: t("sidebar.feed_actions.create_list"),
|
||||
content: () => <ListCreationModalContent />,
|
||||
})
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "text",
|
||||
enabled: !!(folderName && typeof view === "number"),
|
||||
label: t("sidebar.feed_column.context_menu.change_to_other_view"),
|
||||
submenu: views
|
||||
.filter((v) => v.view !== view)
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
|
|||
className={cn(
|
||||
active === index && item.className,
|
||||
"flex h-11 flex-col items-center gap-1 text-xl",
|
||||
ELECTRON ? "hover:!bg-theme-vibrancyBg" : "",
|
||||
active === index && useHotkeysSwitch ? "bg-zinc-500/30" : "",
|
||||
ELECTRON ? "hover:!bg-theme-item-hover" : "",
|
||||
active === index && useHotkeysSwitch ? "bg-theme-item-active" : "",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
setActive(index)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { useListById } from "~/store/list"
|
|||
import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription"
|
||||
import { useFeedUnreadStore } from "~/store/unread"
|
||||
|
||||
import { feedColumnStyles } from "./styles"
|
||||
import { UnreadNumber } from "./unread-number"
|
||||
|
||||
interface FeedItemProps {
|
||||
|
|
@ -72,9 +73,10 @@ const FeedItemImpl = ({ view, feedId, className }: FeedItemProps) => {
|
|||
<>
|
||||
<div
|
||||
data-feed-id={feedId}
|
||||
data-active={isActive || isContextMenuOpen}
|
||||
className={cn(
|
||||
"flex w-full cursor-menu items-center justify-between rounded-md py-[2px] pr-2.5 text-sm font-medium leading-loose",
|
||||
(isActive || isContextMenuOpen) && "bg-native-active",
|
||||
feedColumnStyles.item,
|
||||
isFeed ? "py-[2px]" : "py-1.5",
|
||||
className,
|
||||
)}
|
||||
|
|
@ -204,9 +206,10 @@ const ListItemImpl: Component<{
|
|||
return (
|
||||
<div
|
||||
data-list-id={listId}
|
||||
data-active={isActive || isContextMenuOpen}
|
||||
className={cn(
|
||||
"flex w-full cursor-menu items-center justify-between rounded-md pr-2.5 text-sm font-medium leading-loose",
|
||||
(isActive || isContextMenuOpen) && "bg-native-active",
|
||||
feedColumnStyles.item,
|
||||
"py-1.5 pl-2.5",
|
||||
className,
|
||||
)}
|
||||
|
|
@ -275,10 +278,11 @@ const InboxItemImpl: Component<{
|
|||
if (!inbox) return null
|
||||
return (
|
||||
<div
|
||||
data-active={isActive || isContextMenuOpen}
|
||||
data-inbox-id={inboxId}
|
||||
className={cn(
|
||||
"flex w-full cursor-menu items-center justify-between rounded-md pr-2.5 text-sm font-medium leading-loose",
|
||||
(isActive || isContextMenuOpen) && "bg-native-active",
|
||||
feedColumnStyles.item,
|
||||
"py-[2px] pl-2.5",
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { useFeedUnreadStore } from "~/store/unread"
|
|||
|
||||
import { getFeedListSort, setFeedListSortBy, setFeedListSortOrder, useFeedListSort } from "./atom"
|
||||
import { SortableFeedList, SortByAlphabeticalInbox, SortByAlphabeticalList } from "./sort-by"
|
||||
import { feedColumnStyles } from "./styles"
|
||||
import { UnreadNumber } from "./unread-number"
|
||||
|
||||
const useFeedsGroupedData = (view: FeedViewType) => {
|
||||
|
|
@ -167,9 +168,10 @@ function FeedListImpl({ className, view }: { className?: string; view: number })
|
|||
|
||||
<ScrollArea.ScrollArea mask={false} flex viewportClassName="!px-3" rootClassName="h-full">
|
||||
<div
|
||||
data-active={feedId === FEED_COLLECTION_LIST}
|
||||
className={cn(
|
||||
"mt-1 flex h-8 w-full shrink-0 cursor-menu items-center gap-2 rounded-md px-2.5 transition-colors",
|
||||
feedId === FEED_COLLECTION_LIST && "bg-native-active",
|
||||
"mt-1 flex h-8 w-full shrink-0 cursor-menu items-center gap-2 rounded-md px-2.5",
|
||||
feedColumnStyles.item,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import clsx from "clsx"
|
||||
|
||||
export const feedColumnStyles = {
|
||||
item: clsx(
|
||||
!window.electron && tw`duration-200 hover:bg-theme-item-hover`,
|
||||
tw`data-[active=true]:!bg-theme-item-active`,
|
||||
),
|
||||
}
|
||||
|
|
@ -119,8 +119,8 @@ export function SettingModalLayout(
|
|||
{settings.map((t) => (
|
||||
<button
|
||||
key={t.path}
|
||||
className={`my-0.5 flex w-full items-center rounded-lg px-2.5 py-0.5 leading-loose text-theme-foreground/70 transition-colors ${
|
||||
tab === t.path ? "bg-theme-item-active text-theme-foreground/90" : ""
|
||||
className={`my-0.5 flex w-full items-center rounded-lg px-2.5 py-0.5 leading-loose text-theme-foreground/70 duration-200 hover:bg-theme-item-hover ${
|
||||
tab === t.path ? "!bg-theme-item-active !text-theme-foreground/90" : ""
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => setTab(t.path)}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ export const SettingInvitations = () => {
|
|||
</p>
|
||||
</div>
|
||||
<Button
|
||||
disabled={
|
||||
!limitation.data || (invitations?.data && invitations?.data?.length >= limitation.data)
|
||||
}
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t("invitation.confirmModal.title"),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
import { WEB_URL } from "@follow/shared/constants"
|
||||
|
||||
import { Avatar, AvatarImage } from "~/components/ui/avatar"
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { Divider } from "~/components/ui/divider"
|
||||
import { LoadingCircle } from "~/components/ui/loading"
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import { ScrollArea } from "~/components/ui/scroll-area"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip"
|
||||
import { views } from "~/constants"
|
||||
import { useAuthQuery, useI18n } from "~/hooks/common"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { Balance } from "~/modules/wallet/balance"
|
||||
import { Queries } from "~/queries"
|
||||
|
||||
import { ListCreationModalContent, ListFeedsModalContent } from "./modals"
|
||||
|
||||
export const SettingLists = () => {
|
||||
const t = useI18n()
|
||||
const listList = useAuthQuery(Queries.lists.list())
|
||||
|
||||
const { present } = useModalStack()
|
||||
|
||||
return (
|
||||
<section className="mt-4">
|
||||
<div className="mb-4 space-y-2 text-sm">
|
||||
<p>{t.settings("lists.info")}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.create"),
|
||||
content: () => <ListCreationModalContent />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re mr-1 text-base" />
|
||||
{t.settings("lists.create")}
|
||||
</Button>
|
||||
<Divider className="mb-6 mt-8" />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<ScrollArea.ScrollArea viewportClassName="max-h-[380px]">
|
||||
{listList.data?.length ? (
|
||||
<Table className="mt-4">
|
||||
<TableHeader className="border-b">
|
||||
<TableRow className="[&_*]:!font-semibold">
|
||||
<TableHead size="sm">{t.settings("lists.title")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.view")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.fee.label")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.subscriptions")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.earnings")}</TableHead>
|
||||
<TableHead size="sm" className="center">
|
||||
{t.common("words.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="border-t-[12px] border-transparent [&_td]:!px-3">
|
||||
{listList.data?.map((row) => (
|
||||
<TableRow key={row.title} className="h-8">
|
||||
<TableCell size="sm">
|
||||
<a
|
||||
target="_blank"
|
||||
href={`${WEB_URL}/list/${row.id}`}
|
||||
className="inline-flex items-center gap-2 font-semibold"
|
||||
>
|
||||
{row.image && (
|
||||
<Avatar className="size-6">
|
||||
<AvatarImage src={row.image} />
|
||||
</Avatar>
|
||||
)}
|
||||
<span className="inline-block max-w-[200px] truncate">{row.title}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn("inline-flex items-center", views[row.view].className)}
|
||||
>
|
||||
{views[row.view].icon}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t(views[row.view].name)}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{row.fee}
|
||||
<i className="i-mgc-power shrink-0 text-lg text-accent" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell size="sm">{row.subscriptionCount}</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Balance>{BigInt(row.purchaseAmount || 0n)}</Balance>
|
||||
</TableCell>
|
||||
<TableCell size="sm" className="center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.feeds.manage"),
|
||||
content: () => <ListFeedsModalContent id={row.id} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-inbox-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.manage")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.edit.label"),
|
||||
content: () => <ListCreationModalContent id={row.id} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-edit-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.edit")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : listList.isLoading ? (
|
||||
<LoadingCircle size="large" className="center absolute inset-0" />
|
||||
) : (
|
||||
<div className="mt-36 w-full text-center text-sm text-zinc-400">
|
||||
<p>{t.settings("lists.noLists")}</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea.ScrollArea>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -24,8 +24,7 @@ import {
|
|||
FormMessage,
|
||||
} from "~/components/ui/form"
|
||||
import { Input } from "~/components/ui/input"
|
||||
import { LoadingCircle } from "~/components/ui/loading"
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import { useCurrentModal } from "~/components/ui/modal"
|
||||
import { ScrollArea } from "~/components/ui/scroll-area"
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -35,15 +34,12 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip"
|
||||
import { views } from "~/constants"
|
||||
import { useAuthQuery, useI18n } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { createErrorToaster } from "~/lib/error-parser"
|
||||
import { cn, isBizId } from "~/lib/utils"
|
||||
import { isBizId } from "~/lib/utils"
|
||||
import type { FeedModel } from "~/models"
|
||||
import { ViewSelectorRadioGroup } from "~/modules/shared/ViewSelectorRadioGroup"
|
||||
import { Balance } from "~/modules/wallet/balance"
|
||||
import { Queries } from "~/queries"
|
||||
import {
|
||||
getFeedById,
|
||||
|
|
@ -54,143 +50,6 @@ import {
|
|||
import { useListById } from "~/store/list"
|
||||
import { subscriptionActions, useSubscriptionStore } from "~/store/subscription"
|
||||
|
||||
export const SettingLists = () => {
|
||||
const t = useI18n()
|
||||
const listList = useAuthQuery(Queries.lists.list())
|
||||
|
||||
const { present } = useModalStack()
|
||||
|
||||
return (
|
||||
<section className="mt-4">
|
||||
<div className="mb-4 space-y-2 text-sm">
|
||||
<p>{t.settings("lists.info")}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.create"),
|
||||
content: ({ dismiss }) => <ListCreationModalContent dismiss={dismiss} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re mr-1 text-base" />
|
||||
{t.settings("lists.create")}
|
||||
</Button>
|
||||
<Divider className="mb-6 mt-8" />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<ScrollArea.ScrollArea viewportClassName="max-h-[380px]">
|
||||
{listList.data?.length ? (
|
||||
<Table className="mt-4">
|
||||
<TableHeader className="border-b">
|
||||
<TableRow className="[&_*]:!font-semibold">
|
||||
<TableHead size="sm">{t.settings("lists.title")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.view")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.fee.label")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.subscriptions")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.earnings")}</TableHead>
|
||||
<TableHead size="sm" className="center">
|
||||
{t.common("words.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="border-t-[12px] border-transparent [&_td]:!px-3">
|
||||
{listList.data?.map((row) => (
|
||||
<TableRow key={row.title} className="h-8">
|
||||
<TableCell size="sm">
|
||||
<a
|
||||
target="_blank"
|
||||
href={`${WEB_URL}/list/${row.id}`}
|
||||
className="inline-flex items-center gap-2 font-semibold"
|
||||
>
|
||||
{row.image && (
|
||||
<Avatar className="size-6">
|
||||
<AvatarImage src={row.image} />
|
||||
</Avatar>
|
||||
)}
|
||||
<span className="inline-block max-w-[200px] truncate">{row.title}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn("inline-flex items-center", views[row.view].className)}
|
||||
>
|
||||
{views[row.view].icon}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t(views[row.view].name)}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{row.fee}
|
||||
<i className="i-mgc-power shrink-0 text-lg text-accent" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell size="sm">{row.subscriptionCount}</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Balance>{BigInt(row.purchaseAmount || 0n)}</Balance>
|
||||
</TableCell>
|
||||
<TableCell size="sm" className="center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.feeds.manage"),
|
||||
content: () => <ListFeedsModalContent id={row.id} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-inbox-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.manage")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.edit.label"),
|
||||
content: ({ dismiss }) => (
|
||||
<ListCreationModalContent dismiss={dismiss} id={row.id} />
|
||||
),
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-edit-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.edit")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : listList.isLoading ? (
|
||||
<LoadingCircle size="large" className="center absolute inset-0" />
|
||||
) : (
|
||||
<div className="mt-36 w-full text-center text-sm text-zinc-400">
|
||||
<p>{t.settings("lists.noLists")}</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea.ScrollArea>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
view: z.string(),
|
||||
title: z.string().min(1),
|
||||
|
|
@ -199,7 +58,8 @@ const formSchema = z.object({
|
|||
fee: z.number().min(0),
|
||||
})
|
||||
|
||||
const ListCreationModalContent = ({ dismiss, id }: { dismiss: () => void; id?: string }) => {
|
||||
export const ListCreationModalContent = ({ id }: { id?: string }) => {
|
||||
const { dismiss } = useCurrentModal()
|
||||
const { t } = useTranslation(["settings", "common"])
|
||||
|
||||
const list = useListById(id)
|
||||
|
|
@ -333,6 +193,7 @@ const ListCreationModalContent = ({ dismiss, id }: { dismiss: () => void; id?: s
|
|||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(value) => field.onChange(value.target.valueAsNumber)}
|
||||
/>
|
||||
<i className="i-mgc-power ml-4 shrink-0 text-xl text-accent" />
|
||||
|
|
@ -26,6 +26,9 @@ export function Component() {
|
|||
} else {
|
||||
getCallbackUrl().then((url) => {
|
||||
window.open(url, "_top")
|
||||
|
||||
// If you are in development, you can use the following code to open the app
|
||||
console.info("Open in development app", url.replace(DEEPLINK_SCHEME, "follow-dev://"))
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
|
@ -34,9 +37,9 @@ export function Component() {
|
|||
<div className="flex h-screen w-full flex-col items-center justify-center gap-10 px-4 pb-12 pt-[30vh]">
|
||||
<UserAvatar className="gap-8 px-10 py-4 text-2xl" />
|
||||
<h2 className="text-center">
|
||||
{t("redirect.successMessage", { APP_NAME })} <br />
|
||||
{t("redirect.successMessage", { app_name: APP_NAME })} <br />
|
||||
<br />
|
||||
{t("redirect.instruction", { APP_NAME })}
|
||||
{t("redirect.instruction", { app_name: APP_NAME })}
|
||||
</h2>
|
||||
<div className="center flex flex-col gap-4 sm:flex-row">
|
||||
<Button variant="text" className="h-14 px-10 text-base" onClick={() => navigate("/")}>
|
||||
|
|
@ -47,7 +50,7 @@ export function Component() {
|
|||
className="h-14 !rounded-full px-5 text-lg"
|
||||
onClick={async () => window.open(await getCallbackUrl(), "_top")}
|
||||
>
|
||||
{t("redirect.openApp", { APP_NAME })}
|
||||
{t("redirect.openApp", { app_name: APP_NAME })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grow" />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"
|
|||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { LoadingWithIcon } from "~/components/ui/loading"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { toastFetchError } from "~/lib/error-parser"
|
||||
|
|
@ -58,6 +59,18 @@ export function Component() {
|
|||
},
|
||||
})
|
||||
|
||||
if (actions.isPending) {
|
||||
return (
|
||||
<div className="center absolute inset-0 flex">
|
||||
<LoadingWithIcon
|
||||
icon={<i className={iconName} />}
|
||||
size="large"
|
||||
className="-translate-y-full"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle />
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { useSwitchHotKeyScope } from "~/hooks/common"
|
|||
import { nextFrame } from "~/lib/dom"
|
||||
import type { NativeMenuItem } from "~/lib/native-menu"
|
||||
import { CONTEXT_MENU_SHOW_EVENT_KEY } from "~/lib/native-menu"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
export const ContextMenuProvider: Component = ({ children }) => (
|
||||
<>
|
||||
|
|
@ -76,6 +77,7 @@ const Handler = () => {
|
|||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener
|
||||
document.addEventListener(CONTEXT_MENU_SHOW_EVENT_KEY, handler)
|
||||
return () => {
|
||||
document.removeEventListener(CONTEXT_MENU_SHOW_EVENT_KEY, handler)
|
||||
|
|
@ -108,6 +110,10 @@ const Item = memo(({ item }: { item: NativeMenuItem }) => {
|
|||
preventDefault: true,
|
||||
})
|
||||
|
||||
if (item.hide) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case "separator": {
|
||||
return <ContextMenuSeparator />
|
||||
|
|
@ -120,17 +126,20 @@ const Item = memo(({ item }: { item: NativeMenuItem }) => {
|
|||
: ContextMenuItem
|
||||
|
||||
const Sub = item.submenu ? ContextMenuSub : Fragment
|
||||
|
||||
return (
|
||||
<Sub>
|
||||
<Wrapper
|
||||
ref={itemRef}
|
||||
disabled={item.enabled === false || (item.click === undefined && !item.submenu)}
|
||||
disabled={item.click === undefined && !item.submenu}
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-2"
|
||||
checked={item.checked}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
{item.icon && (
|
||||
<span className="absolute left-2 flex items-center justify-center">{item.icon}</span>
|
||||
)}
|
||||
<span className={cn(item.icon && "pl-6")}>{item.label}</span>
|
||||
|
||||
{!!item.shortcut && (
|
||||
<div className="ml-auto pl-4">
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@
|
|||
"sidebar.feed_actions.copy_feed_url": "Copy feed URL",
|
||||
"sidebar.feed_actions.copy_list_id": "Copy list ID",
|
||||
"sidebar.feed_actions.copy_list_url": "Copy list URL",
|
||||
"sidebar.feed_actions.create_list": "Create New list",
|
||||
"sidebar.feed_actions.edit": "Edit",
|
||||
"sidebar.feed_actions.edit_feed": "Edit feed",
|
||||
"sidebar.feed_actions.edit_inbox": "Edit inbox",
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@
|
|||
"sidebar.feed_actions.copy_feed_url": "复制链接",
|
||||
"sidebar.feed_actions.copy_list_id": "复制列表 ID",
|
||||
"sidebar.feed_actions.copy_list_url": "复制列表链接",
|
||||
"sidebar.feed_actions.create_list": "创建列表",
|
||||
"sidebar.feed_actions.edit": "编辑",
|
||||
"sidebar.feed_actions.edit_feed": "编辑订阅",
|
||||
"sidebar.feed_actions.edit_inbox": "编辑收件箱",
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@
|
|||
"sidebar.feed_actions.copy_feed_url": "複製訂閱源 URL",
|
||||
"sidebar.feed_actions.copy_list_id": "複製清單 ID",
|
||||
"sidebar.feed_actions.copy_list_url": "複製清單 URL",
|
||||
"sidebar.feed_actions.create_list": "建立清單",
|
||||
"sidebar.feed_actions.edit": "編輯",
|
||||
"sidebar.feed_actions.edit_feed": "編輯訂閱源",
|
||||
"sidebar.feed_actions.edit_inbox": "編輯收件箱",
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@
|
|||
"sidebar.feed_actions.copy_feed_url": "複製摘要 URL",
|
||||
"sidebar.feed_actions.copy_list_id": "複製列表 ID",
|
||||
"sidebar.feed_actions.copy_list_url": "複製列表連結",
|
||||
"sidebar.feed_actions.create_list": "建立列表",
|
||||
"sidebar.feed_actions.edit": "編輯",
|
||||
"sidebar.feed_actions.edit_feed": "編輯摘要",
|
||||
"sidebar.feed_actions.edit_inbox": "編輯收件匣",
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@
|
|||
"invitation.tableHeaders.creationTime": "创建时间",
|
||||
"invitation.tableHeaders.usedBy": "使用者",
|
||||
"invitation.title": "邀请码",
|
||||
"lists.create": "创建新列表",
|
||||
"lists.create": "创建列表",
|
||||
"lists.created.error": "创建列表失败",
|
||||
"lists.created.success": "创建列表成功",
|
||||
"lists.description": "描述",
|
||||
|
|
|
|||
Loading…
Reference in New Issue