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:
CHANX 2024-10-07 18:04:14 +08:00 committed by GitHub
parent ce72b7a57f
commit d2b5cc36e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 304 additions and 222 deletions

View File

@ -28,6 +28,7 @@ const ContextMenuSubTrigger = React.forwardRef<
inset && "pl-8",
"center gap-2",
className,
props.disabled && "cursor-not-allowed opacity-30",
)}
{...props}
>

View File

@ -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,

View File

@ -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"),

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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)

View File

@ -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)

View File

@ -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,
)}

View File

@ -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()

View File

@ -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`,
),
}

View File

@ -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)}

View File

@ -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"),

View File

@ -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>
)
}

View File

@ -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" />

View File

@ -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" />

View File

@ -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 />

View File

@ -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">

View File

@ -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",

View File

@ -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": "编辑收件箱",

View File

@ -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": "編輯收件箱",

View File

@ -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": "編輯收件匣",

View File

@ -153,7 +153,7 @@
"invitation.tableHeaders.creationTime": "创建时间",
"invitation.tableHeaders.usedBy": "使用者",
"invitation.title": "邀请码",
"lists.create": "创建列表",
"lists.create": "创建列表",
"lists.created.error": "创建列表失败",
"lists.created.success": "创建列表成功",
"lists.description": "描述",