diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useRouteParams.ts b/apps/desktop/layer/renderer/src/hooks/biz/useRouteParams.ts index 77c356f8c..a6ab4b437 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useRouteParams.ts +++ b/apps/desktop/layer/renderer/src/hooks/biz/useRouteParams.ts @@ -1,5 +1,6 @@ import { getReadonlyRoute, useReadonlyRouteSelector } from "@follow/components/atoms/route.js" import { FeedViewType } from "@follow/constants" +import { isBizId } from "@follow/utils/utils" import type { Params } from "react-router" import { useParams } from "react-router" @@ -13,6 +14,7 @@ import { ROUTE_TIMELINE_OF_VIEW, } from "~/constants" import { getListById } from "~/store/list" +import { getSubscriptionByFeedId } from "~/store/subscription" export const useRouteEntryId = () => { const { entryId } = useParams() @@ -35,6 +37,7 @@ export interface BizRouteParams { inboxId?: string listId?: string timelineId?: string + isPreview?: boolean } const parseRouteParams = (params: Params): BizRouteParams => { @@ -43,6 +46,13 @@ const parseRouteParams = (params: Params): BizRouteParams => { : undefined const list = listId ? getListById(listId) : undefined + let isPreview = false + if (listId) { + isPreview = !getSubscriptionByFeedId(listId) + } else if (params.feedId) { + isPreview = isBizId(params.feedId) && !getSubscriptionByFeedId(params.feedId) + } + return { view: params.timelineId?.startsWith(ROUTE_TIMELINE_OF_VIEW) ? (Number.parseInt( @@ -64,6 +74,7 @@ const parseRouteParams = (params: Params): BizRouteParams => { : undefined, listId, timelineId: params.timelineId, + isPreview, } } diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx index 7ae3e236d..359e52c03 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx @@ -1,4 +1,4 @@ -import { ActionButton } from "@follow/components/ui/button/index.js" +import { ActionButton, Button } from "@follow/components/ui/button/index.js" import { DividerVertical } from "@follow/components/ui/divider/index.js" import { RotatingRefreshIcon } from "@follow/components/ui/loading/index.jsx" import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js" @@ -14,6 +14,7 @@ import { useTimelineColumnShow } from "~/atoms/sidebar" import { useWhoami } from "~/atoms/user" import { FEED_COLLECTION_LIST, ROUTE_ENTRY_PENDING } from "~/constants" import { shortcuts } from "~/constants/shortcuts" +import { useFollow } from "~/hooks/biz/useFollow" import { useRouteParams } from "~/hooks/biz/useRouteParams" import { EntryHeader } from "~/modules/entry-content/header" import { useRefreshFeedMutation } from "~/queries/feed" @@ -34,10 +35,11 @@ export const EntryListHeader: FC<{ }> = ({ refetch, isRefreshing, hasUpdate }) => { const routerParams = useRouteParams() const { t } = useTranslation() + const { t: tCommon } = useTranslation("common") const unreadOnly = useGeneralSettingKey("unreadOnly") - const { feedId, entryId, view } = routerParams + const { feedId, entryId, view, isPreview, listId } = routerParams const headerTitle = useFeedHeaderTitle() @@ -57,6 +59,8 @@ export const EntryListHeader: FC<{ const feed = useFeedById(feedId) + const follow = useFollow() + const titleStyleBasedView = ["pl-6", "pl-7", "pl-7", "pl-7", "px-5", "pl-6"] const feedColumnShow = useTimelineColumnShow() @@ -139,6 +143,21 @@ export const EntryListHeader: FC<{ + {isPreview && ( + + )} {/* */} diff --git a/apps/desktop/layer/renderer/src/modules/timeline-column/FeedItem.tsx b/apps/desktop/layer/renderer/src/modules/timeline-column/FeedItem.tsx index 1669f6faa..3e740ec4a 100644 --- a/apps/desktop/layer/renderer/src/modules/timeline-column/FeedItem.tsx +++ b/apps/desktop/layer/renderer/src/modules/timeline-column/FeedItem.tsx @@ -1,5 +1,6 @@ import { useMobile } from "@follow/components/hooks/useMobile.js" import { OouiUserAnonymous } from "@follow/components/icons/OouiUserAnonymous.jsx" +import { Button } from "@follow/components/ui/button/index.js" import { Tooltip, TooltipContent, @@ -17,6 +18,7 @@ import { useTranslation } from "react-i18next" import { MenuItemSeparator, MenuItemText, useShowContextMenu } from "~/atoms/context-menu" import { getMainContainerElement } from "~/atoms/dom" import { useFeedActions, useInboxActions, useListActions } from "~/hooks/biz/useFeedActions" +import { useFollow } from "~/hooks/biz/useFollow" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useContextMenu } from "~/hooks/common/useContextMenu" @@ -40,6 +42,7 @@ interface FeedItemProps { feedId: string view?: number className?: string + isPreview?: boolean } const DraggableItemWrapper: Component< @@ -61,9 +64,9 @@ const DraggableItemWrapper: Component< ) } -const FeedItemImpl = ({ view, feedId, className }: FeedItemProps) => { +const FeedItemImpl = ({ view, feedId, className, isPreview }: FeedItemProps) => { const { t } = useTranslation() - const subscription = useSubscriptionByFeedId(feedId)! + const subscription = useSubscriptionByFeedId(feedId) const navigate = useNavigateEntry() const feed = useFeedById(feedId, (feed) => { return { @@ -153,6 +156,8 @@ const FeedItemImpl = ({ view, feedId, className }: FeedItemProps) => { setIsContextMenuOpen(false) }, }) + const follow = useFollow() + if (!feed) return null const isFeed = feed.type === "feed" || !feed.type @@ -205,7 +210,7 @@ const FeedItemImpl = ({ view, feedId, className }: FeedItemProps) => { )} - {subscription.isPrivate && ( + {subscription?.isPrivate && ( @@ -216,6 +221,21 @@ const FeedItemImpl = ({ view, feedId, className }: FeedItemProps) => { )} + {isPreview && ( + + )} ) @@ -227,7 +247,8 @@ const ListItemImpl: Component<{ listId: string view: FeedViewType iconSize?: number -}> = ({ view, listId, className, iconSize = 22 }) => { + isPreview?: boolean +}> = ({ view, listId, className, iconSize = 22, isPreview }) => { const list = useListById(listId) const isActive = useRouteParamsSelector((routerParams) => routerParams.listId === listId) @@ -264,6 +285,8 @@ const ListItemImpl: Component<{ setIsContextMenuOpen(false) }, }) + const follow = useFollow() + if (!list) return null return (
- {subscription.isPrivate && ( + {subscription?.isPrivate && ( @@ -293,6 +316,20 @@ const ListItemImpl: Component<{ )}
+ {isPreview && ( + + )} ) diff --git a/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx b/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx index 7c5442492..58bdc90be 100644 --- a/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx @@ -6,8 +6,11 @@ import { useTranslation } from "react-i18next" import Selecto from "react-selecto" import { useEventListener } from "usehooks-ts" +import { useRouteParams } from "~/hooks/biz/useRouteParams" import { useAuthQuery } from "~/hooks/common" import { Queries } from "~/queries" +import { useFeedQuery } from "~/queries/feed" +import { useList } from "~/queries/lists" import { useCategoryOpenStateByView, useFeedsGroupedData, @@ -21,6 +24,7 @@ import { useSelectedFeedIdsState, } from "./atom" import { DraggableContext } from "./context" +import { FeedItem, ListItem } from "./FeedItem" import { EmptyFeedList, ListHeader, StarredItem } from "./FeedList.shared" import { useShouldFreeUpSpace } from "./hook" import { SortableFeedList, SortByAlphabeticalInbox, SortByAlphabeticalList } from "./sort-by" @@ -112,6 +116,14 @@ const FeedListImpl = forwardRef @@ -213,11 +225,14 @@ const FeedListImpl = forwardRef - {hasListData && ( + {(hasListData || (isListPreview && listId)) && ( <>
{t("words.lists")}
+ {isListPreview && listId && ( + + )} )} @@ -240,6 +255,9 @@ const FeedListImpl = forwardRef )} + {isFeedPreview && feedId && ( + + )}
{hasData ? ( diff --git a/apps/desktop/layer/renderer/src/store/subscription/hooks.ts b/apps/desktop/layer/renderer/src/store/subscription/hooks.ts index 4a3cda8db..067a1d98f 100644 --- a/apps/desktop/layer/renderer/src/store/subscription/hooks.ts +++ b/apps/desktop/layer/renderer/src/store/subscription/hooks.ts @@ -68,9 +68,12 @@ export const useCategoriesByView = (view: FeedViewType) => ), ) -export const useSubscriptionByFeedId = (feedId: FeedId) => +export const useSubscriptionByFeedId = (feedId?: FeedId) => useSubscriptionStore( - useCallback((state) => subscriptionByFeedIdSelector(feedId)(state) || null, [feedId]), + useCallback( + (state) => (feedId ? subscriptionByFeedIdSelector(feedId)(state) || null : null), + [feedId], + ), ) export const useSubscriptionsByFeedIds = (feedIds: FeedId[]) => useSubscriptionStore( diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index fc86e5d0e..b4ab761ca 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -115,6 +115,7 @@ export default ({ mode }) => { "/forget-password": proxyConfig, "/reset-password": proxyConfig, "/register": proxyConfig, + "/share": proxyConfig, ...(env.VITE_DEV_PROXY ? { diff --git a/packages/internal/components/src/ui/button/variants.tsx b/packages/internal/components/src/ui/button/variants.tsx index 08a904cb7..9846e98e3 100644 --- a/packages/internal/components/src/ui/button/variants.tsx +++ b/packages/internal/components/src/ui/button/variants.tsx @@ -36,6 +36,7 @@ export const styledButtonVariant = cva( ], variants: { size: { + sm: "px-2 py-1 rounded-md text-xs", default: "px-3 py-1.5 rounded-lg text-sm", lg: "px-4 py-2 rounded-lg text-base", }, diff --git a/packages/internal/shared/src/hono.ts b/packages/internal/shared/src/hono.ts index 24c6493f0..67d7811c2 100644 --- a/packages/internal/shared/src/hono.ts +++ b/packages/internal/shared/src/hono.ts @@ -2747,6 +2747,121 @@ declare const urlReadsOpenAPISchema: z.ZodObject<{ count: number; }>; +declare const feedAnalytics: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "feed_analytics"; + schema: undefined; + columns: { + feedId: drizzle_orm_pg_core.PgColumn<{ + name: "feed_id"; + tableName: "feed_analytics"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + updatesPerWeek: drizzle_orm_pg_core.PgColumn<{ + name: "updates_per_week"; + tableName: "feed_analytics"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + subscriptionCount: drizzle_orm_pg_core.PgColumn<{ + name: "subscription_count"; + tableName: "feed_analytics"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + latestEntryPublishedAt: drizzle_orm_pg_core.PgColumn<{ + name: "latest_entry_published_at"; + tableName: "feed_analytics"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + view: drizzle_orm_pg_core.PgColumn<{ + name: "view"; + tableName: "feed_analytics"; + dataType: "number"; + columnType: "PgSmallInt"; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + }; + dialect: "pg"; +}>; +declare const feedAnalyticsOpenAPISchema: zod.ZodObject<{ + feedId: zod.ZodString; + updatesPerWeek: zod.ZodNullable; + subscriptionCount: zod.ZodNullable; + latestEntryPublishedAt: zod.ZodNullable; + view: zod.ZodNullable; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + view: number | null; + feedId: string; + updatesPerWeek: number | null; + subscriptionCount: number | null; + latestEntryPublishedAt: string | null; +}, { + view: number | null; + feedId: string; + updatesPerWeek: number | null; + subscriptionCount: number | null; + latestEntryPublishedAt: string | null; +}>; +declare const feedAnalyticsRelations: drizzle_orm.Relations<"feed_analytics", { + feed: drizzle_orm.One<"feeds", true>; +}>; + declare const feeds: drizzle_orm_pg_core.PgTableWithColumns<{ name: "feeds"; schema: undefined; @@ -13711,6 +13826,8 @@ declare const _routes: hono_hono_base.HonoBase, "/">; type AppType = typeof _routes; -export { type ActionItem, type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type ExtraModel, type FeedModel, type ListModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, type UrlReadsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, captcha, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, readabilities, rsshub, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, trendingFeeds, trendingFeedsOpenAPISchema, trendingFeedsRelations, twoFactor, uploads, urlReads, urlReadsOpenAPISchema, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; +export { type ActionItem, type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type ExtraModel, type FeedModel, type ListModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, type UrlReadsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, captcha, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, extraZodSchema, feedAnalytics, feedAnalyticsOpenAPISchema, feedAnalyticsRelations, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, readabilities, rsshub, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, trendingFeeds, trendingFeedsOpenAPISchema, trendingFeedsRelations, twoFactor, uploads, urlReads, urlReadsOpenAPISchema, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations };