refactor: migrate to client sdk (#4480)

* init

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: migrate API calls to followClient for improved consistency

- Replaced instances of apiClient with followClient in various modules, including push notifications, analytics, achievements, and wallet management.
- Updated related functions to ensure compatibility with the new API structure.
- This refactor enhances code maintainability and aligns with the new client architecture.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update API calls to followClient for consistency

- Replaced instances of apiClient with followClient across various modules, including metadata, entries, feeds, lists, and inbox management.
- Adjusted related functions to align with the new API structure, enhancing maintainability and consistency throughout the codebase.

These changes improve the overall architecture by standardizing API interactions.

Signed-off-by: Innei <tukon479@gmail.com>

* update

* chore: update dependencies and improve profile settings

- Bumped versions of `@better-auth/expo`, `better-auth`, and `@better-auth/stripe` to 1.3.9 for enhanced functionality.
- Updated `@follow-app/client-sdk` reference in internal shared package.
- Added new fields (`bio`, `website`, `socialLinks`) to the profile settings form and adjusted related types for better user data handling.
- Minor adjustments in user store to accommodate new social links data structure.

These changes improve the profile management experience and ensure compatibility with updated dependencies.

Signed-off-by: Innei <tukon479@gmail.com>

* ssr done

* fix type error for action

* chore: update @follow-app/client-sdk to version 0.3.58 and refactor related code

- Updated the version of `@follow-app/client-sdk` to 0.3.58 in `pnpm-lock.yaml` and `pnpm-workspace.yaml`.
- Refactored various components and modules to utilize the updated SDK, including changes in type imports and API calls.
- Removed deprecated `api-fetch` module and replaced it with `followClient` for API interactions.
- Enhanced type safety and consistency across the application by adjusting type definitions and ensuring proper data handling.

These updates improve the overall code quality and maintainability while ensuring compatibility with the latest SDK features.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: remove hono

Signed-off-by: Innei <tukon479@gmail.com>

* fix action type for mobile

* fix(mobile): exclude all view type

* fix(mobile): exclude all for views

* fix(mobile): html render not work

* feat(mobile): auto show source content support

* fix(mobile): show source content action

* feat: more stable switching timeline back to top

* 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>

* refactor: streamline ChatInput key handling logic

- Simplified the Enter key handling in the ChatInput component by removing unnecessary conditions.
- Ensured that the handleSend function is called directly when the Enter key is pressed, improving code clarity.

These changes enhance the readability and maintainability of the ChatInput component.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: breadcrumb return back prev page logic

- Updated the logic in useNavigateEntry to ensure the final view is determined correctly based on the type of the view parameter.
- Refactored the EntryTitle component to simplify the rendering of recent readers, improving readability and maintainability.
- Adjusted the EntryHeaderBreadcrumb to pass the view parameter during navigation, enhancing functionality.

These changes aim to improve code clarity and ensure consistent behavior across navigation and entry display components.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: enhance navigation by integrating view parameter retrieval

- Updated EntryColumnShortcutHandler to utilize getRouteParams for retrieving the view parameter during navigation.
- Ensured that the view parameter is passed correctly when navigating to the next entry, improving the consistency of navigation behavior.

These changes aim to enhance the clarity and functionality of the entry column navigation logic.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: enhance modal styling and structure

- Updated modal component styles for improved visual consistency, including changes to border radius, padding, and text styles.
- Adjusted shadow properties in the Tailwind CSS configuration for better appearance in both light and dark themes.
- Refined layout and spacing within the modal to enhance user experience.

These changes aim to improve the overall aesthetics and usability of the modal component.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: enhance entry actions handling and toolbar integration

- Updated the HIDE_ACTIONS_IN_ENTRY_CONTEXT_MENU to explicitly define its type for better type safety.
- Introduced HIDE_ACTIONS_IN_ENTRY_TOOLBAR_ACTIONS to consolidate hidden actions for the toolbar.
- Refactored ActionBar to utilize useSortedEntryActions for improved action filtering and rendering.

These changes aim to streamline the entry actions logic and improve the maintainability of the ActionBar component.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: remove zen and wider mode

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance subscription column functionality

- Introduced new components for managing subscription entries, including EntrySubscriptionItem and EntrySubscriptionList, to improve the display and interaction within the subscription column.
- Added hooks for handling entry visibility and data fetching, optimizing performance and user experience.
- Implemented a toolbar for toggling the visibility of the entry list, enhancing user control over the interface.
- Refactored sidebar atoms to support new subscription-related states and actions.

These changes aim to provide a more robust and user-friendly experience in managing subscriptions within the application.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: integrate EntriesContext for improved subscription management

- Added EntriesProvider and context to manage subscription entries more effectively across components.
- Refactored EntrySubscriptionList and related components to utilize the new context for data fetching and state management.
- Introduced EntrySubscriptionSkeleton for better loading states in the subscription column.
- Removed the outdated useEntrySubscriptionData hook to streamline data handling.

These changes enhance the overall architecture and performance of the subscription column, providing a more cohesive user experience.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: fix lock

* fix: remove duplicated feed id, improve performance

* feat: back to feed first

* feat: replace MediaGallery with inline media rendering in SocialMediaLayout

* release(mobile): release v0.2.8

* chore(mobile): update build ID increment script and configuration

- Added a force flag to the build ID increment script to allow manual increments regardless of branch or staged changes.
- Updated iOS build version in Info.plist from 134 to 135.
- Removed unused changelog CLI configuration file.
- Modified bump.config.ts to disable tagging for mobile releases.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: adjust AI chat sidebar and error handling styles

- Added keyboard shortcut support for toggling AI chat in AISmartSidebar.
- Improved styling and transition effects in CollapsibleError for better user experience.
- Updated EntryReadHistory to conditionally render based on user visibility.
- Refactored SourceContentView to simplify rendering logic and improve performance.
- Enhanced Kbd component to support abbreviations for better accessibility.

These changes aim to improve the functionality and usability of the AI chat features and related components.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: implement comprehensive app layout architecture

- Introduced a new layout architecture for the Follow application, leveraging React Router v7's nested routing capabilities.
- Added multiple layout components including MainDestopLayout, TimelineEntryTwoColumnLayout, AIEnhancedTimelineLayout, and SubviewLayout to support various application modes.
- Enhanced the AI chat functionality with a dedicated AIChatLayout, allowing for dynamic panel styles based on user preferences.
- Implemented responsive design features and error handling mechanisms across layouts to improve user experience.
- Refactored existing components to utilize the new layout structure, ensuring a cohesive and maintainable codebase.

These changes establish a robust foundation for the application's UI, enhancing flexibility and user interaction.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update bump configuration for PR creation

- Changed the base branch for the GitHub PR creation command from 'main' to 'mobile-main' to align with the new branching strategy.
- Removed the changelog generation script from package.json to streamline the build process.

These updates ensure proper versioning and integration with the new mobile branch structure.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: implement entry context menu functionality

- Added a new hook, useEntryContextMenu, to manage context menu actions for entry items, including copy functionality.
- Integrated the context menu into EntrySubscriptionItem and EntryItemWrapper components, enhancing user interaction with entry items.
- Streamlined context menu handling by consolidating related logic into the new hook, improving code maintainability.

These changes provide a more intuitive context menu experience for users interacting with entries.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance media preview functionality with drag context

- Introduced a new context for managing drag state in the media preview component, allowing for improved user interaction during image dragging.
- Updated the image viewer to handle click events more effectively, considering drag state and timing to prevent unintended dismissals.
- Refactored the modal component to ensure the title is correctly rendered, enhancing accessibility.

These changes improve the overall user experience when interacting with media previews, making the interface more intuitive and responsive.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update client SDK version and dependencies

- Bumped the version of '@follow-app/client-sdk' from 0.3.59 to 0.3.60 in both pnpm-lock.yaml and pnpm-workspace.yaml.
- Updated dependencies for '@folo-services/drizzle' from 0.1.26 to 0.1.27, and '@folo-services/shared' from 0.0.24 to 0.0.25, ensuring compatibility with the latest changes.
- Adjusted Vite configuration to use 'join' instead of 'resolve' for path handling, enhancing code clarity and consistency.
- Removed an unused import in VideoPlayer component to streamline the code.

These updates improve dependency management and maintain code quality across the project.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: interceptor

* chore: update client SDK version to 0.3.64

- Bumped the version of '@follow-app/client-sdk' from 0.3.63 to 0.3.64 in pnpm-lock.yaml and pnpm-workspace.yaml.
- Adjusted component styles in the header for improved layout consistency.
- Refactored user ID handling in metadata fetching to enhance error handling and clarity.
- Updated API client fetch method to use the native fetch function for better compatibility.
- Added User-Agent header in SSR API headers for improved request handling.

These changes ensure the application is up-to-date with the latest SDK version and improve overall code quality and user experience.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update client SDK path in pnpm configuration

- Changed the '@follow-app/client-sdk' entry in pnpm-lock.yaml and pnpm-workspace.yaml from version 0.3.64 to a local path for development purposes.
- Refactored the fetchEntryContentByStream method in store.ts to simplify its parameters and improve API call handling.

These updates facilitate local development and enhance code clarity in the entry synchronization service.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: remove hono

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update better-auth and expo dependencies to version 1.3.11

- Updated '@better-auth/expo' and 'better-auth' to version 1.3.11 in mobile and shared packages.
- Updated '@better-auth/stripe' to version 1.3.11 in shared package.
- Adjusted peer dependencies for better-auth to reflect the new version.

This update ensures compatibility with the latest features and fixes in the better-auth library.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update auth module imports and clean up code

- Changed import path for AuthPlugins from '@follow-app/client-sdk' to '@follow-app/client-sdk/auth'.
- Removed commented-out socialLinks field from the baseAuthPlugins definition for clarity.
- Added a new line for better code readability.

These changes improve the organization and maintainability of the auth module.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: restore socialLinks field in baseAuthPlugins definition

- Reintroduced the socialLinks field in the baseAuthPlugins configuration, allowing for JSON type input.
- This change enhances the flexibility of the authentication module by enabling social link integration.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance API client and entry list hooks

- Added a request interceptor to the FollowClient to append a timestamp to the URL for cache-busting.
- Simplified the fetch call in the entry list hooks by removing unnecessary parameters, improving code clarity.
- Added a new line in the entry sync services to enhance readability.

These changes improve the functionality and maintainability of the API client and entry list hooks.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: add missing type error suppression for authClientContext

- Added a TypeScript error suppression comment for the authClientContext.provide call to prevent type-checking issues.
- This change ensures smoother integration of the authentication client in the application.

Signed-off-by: Innei <tukon479@gmail.com>

* fix lint

Signed-off-by: Innei <tukon479@gmail.com>

* lint

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Co-authored-by: DIYgod <i@diygod.me>
This commit is contained in:
Innei 2025-09-17 00:12:54 +08:00 committed by GitHub
parent 1e2835faed
commit d4b1333617
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
218 changed files with 1432 additions and 28350 deletions

View File

@ -46,7 +46,6 @@ updates:
patterns:
- immer
- re-resizable
- hono
- electron-context-menu
- "@mozilla/readability"
- daisyui

View File

@ -1,4 +1,3 @@
packages/internal/shared/src/hono.ts
pnpm-lock.yaml
CHANGELOG.md

View File

@ -58,7 +58,6 @@
"@types/node": "24.5.0",
"electron": "37.2.0",
"electron-devtools-installer": "4.0.0",
"hono": "4.9.7",
"typescript": "catalog:"
}
}

View File

@ -53,14 +53,14 @@ followClient.addResponseInterceptor(({ response }) => {
return response
})
followClient.addErrorInterceptor(async ({ error, response }) => {
followClient.addErrorInterceptor(async ({ response, error }) => {
if (!response) {
logger.error("API Request failed - no response", error)
return error
}
})
logger.error(`API Request failed: ${response.status} ${response.statusText}`, error)
followClient.addResponseInterceptor(async ({ response }) => {
// Handle specific error cases if needed in main process
if (response.status === 401) {
logger.warn("Authentication failed in main process")
@ -73,7 +73,7 @@ followClient.addErrorInterceptor(async ({ error, response }) => {
// ignore JSON parsing errors
}
return error
return response
})
// Legacy export for compatibility

View File

@ -2,7 +2,6 @@ import { PushReceiver } from "@eneris/push-receiver"
import { callWindowExpose } from "@follow/shared/bridge"
import { APP_PROTOCOL, DEV, LEGACY_APP_PROTOCOL } from "@follow/shared/constants"
import { env } from "@follow/shared/env.desktop"
import type { MessagingData } from "@follow/shared/hono"
import { app, nativeTheme, Notification, shell } from "electron"
import contextMenu from "electron-context-menu"
import path from "pathe"
@ -121,12 +120,15 @@ class AppManagerStatic {
logger.info(
`PushReceiver received notification: ${JSON.stringify(notification.message.data)}`,
)
const data = notification.message.data as MessagingData
const { data } = notification.message
if (!data) {
return
}
switch (data.type) {
case "new-entry": {
const notification = new Notification({
title: data.title,
body: data.description,
title: data.title as string,
body: data.description as string,
})
notification.on("click", () => {
const mainWindow = WindowManager.getMainWindowOrCreate()
@ -134,9 +136,9 @@ class AppManagerStatic {
mainWindow.focus()
const handlers = callWindowExpose(mainWindow)
handlers.navigateEntry({
feedId: data.feedId,
entryId: data.entryId,
view: Number.parseInt(data.view),
feedId: data.feedId as string,
entryId: data.entryId as string,
view: Number.parseInt(data.view as string),
})
})
notification.show()

View File

@ -1,8 +1,8 @@
import { createSettingAtom } from "@follow/atoms/helper/setting.js"
import type { SupportedLanguages } from "@follow/models"
import { defaultGeneralSettings } from "@follow/shared/settings/defaults"
import { hookEnhancedSettings as baseHookEnhancedSettings } from "@follow/shared/settings/hook"
import type { GeneralSettings as BaseGeneralSettings } from "@follow/shared/settings/interface"
import type { SupportedLanguages } from "@follow-app/client-sdk"
import { jotaiStore } from "~/lib/jotai"
import { getDefaultLanguage } from "~/lib/language"

View File

@ -1,9 +1,9 @@
import { Spring } from "@follow/components/constants/spring.js"
import { MotionButtonBase } from "@follow/components/ui/button/index.js"
import { IN_ELECTRON } from "@follow/shared/constants"
import type { MediaModel } from "@follow/shared/hono"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import type { EntryMedia } from "@follow-app/client-sdk"
import useEmblaCarousel from "embla-carousel-react"
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
import { useAnimationControls } from "motion/react"
@ -255,7 +255,7 @@ const HeaderActions: FC<{
)
}
export interface PreviewMediaProps extends MediaModel {
export interface PreviewMediaProps extends EntryMedia {
fallbackUrl?: string
}
export const PreviewMediaContent: FC<{
@ -299,7 +299,7 @@ export const PreviewMediaContent: FC<{
if (media.length === 0) return null
if (media.length === 1) {
const src = media[0]!.url
const src = media[0]!.url!
const { type } = media[0]!
const isVideo = type === "video"
return (

View File

@ -1,4 +1,4 @@
import type { MediaModel } from "@follow/shared/hono"
import type { MediaModel } from "@follow/database/schemas/types"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import useEmblaCarousel from "embla-carousel-react"

View File

@ -1,4 +1,3 @@
"~/components/common/Focusable.js"
import { Spring } from "@follow/components/constants/spring.js"
import { ActionButton, MotionButtonBase } from "@follow/components/ui/button/index.js"
import type { HTMLMediaState } from "@follow/hooks"

View File

@ -90,7 +90,7 @@ export const EntryToastPreview = ({ entryId }: { entryId: string }) => {
<FeedIcon
fallback
className="mask-squircle mask"
feed={feed}
target={feed}
entry={entry.iconEntry}
size={36}
/>

View File

@ -1,6 +1,6 @@
import { env } from "@follow/shared/env.desktop"
import type { AuthSession } from "@follow/shared/hono"
import { setFirebaseTracker, setPostHogTracker, tracker } from "@follow/tracker"
import type { AuthSessionResponse } from "@follow-app/client-sdk"
import posthog from "posthog-js"
import { QUERY_PERSIST_KEY } from "~/constants/app"
@ -25,7 +25,7 @@ export const initAnalytics = async () => {
}),
)
let session: AuthSession | undefined
let session: AuthSessionResponse | undefined
try {
const queryData = JSON.parse(window.localStorage.getItem(QUERY_PERSIST_KEY) ?? "{}")
session = queryData.clientState.queries.find(

View File

@ -1,7 +1,7 @@
import type { UserModel } from "@follow/models"
import { tracker } from "@follow/tracker"
import type { AuthUser } from "@follow-app/client-sdk"
export const setIntegrationIdentify = async (user: UserModel) => {
export const setIntegrationIdentify = async (user: AuthUser) => {
tracker.identify(user)
await import("@sentry/react").then(({ setTag }) => {
setTag("user_id", user.id)

View File

@ -45,8 +45,6 @@ followClient.addResponseInterceptor(({ response }) => {
})
followClient.addErrorInterceptor(async ({ error, response }) => {
const { router } = window
// If api is down
if ((!response || response.status === 0) && navigator.onLine) {
setApiStatus(NetworkStatus.OFFLINE)
@ -58,6 +56,11 @@ followClient.addErrorInterceptor(async ({ error, response }) => {
return error
}
return error
})
followClient.addResponseInterceptor(async ({ response }) => {
const { router } = window
if (response.status === 401) {
// Or we can present LoginModal here.
// router.navigate("/login")
@ -66,8 +69,12 @@ followClient.addErrorInterceptor(async ({ error, response }) => {
userActions.removeCurrentUser()
}
try {
const isJSON = response.headers.get("content-type")?.includes("application/json")
if (!isJSON) return response
const json = await response.clone().json()
const isError = response.status >= 400
if (!isError) return response
if (response.status === 400 && json.code === 1003) {
router.navigate("/invitation")
}
@ -94,5 +101,5 @@ followClient.addErrorInterceptor(async ({ error, response }) => {
// ignore
}
return error
return response
})

View File

@ -1,11 +1,9 @@
import { DEV } from "@follow/shared/constants"
import { env } from "@follow/shared/env.desktop"
import type { AppType } from "@follow/shared/hono"
import { userActions } from "@follow/store/user/store"
import { createDesktopAPIHeaders } from "@follow/utils/headers"
import PKG from "@pkg"
import { hc } from "hono/client"
import { FetchError, ofetch } from "ofetch"
import { ofetch } from "ofetch"
import { createElement } from "react"
import { toast } from "sonner"
@ -84,16 +82,6 @@ export const apiFetch = ofetch.create({
},
})
export const apiClient = hc<AppType>(env.VITE_API_URL, {
fetch: async (input, options = {}) =>
apiFetch(input.toString(), options).catch((err) => {
if (err instanceof FetchError && !err.response) {
setApiStatus(NetworkStatus.OFFLINE)
}
throw err
}),
})
if (DEV) {
DebugRegistry.add("Activation Toast", () => {
setTimeout(() => {

View File

@ -1,6 +1,7 @@
import { apiClient } from "~/lib/api-fetch"
import { getClientId, getSessionId } from "~/lib/client-session"
import { followClient } from "./api-client"
class Analytics4 {
private clientID: string
private sessionID: string
@ -46,8 +47,8 @@ class Analytics4 {
user_properties: this.userProperties,
}
return apiClient.data.g.$post({
json: payload,
return followClient.api.data.sendAnalytics({
...payload,
})
}
}

View File

@ -15,7 +15,7 @@ export const checkLanguage = ({
const pureContent = parseHtml(content)
.toText()
.replaceAll(/https?:\/\/\S+|www\.\S+/g, " ")
const { code } = ACTION_LANGUAGE_MAP[language]
const { code } = ACTION_LANGUAGE_MAP[language] ?? {}
if (!code) {
return false
}

View File

@ -1,7 +1,6 @@
import { FeedViewType } from "@follow/constants"
import { getServerConfigs } from "~/atoms/server-configs"
import type { RSSHubRoute } from "~/modules/discover/types"
import { FEED_COLLECTION_LIST, ROUTE_FEED_PENDING } from "../constants/app"
@ -48,27 +47,6 @@ export function getEntriesParams({
}
}
const rsshubCategoryMap: Partial<Record<string, FeedViewType>> = {
design: FeedViewType.Pictures,
forecast: FeedViewType.Notifications,
live: FeedViewType.Notifications,
picture: FeedViewType.Pictures,
"program-update": FeedViewType.Notifications,
"social-media": FeedViewType.SocialMedia,
}
export const getViewFromRoute = (route: RSSHubRoute) => {
if (route.view) {
return route.view
}
for (const categories of route.categories) {
if (rsshubCategoryMap[categories]) {
return rsshubCategoryMap[categories]
}
}
return null
}
export const getLevelMultiplier = (level: number) => {
if (level === 0) {
return 0.1

View File

@ -3,19 +3,13 @@ import "@follow/components/tailwind"
import "./styles/main.css"
import { IN_ELECTRON, WEB_BUILD } from "@follow/shared/constants"
import {
apiClientContext,
apiContext,
authClientContext,
queryClientContext,
} from "@follow/store/context"
import { apiContext, authClientContext, queryClientContext } from "@follow/store/context"
import { getOS } from "@follow/utils/utils"
import * as React from "react"
import { flushSync } from "react-dom"
import ReactDOM from "react-dom/client"
import { RouterProvider } from "react-router/dom"
import { apiClient } from "~/lib/api-fetch"
import { authClient } from "~/lib/auth"
import { setAppIsReady } from "./atoms/app"
@ -26,7 +20,6 @@ import { followApi } from "./lib/api-client"
import { queryClient } from "./lib/query-client"
import { router } from "./router"
apiClientContext.provide(apiClient)
authClientContext.provide(authClient)
queryClientContext.provide(queryClient)
apiContext.provide(followApi)

View File

@ -1,29 +1,26 @@
import { RiNftFill } from "@follow/components/icons/nft.jsx"
import { Button, MotionButtonBase } from "@follow/components/ui/button/index.js"
import { styledButtonVariant } from "@follow/components/ui/button/variants.js"
import { Input } from "@follow/components/ui/input/Input.js"
import { LoadingCircle, LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js"
import { useOnce } from "@follow/hooks"
import type { ExtractBizResponse } from "@follow/models/types"
import { Chain } from "@follow/utils/chain"
import { cn } from "@follow/utils/utils"
import type { AchievementWithPower } from "@follow-app/client-sdk"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import type { PrimitiveAtom } from "jotai"
import { atom, useStore } from "jotai"
import { nanoid } from "nanoid"
import type { FC, ReactNode } from "react"
import { useEffect, useId, useMemo, useRef, useState } from "react"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { useServerConfigs } from "~/atoms/server-configs"
import { LazyDotLottie } from "~/components/common/LazyDotLottie"
import { VideoPlayer } from "~/components/ui/media/VideoPlayer"
import { ScaleModal } from "~/components/ui/modal/stacked/custom-modal"
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useI18n } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import achievementAnimationUri from "~/lottie/achievement.lottie?url"
const absoluteachievementAnimationUri = new URL(achievementAnimationUri, import.meta.url).href
@ -100,7 +97,6 @@ const prefetchVideos = () => {
})
}
type Achievement = ExtractBizResponse<typeof apiClient.achievement.$get>["data"]
export const AchievementModalContent: FC = () => {
const jotaiStore = useStore()
@ -120,16 +116,14 @@ export const AchievementModalContent: FC = () => {
persist: true,
},
queryFn: async () => {
const res = await apiClient.achievement.$get({
query: {
type: "all",
},
const res = await followClient.api.achievement.list({
type: "all",
})
jotaiStore.set(achievementsDataAtom, res.data)
return res.data
},
initialData: defaultAchievements as Achievement,
initialData: defaultAchievements as AchievementWithPower[],
})
useEffect(() => {
@ -330,16 +324,14 @@ const buildDefaultAchievements = () => {
}
const MintButton: FC<{
achievementsDataAtom: PrimitiveAtom<Achievement | undefined>
achievement: Achievement[number]
achievementsDataAtom: PrimitiveAtom<AchievementWithPower[] | undefined>
achievement: AchievementWithPower
onMinted: () => void
}> = ({ achievementsDataAtom, achievement, onMinted }) => {
const { mutateAsync: mintAchievement, isPending: isMinting } = useMutation({
mutationFn: async (actionId: number) => {
return apiClient.achievement.$put({
json: {
actionId,
},
return followClient.api.achievement.claim({
actionId,
})
},
})
@ -383,15 +375,13 @@ const MintButton: FC<{
}
const IncompleteButton: FC<{
achievement: Achievement[number]
achievement: AchievementWithPower
refetch: () => void
}> = ({ achievement, refetch }) => {
const { mutateAsync: checkAchievement, isPending: checkPending } = useMutation({
mutationFn: async (actionId: number) => {
return apiClient.achievement.check.$post({
json: {
actionId,
},
return followClient.api.achievement.check({
actionId,
})
},
onSuccess: () => {
@ -399,34 +389,31 @@ const IncompleteButton: FC<{
},
})
const { present } = useModalStack()
let Content: ReactNode
const { PRODUCT_HUNT_VOTE_URL } = useServerConfigs() || {}
switch (achievement.actionId) {
case AchievementsActionIdMap.PRODUCT_HUNT_VOTE: {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={!PRODUCT_HUNT_VOTE_URL}
onClick={() => {
present({
title: "Validate Your Vote",
content: () => <VoteValidateModalContent refetch={refetch} />,
})
}}
>
Validate
</Button>
</TooltipTrigger>
{!PRODUCT_HUNT_VOTE_URL && (
<TooltipContent>Product Hunt Vote is not ready, We'll see.</TooltipContent>
)}
</Tooltip>
)
}
// case AchievementsActionIdMap.PRODUCT_HUNT_VOTE: {
// return (
// <Tooltip>
// <TooltipTrigger asChild>
// <Button
// disabled={!PRODUCT_HUNT_VOTE_URL}
// onClick={() => {
// present({
// title: "Validate Your Vote",
// content: () => <VoteValidateModalContent refetch={refetch} />,
// })
// }}
// >
// Validate
// </Button>
// </TooltipTrigger>
// {!PRODUCT_HUNT_VOTE_URL && (
// <TooltipContent>Product Hunt Vote is not ready, We'll see.</TooltipContent>
// )}
// </Tooltip>
// )
// }
default: {
Content = (
<>
@ -476,74 +463,3 @@ const IncompleteButton: FC<{
</button>
)
}
const VoteValidateModalContent: FC<{ refetch: () => void }> = ({ refetch }) => {
const ref = useRef<HTMLInputElement>(null)
const { dismiss } = useCurrentModal()
const { present } = useModalStack()
const { mutateAsync: audit, isPending } = useMutation({
mutationFn: (username: string) => {
return apiClient.achievement.audit.$post({
json: {
actionId: AchievementsActionIdMap.PRODUCT_HUNT_VOTE,
payload: {
username,
},
},
})
},
onSuccess: () => {
dismiss()
refetch()
present({
title: "Thank you!",
content: () => <div>Thank you for your vote. Please wait for our verification.</div>,
clickOutsideToDismiss: true,
})
},
})
const { PRODUCT_HUNT_VOTE_URL } = useServerConfigs() || {}
const id = useId()
const openOnceRef = useRef(false)
useEffect(() => {
if (openOnceRef.current) return
if (PRODUCT_HUNT_VOTE_URL) {
window.open(PRODUCT_HUNT_VOTE_URL, "_blank")
openOnceRef.current = true
}
}, [PRODUCT_HUNT_VOTE_URL])
return (
<form
className="flex flex-col gap-2"
onSubmit={(e) => {
e.preventDefault()
if (!ref.current?.value) return
audit(ref.current.value)
}}
>
<label className="text-sm" htmlFor={id}>
Please vote for {APP_NAME} on{" "}
<a
href={PRODUCT_HUNT_VOTE_URL}
className="follow-link--underline"
target="_blank"
rel="noreferrer"
>
Product Hunt!
</a>{" "}
Then fill in your username here.
</label>
<div>
<Input ref={ref} autoFocus id={id} placeholder="Your Product Hunt username" />
</div>
<div className="mt-2 flex justify-end">
<Button isLoading={isPending} type="submit">
Validate
</Button>
</div>
</form>
)
}

View File

@ -1,6 +1,8 @@
import { ResponsiveSelect } from "@follow/components/ui/select/responsive.js"
import { ACTION_LANGUAGE_MAP } from "@follow/shared/language"
import type { ActionAction } from "@follow/store/action/constant"
import { availableActionMap as availableActionMapOriginal } from "@follow/store/action/constant"
import type { ActionId } from "@follow-app/client-sdk"
import { useTranslation } from "react-i18next"
import { defaultResources } from "~/@types/default-resource"
@ -22,7 +24,7 @@ export const availableActionMap: typeof availableActionMapOriginal = {
...availableActionMapOriginal.translation,
prefixElement: <AiTargetLanguageSelector />,
},
}
} as Record<ActionId, ActionAction>
function AiTargetLanguageSelector() {
const { t } = useTranslation("settings")

View File

@ -1,10 +1,10 @@
import { ActionButton, Button } from "@follow/components/ui/button/index.js"
import { Divider } from "@follow/components/ui/divider/index.js"
import { Input } from "@follow/components/ui/input/index.js"
import type { ActionId } from "@follow/models/types"
import type { ActionAction } from "@follow/store/action/constant"
import { useActionRule } from "@follow/store/action/hooks"
import { actionActions } from "@follow/store/action/store"
import type { ActionId } from "@follow-app/client-sdk"
import { merge } from "es-toolkit/compat"
import { Fragment, useMemo } from "react"
import { useTranslation } from "react-i18next"

View File

@ -9,10 +9,10 @@ import {
SelectValue,
} from "@follow/components/ui/select/index.jsx"
import { ResponsiveSelect } from "@follow/components/ui/select/responsive.js"
import type { ActionFeedField, ActionOperation } from "@follow/models/types"
import { filterFieldOptions, filterOperatorOptions } from "@follow/store/action/constant"
import { useActionRule } from "@follow/store/action/hooks"
import { actionActions } from "@follow/store/action/store"
import type { ActionFeedField, ActionOperation } from "@follow-app/client-sdk"
import { Fragment } from "react"
import { useTranslation } from "react-i18next"

View File

@ -33,7 +33,12 @@ const SingleEntryCard = ({ entry }: { entry: SingleEntry }) => {
{/* Header */}
<div className="flex w-full items-start justify-between">
<div className="flex">
<FeedIcon disableFadeIn entry={null} feed={undefined} siteUrl={entry.url ?? undefined} />
<FeedIcon
disableFadeIn
entry={null}
target={undefined}
siteUrl={entry.url ?? undefined}
/>
<h3 className="text-text line-clamp-2 flex-1 font-semibold leading-tight">
{entry.title || "Untitled Entry"}
</h3>

View File

@ -31,10 +31,10 @@ const AIDisplayFeedPartBase = ({
<div className="flex">
<FeedIcon
disableFadeIn
feed={{
target={{
type: "feed",
title,
id: feedId,
image,
siteUrl,
}}

View File

@ -55,10 +55,9 @@ const Item = memo(
<div className="flex">
<FeedIcon
disableFadeIn
feed={{
target={{
type: "feed",
title,
id: feedId,
image,
siteUrl,
}}

View File

@ -1,6 +1,6 @@
import type { UIMessage } from "ai"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
export const generateChatTitle = async (messages: UIMessage[]) => {
if (messages.length < 2) return null
@ -23,8 +23,8 @@ export const generateChatTitle = async (messages: UIMessage[]) => {
}
})
const response = await apiClient.ai["summary-title"].$post({
json: { messages: relevantMessages },
const response = await followClient.api.ai.summaryTitle({
messages: relevantMessages,
})
if ("title" in response) {

View File

@ -39,7 +39,7 @@ export const PodcastButton = ({ feed }: { feed: FeedModel }) => {
content={
<>
<div className="mb-6 flex gap-4">
<FeedIcon feed={feed} entry={entry.iconEntry} size={58} fallback={false} noMargin />
<FeedIcon target={feed} entry={entry.iconEntry} size={58} fallback={false} noMargin />
<div className="flex flex-col justify-center">
<Marquee
play={playerValue.status === "playing"}
@ -92,7 +92,7 @@ export const PodcastButton = ({ feed }: { feed: FeedModel }) => {
}
>
<div className="flex size-5 items-center justify-center">
<FeedIcon feed={feed} size={22} noMargin />
<FeedIcon target={feed} size={22} noMargin />
</div>
</PresentSheet>
)

View File

@ -18,7 +18,7 @@ import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
const formSchema = z.object({
referral: z.string().optional(),
@ -37,7 +37,7 @@ function getDefaultReferralCode() {
}
async function getReferralCycleDays(code: string) {
return apiClient.referrals.days.$get({ query: { code } })
return followClient.api.referrals.getDays({ code })
}
export function ReferralForm({

View File

@ -4,7 +4,8 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.js"
import type { FeedOrListRespModel } from "@follow/models/types"
import type { FeedModel } from "@follow/store/feed/types"
import type { ListModel } from "@follow/store/list/types"
import { cn } from "@follow/utils/utils"
import { useTranslation } from "react-i18next"
@ -14,7 +15,7 @@ export const BoostCertification = ({
feed,
className,
}: {
feed: FeedOrListRespModel
feed: FeedModel | ListModel
className?: string
}) => {
const showBoostModal = useBoostModal()

View File

@ -54,7 +54,7 @@ export const BoostModalContent = ({ feedId }: { feedId: string }) => {
return (
<div className="flex w-full flex-col gap-3 lg:max-h-[700px] lg:w-[80vw] lg:max-w-[450px]">
<div className="center flex flex-col gap-2">
<FeedIcon noMargin feed={feed} size={50} />
<FeedIcon noMargin target={feed} size={50} />
<h1 className="center mt-2 flex flex-wrap text-lg font-bold">
<div className="center flex shrink-0">

View File

@ -1,9 +1,10 @@
import { tracker } from "@follow/tracker"
import type { BoostFeedRequest } from "@follow-app/client-sdk"
import { useMutation } from "@tanstack/react-query"
import { toast } from "sonner"
import { useAuthQuery, useI18n } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import { toastFetchError } from "~/lib/error-parser"
@ -12,19 +13,15 @@ import { updateFeedBoostStatus } from "./atom"
const query = {
getStatus: ({ feedId }: { feedId: string }) =>
defineQuery(["boostFeed", feedId], async () => {
const res = await apiClient.boosts.$get({
query: {
feedId,
},
const res = await followClient.api.boosts.getFeedBoostLevel({
feedId,
})
return res.data
}),
getBoosters: ({ feedId }: { feedId: string }) =>
defineQuery(["boosters", feedId], async () => {
const res = await apiClient.boosts.boosters.$get({
query: {
feedId,
},
const res = await followClient.api.boosts.getFeedBoosters({
feedId,
})
return res.data
@ -45,8 +42,12 @@ export const useFeedBoostersQuery = (feedId: string | null | undefined) =>
export const useBoostFeedMutation = () => {
const t = useI18n()
return useMutation({
mutationFn: (data: Parameters<typeof apiClient.boosts.$post>[0]["json"]) =>
apiClient.boosts.$post({ json: data }),
mutationFn: (data: BoostFeedRequest) =>
followClient.api.boosts.boostFeed({
amount: data.amount,
feedId: data.feedId,
TOTPCode: data.TOTPCode,
}),
onError(err) {
toastFetchError(err)
},

View File

@ -4,6 +4,7 @@ import { RelativeTime } from "@follow/components/ui/datetime/index.js"
import { useIsSubscribed } from "@follow/store/subscription/hooks"
import { getBackgroundGradient } from "@follow/utils/color"
import { cn, formatNumber } from "@follow/utils/utils"
import type { DiscoveryItem, TrendingFeedItem } from "@follow-app/client-sdk"
import type { FC } from "react"
import { memo } from "react"
import { useTranslation } from "react-i18next"
@ -13,82 +14,77 @@ import { Media } from "~/components/ui/media/Media"
import { useFollow } from "~/hooks/biz/useFollow"
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useFeedSafeUrl } from "~/hooks/common/useFeedSafeUrl"
import type { apiClient } from "~/lib/api-fetch"
import { FollowSummary } from "../feed/feed-summary"
export type DiscoverItem = Awaited<ReturnType<typeof apiClient.discover.$post>>["data"][number]
export const FeedCardActions: FC<{
item: DiscoverItem
onSuccess?: (item: DiscoverItem) => void
export function FeedCardActions<T extends TrendingFeedItem | DiscoveryItem>({
item,
onSuccess,
isSubscribed,
followButtonVariant,
followedButtonVariant = "ghost",
followButtonClassName,
followedButtonClassName,
}: {
item: T
onSuccess?: (item: T) => void
isSubscribed: boolean
followButtonVariant?: "ghost" | "outline"
followedButtonVariant?: "ghost" | "outline"
followButtonClassName?: string
followedButtonClassName?: string
}> = memo(
({
item,
onSuccess,
isSubscribed,
followButtonVariant,
followedButtonVariant = "ghost",
followButtonClassName,
followedButtonClassName,
}) => {
const follow = useFollow()
const { t } = useTranslation()
const location = useLocation()
}) {
const follow = useFollow()
const { t } = useTranslation()
const location = useLocation()
return (
<div className="flex items-center justify-between gap-2">
{!isSubscribed && (
<Button
variant="ghost"
disabled={!item.feed?.id}
buttonClassName="rounded-lg px-3 font-medium text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800/80 dark:hover:text-white"
onClick={() => {
if (!item.feed?.id) return
navigateEntry({
feedId: item.feed.id,
view: item.analytics?.view ?? 0,
backPath: location.pathname,
})
}}
>
{t("discover.preview")}
</Button>
)}
return (
<div className="flex items-center justify-between gap-2">
{!isSubscribed && (
<Button
variant={isSubscribed ? followedButtonVariant : followButtonVariant}
variant="ghost"
disabled={!item.feed?.id}
buttonClassName="rounded-lg px-3 font-medium text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800/80 dark:hover:text-white"
onClick={() => {
follow({
isList: !!item.list?.id,
id: item.list?.id || item.feed?.id,
url: item.feed?.url,
onSuccess() {
onSuccess?.(item)
},
if (!item.feed?.id) return
navigateEntry({
feedId: item.feed.id,
view: item.analytics?.view ?? 0,
backPath: location.pathname,
})
}}
buttonClassName={cn(
"relative overflow-hidden rounded-lg font-medium transition-all duration-300",
isSubscribed ? "border-zinc-200/80 px-3 text-zinc-400 dark:border-zinc-700/80" : "",
isSubscribed ? followedButtonClassName : followButtonClassName,
)}
>
{isSubscribed ? t("feed.actions.followed") : t("feed.actions.follow")}
{t("discover.preview")}
</Button>
</div>
)
},
)
)}
<Button
variant={isSubscribed ? followedButtonVariant : followButtonVariant}
onClick={() => {
follow({
isList: "list" in item && !!item.list?.id,
id: ("list" in item && item.list?.id) || item.feed?.id,
url: item.feed?.url,
onSuccess() {
onSuccess?.(item)
},
})
}}
buttonClassName={cn(
"relative overflow-hidden rounded-lg font-medium transition-all duration-300",
isSubscribed ? "border-zinc-200/80 px-3 text-zinc-400 dark:border-zinc-700/80" : "",
isSubscribed ? followedButtonClassName : followButtonClassName,
)}
>
{isSubscribed ? t("feed.actions.followed") : t("feed.actions.follow")}
</Button>
</div>
)
}
interface DiscoverFeedCardProps {
item: DiscoverItem
onSuccess?: (item: DiscoverItem) => void
onUnSubscribed?: (item: DiscoverItem) => void
item: DiscoveryItem
onSuccess?: (item: DiscoveryItem) => void
onUnSubscribed?: (item: DiscoveryItem) => void
className?: string
}
@ -108,7 +104,7 @@ export const DiscoverFeedCard: FC<DiscoverFeedCardProps> = memo(
)}
>
<CardHeader className="p-4 pb-2">
<FollowSummary feed={item.feed || item.list!} docs={item.docs} />
<FollowSummary feed={(item.feed || item.list!) as any} docs={item.docs} />
</CardHeader>
<CardContent className="px-4">
{item.docs ? (
@ -171,7 +167,7 @@ export const DiscoverFeedCard: FC<DiscoverFeedCardProps> = memo(
)
const SearchResultContent: FC<{
entry: NonUndefined<DiscoverItem["entries"]>[number]
entry: NonUndefined<DiscoveryItem["entries"]>[number]
}> = memo(({ entry }) => {
const safeUrl = useFeedSafeUrl(entry.id)
return (
@ -205,7 +201,7 @@ const SearchResultContent: FC<{
})
const FeedCardMediaThumbnail: FC<{
entry: NonUndefined<DiscoverItem["entries"]>[number]
entry: NonUndefined<DiscoveryItem["entries"]>[number]
}> = ({ entry }) => {
const [, , , bgAccent, bgAccentLight] = getBackgroundGradient(
entry.title || entry.url || "Untitled",

View File

@ -19,6 +19,7 @@ import {
regexpPathToPath,
} from "@follow/utils/path-parser"
import { cn } from "@follow/utils/utils"
import type { RSSHubRouteMetadata } from "@follow-app/client-sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { m } from "motion/react"
import type { FC } from "react"
@ -39,7 +40,6 @@ import {
} from "~/components/ui/modal/stacked/hooks"
import { FeedForm } from "./FeedForm"
import type { RSSHubRoute } from "./types"
import { normalizeRSSHubParameters } from "./utils"
const FeedMaintainers = ({ maintainers }: { maintainers?: string[] }) => {
@ -92,7 +92,7 @@ export const DiscoverFeedForm = ({
viewportClassName,
rootClassName,
}: {
route: RSSHubRoute
route: RSSHubRouteMetadata
routePrefix: string
noDescription?: boolean
routeParams?: RouteParams

View File

@ -11,6 +11,7 @@ import {
import { Input } from "@follow/components/ui/input/index.js"
import { SegmentGroup, SegmentItem } from "@follow/components/ui/segment/index.js"
import { ResponsiveSelect } from "@follow/components/ui/select/responsive.js"
import type { DiscoveryItem } from "@follow-app/client-sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { repository } from "@pkg"
import { useMutation } from "@tanstack/react-query"
@ -25,7 +26,7 @@ import { z } from "zod"
import { useIsInMASReview } from "~/atoms/server-configs"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { DiscoverFeedCard } from "./DiscoverFeedCard"
import { FeedForm } from "./FeedForm"
@ -90,9 +91,7 @@ const info = {
}
>
type DiscoverSearchData = Awaited<ReturnType<typeof apiClient.discover.$post>>["data"]
const discoverSearchDataAtom = atom<Record<string, DiscoverSearchData>>()
const discoverSearchDataAtom = atom<Record<string, DiscoveryItem[]>>()
export function DiscoverForm({ type = "search" }: { type?: string }) {
const { prefix, default: defaultValue, schema: formSchema } = info[type]!
@ -125,11 +124,9 @@ export function DiscoverForm({ type = "search" }: { type?: string }) {
const jotaiStore = useStore()
const mutation = useMutation({
mutationFn: async ({ keyword, target }: { keyword: string; target: "feeds" | "lists" }) => {
let { data } = await apiClient.discover.$post({
json: {
keyword: keyword.trim(),
target,
},
let { data } = await followClient.api.discover.discover({
keyword: keyword.trim(),
target,
})
if (isInMASReview) {
data = data.filter((item) => !item.list?.fee)
@ -205,7 +202,7 @@ export function DiscoverForm({ type = "search" }: { type?: string }) {
)
const handleSuccess = useCallback(
(item: DiscoverSearchData[number]) => {
(item: DiscoveryItem) => {
const currentData = jotaiStore.get(discoverSearchDataAtom)
if (!currentData) return
jotaiStore.set(
@ -229,7 +226,7 @@ export function DiscoverForm({ type = "search" }: { type?: string }) {
)
const handleUnSubscribed = useCallback(
(item: DiscoverSearchData[number]) => {
(item: DiscoveryItem) => {
const currentData = jotaiStore.get(discoverSearchDataAtom)
if (!currentData) return
jotaiStore.set(

View File

@ -2,7 +2,6 @@ import { Button } from "@follow/components/ui/button/index.js"
import { CollapseCss, CollapseCssGroup } from "@follow/components/ui/collapse/index.js"
import { DropZone } from "@follow/components/ui/drop-zone/index.js"
import { Form, FormControl, FormField, FormItem } from "@follow/components/ui/form/index.jsx"
import type { BizRespose } from "@follow/models"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
import { Fragment } from "react"
@ -12,20 +11,13 @@ import { z } from "zod"
import { Media } from "~/components/ui/media/Media"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { apiFetch } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { toastFetchError } from "~/lib/error-parser"
import { OpmlSelectionModal } from "./OpmlSelectionModal"
import type { ParsedOpmlData } from "./types"
const parseOpmlFile = async (file: File): Promise<ParsedOpmlData> => {
const formData = new FormData()
formData.append("file", file)
const data = await apiFetch<BizRespose<ParsedOpmlData>>("/subscriptions/parse-opml", {
method: "POST",
body: formData,
})
const parseOpmlFile = async (file: File) => {
const data = await followClient.api.subscriptions.parseOpml(await file.arrayBuffer())
return data.data
}

View File

@ -14,13 +14,14 @@ import { RootPortal } from "@follow/components/ui/portal/index.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { Switch } from "@follow/components/ui/switch/index.jsx"
import { FeedViewType } from "@follow/constants"
import type { EntryModelSimple, FeedAnalyticsModel, FeedModel } from "@follow/models/types"
import { useFeedByIdOrUrl } from "@follow/store/feed/hooks"
import type { FeedModel } from "@follow/store/feed/types"
import { useCategories, useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
import { subscriptionSyncService } from "@follow/store/subscription/store"
import { whoami } from "@follow/store/user/getters"
import { tracker } from "@follow/tracker"
import { cn } from "@follow/utils/utils"
import type { FeedAnalyticsModel, ParsedEntry } from "@follow-app/client-sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
import { useCallback, useEffect, useMemo, useRef } from "react"
@ -150,6 +151,7 @@ export const FeedForm: Component<{
}, [
defaultValues,
feed,
feedQuery.data?.analytics,
feedQuery.data?.entries,
feedQuery.data?.subscription,
feedQuery.error,
@ -190,7 +192,7 @@ const FeedInnerForm = ({
hideFromTimeline?: boolean | null
}
feed: FeedModel
entries?: EntryModelSimple[]
entries?: ParsedEntry[]
analytics?: FeedAnalyticsModel
placeholderRef: React.RefObject<HTMLDivElement | null>

View File

@ -1,6 +1,8 @@
import { Skeleton } from "@follow/components/ui/skeleton/index.js"
import type { FeedAnalyticsModel, FeedOrListRespModel, ListAnalyticsModel } from "@follow/models"
import type { FeedModel } from "@follow/store/feed/types"
import type { ListModel } from "@follow/store/list/types"
import { formatNumber } from "@follow/utils"
import type { FeedAnalyticsModel, ListAnalyticsSchema } from "@follow-app/client-sdk"
import type { FC } from "react"
import { useTranslation } from "react-i18next"
@ -9,9 +11,9 @@ import { RelativeTime } from "~/components/ui/datetime"
import { FollowSummary } from "../feed/feed-summary"
export interface FeedSummaryProps {
feed: FeedOrListRespModel
feed: FeedModel | ListModel
analytics?: FeedAnalyticsModel | ListAnalyticsModel
analytics?: FeedAnalyticsModel | ListAnalyticsSchema
showAnalytics?: boolean
isLoading?: boolean

View File

@ -12,7 +12,6 @@ import { Input } from "@follow/components/ui/input/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { Switch } from "@follow/components/ui/switch/index.jsx"
import { FeedViewType } from "@follow/constants"
import type { ListAnalyticsModel } from "@follow/models/types"
import { useListById, usePrefetchListById } from "@follow/store/list/hooks"
import type { ListModel } from "@follow/store/list/types"
import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
@ -20,6 +19,7 @@ import { subscriptionSyncService } from "@follow/store/subscription/store"
import { whoami } from "@follow/store/user/getters"
import { tracker } from "@follow/tracker"
import { cn } from "@follow/utils/utils"
import type { ListAnalyticsSchema } from "@follow-app/client-sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
import { useEffect, useRef } from "react"
@ -176,7 +176,7 @@ const ListInnerForm = ({
hideFromTimeline?: boolean | null
}
list: ListModel
analytics?: ListAnalyticsModel
analytics?: ListAnalyticsSchema
isLoading: boolean
}) => {
const subscription = useSubscriptionByFeedId(id || "") || subscriptionData
@ -261,8 +261,6 @@ const ListInnerForm = ({
feed={{
...list,
fee: list.fee || 0,
createdAt: null,
updatedAt: null,
}}
analytics={analytics}
showAnalytics

View File

@ -3,9 +3,9 @@ import { Checkbox } from "@follow/components/ui/checkbox/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.jsx"
import type { BizRespose } from "@follow/models"
import { subscriptionSyncService } from "@follow/store/subscription/store"
import { cn } from "@follow/utils/utils"
import type { ExtractResponseData, SubscriptionParseOpmlResponse } from "@follow-app/client-sdk"
import { useMutation } from "@tanstack/react-query"
import Fuse from "fuse.js"
import { useCallback, useMemo, useState } from "react"
@ -13,22 +13,17 @@ import { Trans, useTranslation } from "react-i18next"
import { toast } from "sonner"
import { useCurrentModal } from "~/components/ui/modal/stacked/hooks"
import { apiFetch } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { toastFetchError } from "~/lib/error-parser"
import type { ParsedFeedItem, ParsedOpmlData } from "./types"
import type { ParsedFeedItem } from "./types"
type FeedResponseList = {
id: string
url: string
title: string | null
}[]
export const OpmlSelectionModal = ({
parsedData,
file,
}: {
parsedData: ParsedOpmlData
parsedData: ExtractResponseData<SubscriptionParseOpmlResponse>
file: File
}) => {
@ -41,16 +36,7 @@ export const OpmlSelectionModal = ({
formData.append("file", file)
formData.append("items", JSON.stringify(selectedItems.map((i) => i.url)))
const { data } = await apiFetch<
BizRespose<{
successfulItems: FeedResponseList
conflictItems: FeedResponseList
parsedErrorItems: FeedResponseList
}>
>("/subscriptions/import", {
method: "POST",
body: formData,
})
const { data } = await followClient.api.subscriptions.import(formData)
return data
},

View File

@ -1,185 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/card/index.jsx"
import { SiteIcon } from "@follow/components/ui/icon/SiteIcon.js"
import { RSSHubCategories } from "@follow/constants"
import { getHighestWeightColor } from "@follow/utils/color"
import { clsx, cn, getUrlIcon } from "@follow/utils/utils"
import { upperFirst } from "es-toolkit/compat"
import type { FC } from "react"
import { memo, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { RecommendationContent } from "./RecommendationContent"
import type { RSSHubRouteDeclaration } from "./types"
interface RecommendationCardProps {
data: RSSHubRouteDeclaration
routePrefix: string
setCategory: (category: string) => void
}
export const RecommendationCard: FC<RecommendationCardProps> = memo(
({ data, routePrefix, setCategory }) => {
const { t } = useTranslation()
const { present } = useModalStack()
const { maintainers, categories } = useMemo(() => {
const maintainers = new Set<string>()
const categories = new Set<string>()
for (const route in data.routes) {
const routeData = data.routes[route]!
if (routeData.maintainers) {
routeData.maintainers.forEach((m) => maintainers.add(m))
}
if (routeData.categories) {
routeData.categories.forEach((c) => categories.add(c))
}
}
categories.delete("popular")
return {
maintainers: Array.from(maintainers),
categories: Array.from(categories) as unknown as typeof RSSHubCategories,
}
}, [data])
return (
<Card className="shadow-background border-border flex flex-col overflow-hidden rounded-lg border">
<CardHeader className="@container relative p-5 pb-3">
<div className="@[280px]:h-[80px] absolute left-0 top-0 h-[50px] w-full overflow-hidden dark:brightness-75">
<span className="pointer-events-none opacity-50">
<BackgroundGradient className="size-full" url={data.url} />
</span>
</div>
<CardTitle className="@[280px]:pt-10 relative z-[1] flex items-center pt-[10px] text-base">
<span className="center bg-background box-content flex aspect-square rounded-full p-1.5">
<span className="overflow-hidden rounded-full">
<SiteIcon size={28} siteUrl={`https://${data.url}`} />
</span>
</span>
<a
href={`https://${data.url}`}
target="_blank"
rel="noreferrer"
className="text-title2 ml-2 line-clamp-1 translate-y-1.5"
>
{data.name}
</a>
</CardTitle>
</CardHeader>
<CardContent className="flex grow flex-col p-5 pt-0">
<ul className="text-text grow gap-2 space-y-1 text-sm">
{Object.keys(data.routes).map((route) => {
const routeData = data.routes[route]!
// some routes have multiple paths, like `huxiu`
if (Array.isArray(routeData.path)) {
routeData.path = routeData.path.find((p) => p === route) ?? routeData.path[0]
}
return (
<li
key={route}
className="hover:text-accent group ml-1"
onClick={(e) => {
;(e.target as HTMLElement).querySelector("button")?.click()
}}
tabIndex={-1}
>
<button
type="button"
className="before:bg-accent text-headline group-hover:bg-material-medium relative ml-3 rounded p-0.5 px-1 text-left duration-200 before:absolute before:inset-y-0 before:-left-3 before:mb-auto before:mt-2 before:size-1.5 before:rounded-full before:content-['']"
onClick={() => {
present({
id: `recommendation-content-${route}`,
content: () => (
<RecommendationContent
routePrefix={routePrefix}
route={data.routes[route]!}
/>
),
icon: (
<FeedIcon className="size-4" size={16} siteUrl={`https://${data.url}`} />
),
title: `${data.name} - ${data.routes[route]!.name}`,
})
}}
>
{data.routes[route]!.name}
</button>
</li>
)
})}
</ul>
<div className="text-text-secondary mt-4 flex flex-col gap-2">
<div className="flex flex-1 items-center text-sm">
<i className="i-mingcute-hammer-line mr-1 shrink-0 translate-y-0.5 self-start" />
<span className="text-body flex flex-wrap gap-1">
{maintainers.map((m) => (
<a
key={m}
href={`https://github.com/${m}`}
className="follow-link--underline"
target="_blank"
rel="noreferrer"
>
@{m}
</a>
))}
</span>
</div>
<div className="flex flex-1 items-center text-sm">
<i className="i-mingcute-tag-2-line mr-1 shrink-0 translate-y-0.5 self-start" />
<span className="text-body flex flex-wrap gap-1">
{categories.map((c) => (
<button
onClick={() => {
if (!RSSHubCategories.includes(c)) return
setCategory(c)
}}
key={c}
type="button"
className={clsx(
"bg-material-medium hover:bg-material-medium cursor-pointer rounded px-1.5 duration-200",
!RSSHubCategories.includes(c) && "pointer-events-none opacity-50",
)}
>
{RSSHubCategories.includes(c)
? t(`discover.category.${c}`, { ns: "common" })
: upperFirst(c)}
</button>
))}
</span>
</div>
</div>
</CardContent>
</Card>
)
},
)
const BackgroundGradient = memo(({ url, className }: { url: string; className?: string }) => {
const [color, setColor] = useState<string>()
useEffect(() => {
const { src } = getUrlIcon(`https://${url}`)
const image = new Image()
image.src = src
image.crossOrigin = "anonymous"
image.onload = () => {
try {
const color = getHighestWeightColor(image)
setColor(color)
} catch {
setColor("#333")
}
}
}, [url])
return (
<div
className={cn("pointer-events-none size-[500px]", className)}
style={{ backgroundColor: color }}
/>
)
})

View File

@ -1,11 +1,12 @@
import type { RSSHubRouteMetadata } from "@follow-app/client-sdk"
import { DiscoverFeedForm } from "./DiscoverFeedForm"
import type { RSSHubRoute } from "./types"
export const RecommendationContent = ({
route,
routePrefix,
}: {
route: RSSHubRoute
route: RSSHubRouteMetadata
routePrefix: string
}) => (
<div className="mx-auto w-full max-w-[700px] sm:min-w-[550px]">

View File

@ -1,21 +1,21 @@
import { useIsSubscribed } from "@follow/store/subscription/hooks"
import { formatNumber } from "@follow/utils"
import type { TrendingFeedItem } from "@follow-app/client-sdk"
import type { FC } from "react"
import { useTranslation } from "react-i18next"
import { FollowSummary } from "../feed/feed-summary"
import type { DiscoverItem } from "./DiscoverFeedCard"
import { FeedCardActions } from "./DiscoverFeedCard"
export const TrendingFeedCard: FC<{
item: DiscoverItem
item: TrendingFeedItem
}> = ({ item }) => {
const { t } = useTranslation("common")
const { analytics } = item
const isSubscribed = useIsSubscribed(item.feed?.id || item.list?.id || "")
const isSubscribed = useIsSubscribed(item.feed?.id || "")
return (
<div>
<FollowSummary simple feed={item.feed!} />
<FollowSummary simple feed={item.feed! as any} />
<div className="text-body text-text-secondary mt-2 flex items-center justify-between">
{analytics?.subscriptionCount ? (

View File

@ -1,19 +1,5 @@
import type { FeedViewType } from "@follow/constants"
export * from "@follow/models/rsshub"
export type ParsedFeedItem = {
url: string
title: string | null
category?: string | null
}
export type ParsedOpmlData = {
remaining: number
subscriptions: {
category: string | null
title: string
url: string
view: FeedViewType
}[]
}

View File

@ -1,4 +1,4 @@
import type { RSSHubParameter, RSSHubParameterObject } from "./types"
import type { RSSHubParameter, RSSHubParameterObject } from "@follow/models/rsshub"
export const normalizeRSSHubParameters = (
parameters: RSSHubParameter,

View File

@ -86,7 +86,7 @@ const EntrySubscriptionItemImpl = ({ entryId, view, className }: EntrySubscripti
{...contextMenuProps}
>
<div className="flex min-w-0 flex-1 items-start gap-2">
<FeedIcon fallback feed={feed} size={14} className="mt-0.5 shrink-0" />
<FeedIcon fallback target={feed} size={14} className="mt-0.5 shrink-0" />
<div className="min-w-0 flex-1">
<EllipsisHorizontalTextWithTooltip

View File

@ -73,7 +73,7 @@ const entrySelector = (state: EntryModel) => {
video,
}
}
export function AllItem({ entryId, entryPreview, translation }: UniversalItemProps) {
export function AllItem({ entryId, translation }: UniversalItemProps) {
const entry = useEntry(entryId, entrySelector)
const simple = true
@ -84,18 +84,17 @@ export function AllItem({ entryId, entryPreview, translation }: UniversalItemPro
const inInCollection = useRouteParamsSelector((s) => s.feedId === FEED_COLLECTION_LIST)
const feed =
useFeedById(entry?.feedId, (feed) => {
return {
type: feed.type,
ownerUserId: feed.ownerUserId,
id: feed.id,
title: feed.title,
url: (feed as any).url || "",
image: feed.image,
siteUrl: feed.siteUrl,
}
}) || entryPreview?.feeds
const feed = useFeedById(entry?.feedId, (feed) => {
return {
type: feed.type,
ownerUserId: feed.ownerUserId,
id: feed.id,
title: feed.title,
url: (feed as any).url || "",
image: feed.image,
siteUrl: feed.siteUrl,
}
})
const inbox = useInboxById(entry?.inboxId)
@ -141,7 +140,7 @@ export function AllItem({ entryId, entryPreview, translation }: UniversalItemPro
"before:bg-accent before:absolute before:-left-4 before:top-[14px] before:block before:size-2 before:rounded-full",
)}
>
<FeedIcon feed={related} fallback entry={entry?.iconEntry} size={20} />
<FeedIcon target={related} fallback entry={entry?.iconEntry} size={20} />
<div className={cn("flex h-fit min-w-0 flex-1 flex-row items-center text-sm leading-tight")}>
<div
className={cn(
@ -249,7 +248,7 @@ AllItem.wrapperClassName = "pl-7 pr-5"
export function AllItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="cursor-menu group relative flex py-4">
<FeedIcon feed={feed} fallback className="mr-2 size-5" />
<FeedIcon target={feed} fallback className="mr-2 size-5" />
<div className="-mt-0.5 min-w-0 flex-1 text-sm leading-tight">
<div className="text-text-secondary flex gap-1 text-[10px] font-bold">
<FeedTitle feed={feed} />

View File

@ -9,8 +9,8 @@ import { FeedTitle } from "~/modules/feed/feed-title"
import { readableContentMaxWidth } from "../styles"
import type { EntryItemStatelessProps, UniversalItemProps } from "../types"
export function ArticleItem({ entryId, entryPreview, translation }: UniversalItemProps) {
return <ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} />
export function ArticleItem({ entryId, translation }: UniversalItemProps) {
return <ListItem entryId={entryId} translation={translation} />
}
ArticleItem.wrapperClassName = readableContentMaxWidth
@ -18,7 +18,7 @@ ArticleItem.wrapperClassName = readableContentMaxWidth
export function ArticleItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="group relative flex py-4">
<FeedIcon feed={feed} fallback className="mr-2 size-5" />
<FeedIcon target={feed} fallback className="mr-2 size-5" />
<div className="-mt-0.5 min-w-0 flex-1 text-sm leading-tight">
<div className="text-text-secondary flex gap-1 text-[10px] font-bold">
<FeedTitle feed={feed} />

View File

@ -5,8 +5,8 @@ import { ListItem } from "~/modules/entry-column/templates/list-item-template"
import { readableContentMaxWidth } from "../styles"
import type { UniversalItemProps } from "../types"
export function AudioItem({ entryId, entryPreview, translation }: UniversalItemProps) {
return <ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} />
export function AudioItem({ entryId, translation }: UniversalItemProps) {
return <ListItem entryId={entryId} translation={translation} />
}
AudioItem.wrapperClassName = readableContentMaxWidth

View File

@ -8,8 +8,8 @@ import { FeedTitle } from "~/modules/feed/feed-title"
import { readableContentMaxWidth } from "../styles"
import type { EntryItemStatelessProps, UniversalItemProps } from "../types"
export function NotificationItem({ entryId, entryPreview, translation }: UniversalItemProps) {
return <ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} simple />
export function NotificationItem({ entryId, translation }: UniversalItemProps) {
return <ListItem entryId={entryId} translation={translation} simple />
}
NotificationItem.wrapperClassName = readableContentMaxWidth
@ -17,7 +17,7 @@ NotificationItem.wrapperClassName = readableContentMaxWidth
export function NotificationItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="cursor-menu group relative flex py-4">
<FeedIcon feed={feed} fallback className="mr-2 size-5" />
<FeedIcon target={feed} fallback className="mr-2 size-5" />
<div className="-mt-0.5 min-w-0 flex-1 text-sm leading-tight">
<div className="text-text-secondary flex gap-1 text-[10px] font-bold">
<FeedTitle feed={feed} />

View File

@ -22,9 +22,9 @@ import { EntryItemWrapper } from "../layouts/EntryItemWrapper"
import { GridItem, GridItemFooter } from "../templates/grid-item-template"
import type { UniversalItemProps } from "../types"
export function PictureItem({ entryId, entryPreview, translation }: UniversalItemProps) {
export function PictureItem({ entryId, translation }: UniversalItemProps) {
const entry = useEntry(entryId, (state) => ({ media: state.media, id: state.id }))
const entryMedia = entry?.media || entryPreview?.entries?.media || []
const entryMedia = entry?.media || []
const isActive = useRouteParamsSelector(({ entryId }) => entryId === entry?.id)
@ -33,7 +33,7 @@ export function PictureItem({ entryId, entryPreview, translation }: UniversalIte
const previewMedia = usePreviewMedia(entryContent)
if (!entry) return null
return (
<GridItem entryId={entryId} entryPreview={entryPreview} translation={translation}>
<GridItem entryId={entryId} translation={translation}>
<div className="relative flex gap-2 overflow-x-auto">
{entryMedia ? (
<SwipeMedia
@ -64,7 +64,7 @@ const proxySize = {
export const PictureWaterFallItem = memo(function PictureWaterFallItem({
entryId,
entryPreview,
translation,
index,
className,
@ -93,7 +93,7 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({
}
}, [ref, intersectionObserver])
const media = entry?.media || entryPreview?.entries?.media || []
const media = entry?.media || []
if (media?.length === 0) return null
if (!entry) return null
@ -122,11 +122,7 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({
/>
<div className="z-[3] shrink-0 overflow-hidden rounded-b-md pb-1">
<GridItemFooter
entryId={entryId}
entryPreview={entryPreview}
translation={translation}
/>
<GridItemFooter entryId={entryId} translation={translation} />
</div>
</MasonryItemFixedDimensionWrapper>
) : (

View File

@ -87,7 +87,7 @@ export const SocialMediaItem: EntryListItemFC = ({ entryId, translation }) => {
"before:bg-accent before:absolute before:-left-3 before:top-8 before:block before:size-2 before:rounded-full",
)}
>
<FeedIcon fallback feed={feed} entry={entry.iconEntry} size={32} className="mt-1" />
<FeedIcon fallback target={feed} entry={entry.iconEntry} size={32} className="mt-1" />
<div ref={ref} className="ml-2 min-w-0 flex-1">
<div className="-mt-0.5 flex-1 text-sm">
<div className="flex select-none flex-wrap space-x-1 leading-6" ref={titleRef}>
@ -140,7 +140,7 @@ SocialMediaItem.wrapperClassName = readableContentMaxWidth
export function SocialMediaItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="relative flex py-4">
<FeedIcon fallback feed={feed} size={32} className="mr-2 mt-1" />
<FeedIcon fallback target={feed} size={32} className="mr-2 mt-1" />
<div className="min-w-0 flex-1">
<div className="-mt-0.5 flex-1 text-sm">
<div className="flex select-none flex-wrap space-x-1 leading-6">

View File

@ -18,7 +18,7 @@ import type { EntryItemStatelessProps, UniversalItemProps } from "../types"
const ViewTag = IN_ELECTRON ? "webview" : "iframe"
export function VideoItem({ entryId, entryPreview, translation }: UniversalItemProps) {
export function VideoItem({ entryId, translation }: UniversalItemProps) {
const entry = useEntry(entryId, (state) => {
const { id, url } = state
@ -81,7 +81,7 @@ export function VideoItem({ entryId, entryPreview, translation }: UniversalItemP
if (!entry) return null
return (
<GridItem entryId={entryId} entryPreview={entryPreview} translation={translation}>
<GridItem entryId={entryId} translation={translation}>
<div className="cursor-card w-full">
<div className="relative overflow-x-auto" ref={ref}>
{miniIframeSrc && showPreview ? (
@ -167,7 +167,7 @@ export function VideoItemStateLess({ entry, feed }: EntryItemStatelessProps) {
</div>
</div>
<div className="flex items-center gap-1 truncate text-[13px]">
<FeedIcon fallback noMargin className="flex" feed={feed} size={18} />
<FeedIcon fallback noMargin className="flex" target={feed} size={18} />
<span className="min-w-0 truncate pl-1">
<FeedTitle feed={feed} />
</span>

View File

@ -33,7 +33,7 @@ import type { EntryItemStatelessProps, UniversalItemProps } from "../types"
const ViewTag = IN_ELECTRON ? "webview" : "iframe"
export function VideoItem({ entryId, entryPreview, translation }: UniversalItemProps) {
export function VideoItem({ entryId, translation }: UniversalItemProps) {
const entry = useEntry(entryId, (state) => {
const { id, url } = state
@ -100,7 +100,7 @@ export function VideoItem({ entryId, entryPreview, translation }: UniversalItemP
if (!entry) return null
return (
<GridItem entryId={entryId} entryPreview={entryPreview} translation={translation}>
<GridItem entryId={entryId} translation={translation}>
<div
className="cursor-card w-full"
onClick={(e) => {
@ -273,7 +273,7 @@ export function VideoItemStateLess({ entry, feed }: EntryItemStatelessProps) {
</div>
</div>
<div className="flex items-center gap-1 truncate text-[13px]">
<FeedIcon fallback noMargin className="flex" feed={feed} size={18} />
<FeedIcon fallback noMargin className="flex" target={feed} size={18} />
<span className="min-w-0 truncate pl-1">
<FeedTitle feed={feed} />
</span>

View File

@ -20,14 +20,14 @@ interface GridItemProps extends UniversalItemProps {
wrapperClassName?: string
}
export function GridItem(props: GridItemProps) {
const { entryId, entryPreview, wrapperClassName, children, translation } = props
const { entryId, wrapperClassName, children, translation } = props
const hasEntry = useHasEntry(entryId)
if (!hasEntry) return null
return (
<div className={cn("p-1.5", wrapperClassName)}>
{children}
<GridItemFooter entryId={entryId} entryPreview={entryPreview} translation={translation} />
<GridItemFooter entryId={entryId} translation={translation} />
</div>
)
}
@ -38,7 +38,7 @@ export const GridItemFooter = ({
titleClassName,
descriptionClassName,
timeClassName,
}: Pick<GridItemProps, "entryId" | "entryPreview" | "translation"> & {
}: Pick<GridItemProps, "entryId" | "translation"> & {
titleClassName?: string
descriptionClassName?: string
timeClassName?: string
@ -105,7 +105,7 @@ export const GridItemFooter = ({
fallback
noMargin
className="flex"
feed={feeds!}
target={feeds!}
entry={entry?.iconEntry}
size={18}
/>

View File

@ -55,7 +55,6 @@ const entrySelector = (state: EntryModel) => {
}
export function ListItem({
entryId,
entryPreview,
translation,
simple,
}: UniversalItemProps & {
@ -71,18 +70,17 @@ export function ListItem({
const inInCollection = useRouteParamsSelector((s) => s.feedId === FEED_COLLECTION_LIST)
const feed =
useFeedById(entry?.feedId, (feed) => {
return {
type: feed.type,
ownerUserId: feed.ownerUserId,
id: feed.id,
title: feed.title,
url: (feed as any).url || "",
image: feed.image,
siteUrl: feed.siteUrl,
}
}) || entryPreview?.feeds
const feed = useFeedById(entry?.feedId, (feed) => {
return {
type: feed.type,
ownerUserId: feed.ownerUserId,
id: feed.id,
title: feed.title,
url: (feed as any).url || "",
image: feed.image,
siteUrl: feed.siteUrl,
}
})
const inbox = useInboxById(entry?.inboxId)
@ -149,7 +147,7 @@ export function ListItem({
"before:bg-accent before:absolute before:-left-3 before:top-6 before:block before:size-2 before:rounded-full",
)}
>
<FeedIcon feed={related} fallback entry={entry?.iconEntry} size={24} />
<FeedIcon target={related} fallback entry={entry?.iconEntry} size={24} />
<div
className={cn("-mt-0.5 ml-1 h-fit flex-1 text-sm leading-tight", lineClamp.global)}
style={{
@ -226,7 +224,7 @@ export function ListItem({
fallbackElement={
<div className={clsx("bg-material-ultra-thick", "size-[80px]", "rounded")} />
}
feed={feed || inbox}
target={feed || inbox}
entry={entry?.iconEntry}
size={80}
className="m-0 rounded"

View File

@ -55,7 +55,7 @@ const entrySelector = (state: EntryModel) => {
}
export function ListItem({
entryId,
entryPreview,
translation,
simple,
}: UniversalItemProps & {
@ -71,18 +71,17 @@ export function ListItem({
const inInCollection = useRouteParamsSelector((s) => s.feedId === FEED_COLLECTION_LIST)
const feed =
useFeedById(entry?.feedId, (feed) => {
return {
type: feed.type,
ownerUserId: feed.ownerUserId,
id: feed.id,
title: feed.title,
url: (feed as any).url || "",
image: feed.image,
siteUrl: feed.siteUrl,
}
}) || entryPreview?.feeds
const feed = useFeedById(entry?.feedId, (feed) => {
return {
type: feed.type,
ownerUserId: feed.ownerUserId,
id: feed.id,
title: feed.title,
url: (feed as any).url || "",
image: feed.image,
siteUrl: feed.siteUrl,
}
})
const inbox = useInboxById(entry?.inboxId)
@ -151,7 +150,7 @@ export function ListItem({
settingWideMode ? "py-3" : "py-4",
)}
>
<FeedIcon feed={related} fallback entry={entry?.iconEntry} />
<FeedIcon target={related} fallback entry={entry?.iconEntry} />
<div
className={cn("-mt-0.5 flex-1 text-sm leading-tight", lineClamp.global)}
style={{
@ -234,7 +233,7 @@ export function ListItem({
)}
/>
}
feed={feed || inbox}
target={feed || inbox}
entry={entry?.iconEntry}
size={settingWideMode ? 65 : 80}
className="m-0 rounded"

View File

@ -1,19 +1,10 @@
import type {
CombinedEntryModel,
EntryModelSimple,
FeedModel,
FeedOrListRespModel,
} from "@follow/models/types"
import type { FeedModel } from "@follow/store/feed/types"
import type { EntryTranslation } from "@follow/store/translation/types"
import type { ParsedEntry } from "@follow-app/client-sdk"
import type { FC } from "react"
export type UniversalItemProps = {
entryId: string
entryPreview?: CombinedEntryModel & {
feeds: FeedOrListRespModel
feedId: string
inboxId: string
}
translation?: EntryTranslation
}
@ -23,6 +14,6 @@ export type EntryListItemFC<P extends object = object> = FC<P & UniversalItemPro
export type EntryItemStatelessProps = {
feed: FeedModel
entry: EntryModelSimple
entry: ParsedEntry
view?: number
}

View File

@ -5,7 +5,7 @@ import {
useMasonryItemWidth,
} from "@follow/components/ui/masonry/contexts.jsx"
import { useMasonryColumn } from "@follow/components/ui/masonry/hooks.js"
import type { MediaModel } from "@follow/models/types"
import type { MediaModel } from "@folo-services/drizzle"
import type { RenderComponentProps } from "masonic"
import { Masonry } from "masonic"
import { useState } from "react"
@ -47,7 +47,7 @@ const Render: React.ComponentType<
/>
)
}
export const ImageGallery = ({ images }: { images: MediaModel }) => {
export const ImageGallery = ({ images }: { images: MediaModel[] }) => {
const { containerRef, currentColumn, currentItemWidth } = useMasonryColumn(gutter)
const [masonryItemsRadio, setMasonryItemsRadio] = useState<Record<string, number>>({})

View File

@ -80,7 +80,7 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
return compact ? (
<div className="cursor-button @sm:-mx-3 @sm:p-3 -mx-6 flex items-center gap-2 rounded-lg p-6 transition-colors">
<FeedIcon fallback feed={feed || inbox} entry={entry.iconEntry} size={50} />
<FeedIcon fallback target={feed || inbox} entry={entry.iconEntry} size={50} />
<div className="leading-6">
<div className="flex items-center gap-1 text-base font-semibold">
<span>{entry.author || feed?.title || inbox?.title}</span>
@ -119,7 +119,7 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
})
}
>
<FeedIcon fallback feed={feed || inbox} entry={entry.iconEntry} size={16} />
<FeedIcon fallback target={feed || inbox} entry={entry.iconEntry} size={16} />
{getPreferredTitle(feed || inbox, entry.titleEntry)}
</div>

View File

@ -6,5 +6,5 @@ export const ImageGalleryContent = ({ entryId }: { entryId: string }) => {
const images = useEntry(entryId, (entry) => entry.media)
// images?.length && images.length > 5
// We don't need to check here, we already check in the action
return <ImageGallery images={images} />
return <ImageGallery images={images || []} />
}

View File

@ -52,7 +52,7 @@ export const SupportCreator = ({ entryId }: { entryId: string }) => {
enableModal
/>
) : (
<FeedIcon noMargin className="w-40 flex-col gap-3 p-0" size={46} feed={feed} fallback />
<FeedIcon noMargin className="w-40 flex-col gap-3 p-0" size={46} target={feed} fallback />
)}
<span className="-mt-6 text-lg font-medium">{feed.title}</span>

View File

@ -4,9 +4,9 @@ import { RootPortal } from "@follow/components/ui/portal/index.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { FeedViewType } from "@follow/constants"
import { useTitle } from "@follow/hooks"
import type { FeedModel } from "@follow/models/types"
import { useEntry } from "@follow/store/entry/hooks"
import { useFeedById } from "@follow/store/feed/hooks"
import type { FeedModel } from "@follow/store/feed/types"
import { useIsInbox } from "@follow/store/inbox/hooks"
import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
import { thenable } from "@follow/utils"

View File

@ -6,9 +6,9 @@ import { RootPortal } from "@follow/components/ui/portal/index.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { FeedViewType } from "@follow/constants"
import { useTitle } from "@follow/hooks"
import type { FeedModel } from "@follow/models/types"
import { useEntry } from "@follow/store/entry/hooks"
import { useFeedById } from "@follow/store/feed/hooks"
import type { FeedModel } from "@follow/store/feed/types"
import { useIsInbox } from "@follow/store/inbox/hooks"
import { thenable } from "@follow/utils"
import { nextFrame, stopPropagation } from "@follow/utils/dom"

View File

@ -54,7 +54,7 @@ export const AuthorHeader: React.FC<AuthorHeaderProps> = ({
{showAvatar && (
<FeedIcon
fallback
feed={feed}
target={feed}
entry={entry.iconEntry}
size={avatarSize}
className="shrink-0"

View File

@ -5,8 +5,9 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.jsx"
import type { FeedOrListRespModel } from "@follow/models/types"
import { useWhoami } from "@follow/store/user/hooks"
import type { FeedModel } from "@follow/store/feed/types"
import type { ListModel } from "@follow/store/list/types"
import { useUserById, useWhoami } from "@follow/store/user/hooks"
import { cn } from "@follow/utils/utils"
import { useTranslation } from "react-i18next"
@ -17,11 +18,11 @@ export const FeedCertification = ({
feed,
className,
}: {
feed: FeedOrListRespModel
feed: FeedModel | ListModel
className?: string
}) => {
const me = useWhoami()
const presentUserProfile = usePresentUserProfileModal("drawer")
const { t } = useTranslation()
const { type } = feed
@ -61,17 +62,8 @@ export const FeedCertification = ({
</div>
<div className="mt-1 flex items-center gap-1.5">
<span>{t("feed_item.claimed_by_owner")}</span>
{feed.owner ? (
<Avatar
className="inline-flex aspect-square size-5 rounded-full"
onClick={(e) => {
e.stopPropagation()
presentUserProfile(feed.owner!.id)
}}
>
<AvatarImage src={replaceImgUrlIfNeed(feed.owner.image || undefined)} />
<AvatarFallback>{feed.owner.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
{feed.ownerUserId ? (
<FeedCertificateAvatar userId={feed.ownerUserId} />
) : (
<span>{t("feed_item.claimed_by_unknown")}</span>
)}
@ -82,3 +74,21 @@ export const FeedCertification = ({
))
)
}
const FeedCertificateAvatar = ({ userId }: { userId: string }) => {
const user = useUserById(userId)
const presentUserProfile = usePresentUserProfileModal("drawer")
if (!user) return null
return (
<Avatar
className="inline-flex aspect-square size-5 rounded-full"
onClick={(e) => {
e.stopPropagation()
presentUserProfile(userId)
}}
>
<AvatarImage src={replaceImgUrlIfNeed(user.image || undefined)} />
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
)
}

View File

@ -1,8 +1,6 @@
// import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avatar/index.jsx"
import { PlatformIcon } from "@follow/components/ui/platform-icon/index.jsx"
import type { FeedOrListRespModel } from "@follow/models/types"
import type { FeedModel } from "@follow/store/feed/types"
import type { ListModel } from "@follow/store/list/types"
import { getBackgroundGradient } from "@follow/utils/color"
import { getImageProxyUrl } from "@follow/utils/img-proxy"
import { cn, getUrlIcon } from "@follow/utils/utils"
@ -21,18 +19,22 @@ const getBorderRadius = (size: number) => {
return "rounded-xl" // 12px for extra large avatars
}
function getIconProps(
props: Pick<
Parameters<typeof FeedIcon>[0],
"feed" | "entry" | "useMedia" | "siteUrl" | "fallbackUrl" | "fallback" | "size"
>,
) {
const { feed, entry, useMedia, siteUrl: propSiteUrl, fallbackUrl, fallback, size = 20 } = props
type GetIconPropsProps = {
target?: IconTarget | null
entry?: FeedIconEntry | null
useMedia?: boolean
siteUrl?: string
fallbackUrl?: string
fallback?: boolean
size?: number
}
function getIconProps(props: GetIconPropsProps) {
const { target, entry, useMedia, siteUrl: propSiteUrl, fallbackUrl, fallback, size = 20 } = props
const image =
(useMedia ? entry?.firstPhotoUrl || entry?.authorAvatar : entry?.authorAvatar) || feed?.image
const siteUrl = (feed as FeedModel)?.siteUrl || fallbackUrl
(useMedia ? entry?.firstPhotoUrl || entry?.authorAvatar : entry?.authorAvatar) || target?.image
const siteUrl = (target as FeedModel)?.siteUrl || fallbackUrl
if (propSiteUrl && !feed) {
if (propSiteUrl && !target) {
const [src] = getFeedIconSrc({
siteUrl: propSiteUrl,
})
@ -72,13 +74,13 @@ function getIconProps(
fallbackSrc,
}
}
if (feed?.type === "inbox") {
if (target?.type === "inbox") {
return {
type: "inbox" as const,
}
}
if (feed?.title) {
if (target?.title) {
return {
type: "text" as const,
}
@ -147,10 +149,19 @@ const FallbackableImage = function FallbackableImage({
)
}
type FeedIconFeed =
| Pick<FeedModel, "title" | "image" | "siteUrl" | "type">
| ListModel
| FeedOrListRespModel
// type FeedIconFeed = Pick<FeedModel, "title" | "image" | "siteUrl" | "type"> | ListModel
type IconTarget = {
title?: Nullable<string>
image?: Nullable<string>
siteUrl?: Nullable<string>
type: "feed" | "list" | "inbox"
entry?: FeedIconEntry | null
useMedia?: boolean
feed?: FeedModel | null
fallbackUrl?: string
fallback?: boolean
size?: number
}
export type FeedIconEntry = { authorAvatar?: string | null; firstPhotoUrl?: string | null }
const fadeInVariant = {
@ -160,7 +171,7 @@ const fadeInVariant = {
const isIconLoadedSet = new Set<string>()
export function FeedIcon({
feed,
target,
entry,
fallbackUrl,
className,
@ -172,7 +183,7 @@ export function FeedIcon({
disableFadeIn,
noMargin,
}: {
feed?: FeedIconFeed | null
target?: IconTarget | null
entry?: FeedIconEntry | null
fallbackUrl?: string
className?: string
@ -189,11 +200,11 @@ export function FeedIcon({
noMargin?: boolean
}) {
const marginClassName = cn(noMargin ? "" : "mr-2", className)
const iconProps = getIconProps({ feed, entry, useMedia, siteUrl, fallbackUrl, fallback, size })
const iconProps = getIconProps({ target, entry, useMedia, siteUrl, fallbackUrl, fallback, size })
const colors = useMemo(
() => getBackgroundGradient(feed?.title || (feed as FeedModel)?.url || siteUrl || ""),
[feed?.title, (feed as FeedModel)?.url, siteUrl],
() => getBackgroundGradient(target?.title || (target as FeedModel)?.url || siteUrl || ""),
[target?.title, (target as FeedModel)?.url, siteUrl],
)
const sizeStyle: React.CSSProperties = useMemo(
@ -226,7 +237,7 @@ export function FeedIcon({
fontSize: size / 2,
}}
>
{!!feed?.title && feed.title[0]}
{!!target?.title && target.title[0]}
</span>
</span>
)

View File

@ -1,6 +1,8 @@
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/EllipsisWithTooltip.js"
import type { FeedOrListRespModel } from "@follow/models/types"
import { env } from "@follow/shared/env.desktop"
import type { FeedModel } from "@follow/store/feed/types"
import type { InboxModel } from "@follow/store/inbox/types"
import type { ListModel } from "@follow/store/list/types"
import { cn } from "@follow/utils/utils"
import { UrlBuilder } from "~/lib/url-builder"
@ -13,7 +15,7 @@ export function FollowSummary({
className,
simple,
}: {
feed: FeedOrListRespModel
feed: FeedModel | ListModel | InboxModel
docs?: string
className?: string
simple?: boolean
@ -39,7 +41,7 @@ export function FollowSummary({
<div className={cn("flex select-text flex-col gap-2 text-sm", className)}>
<div className="flex items-center">
<FeedIcon
feed={feed}
target={feed}
fallbackUrl={docs}
className="mask-squircle mask shrink-0 rounded-none"
size={32}

View File

@ -1,5 +1,7 @@
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/EllipsisWithTooltip.js"
import type { FeedOrListRespModel } from "@follow/models/types"
import type { FeedModel } from "@follow/store/feed/types"
import type { InboxModel } from "@follow/store/inbox/types"
import type { ListModel } from "@follow/store/list/types"
import { cn } from "@follow/utils/utils"
import { useUISettingKey } from "~/atoms/settings/ui"
@ -14,7 +16,7 @@ export const FeedTitle = ({
title,
style,
}: {
feed?: FeedOrListRespModel | null
feed?: FeedModel | ListModel | InboxModel | null
className?: string
titleClassName?: string
title?: string | null
@ -29,7 +31,7 @@ export const FeedTitle = ({
<EllipsisHorizontalTextWithTooltip className={cn("truncate", titleClassName)}>
{title || getPreferredTitle(feed)}
</EllipsisHorizontalTextWithTooltip>
{!hideExtraBadge && (
{!hideExtraBadge && feed.type !== "inbox" && (
<>
<FeedCertification feed={feed} />
<BoostCertification feed={feed} />

View File

@ -272,7 +272,7 @@ const SearchItem = memo(function Item({
}}
>
<div className="relative flex w-full items-center justify-between px-1 py-2">
{feed && <FeedIcon className="mr-2 size-5 shrink-0 rounded" feed={feed} />}
{feed && <FeedIcon className="mr-2 size-5 shrink-0 rounded" target={feed} />}
<span className="block min-w-0 flex-1 shrink-0 truncate">{title}</span>
<span className="block min-w-0 shrink-0 grow-0 text-xs font-medium text-zinc-800 opacity-60 dark:text-slate-200/80">
{subtitle}

View File

@ -211,7 +211,7 @@ const CornerPlayerImpl = ({ hideControls, rounded }: ControlButtonProps) => {
{/* play cover */}
<div className="relative size-[3.625rem] shrink-0">
<FeedIcon
feed={feed}
target={feed}
entry={entry.iconEntry}
size={isMobile ? 65.25 : 58}
fallback={false}

View File

@ -12,7 +12,7 @@ import { useMutation } from "@tanstack/react-query"
import { Trans, useTranslation } from "react-i18next"
import { useServerConfigs } from "~/atoms/server-configs"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { SettingSectionTitle } from "~/modules/settings/section"
import { ActivityPoints } from "~/modules/wallet/activity-points"
import { Balance } from "~/modules/wallet/balance"
@ -34,7 +34,7 @@ export const MyWalletSection = ({ className }: { className?: string }) => {
const refreshMutation = useMutation({
mutationFn: async () => {
await apiClient.wallets.refresh.$post()
await followClient.api.wallets.refresh()
},
onSuccess: () => {
walletActions.get().invalidate()

View File

@ -27,7 +27,7 @@ import { z } from "zod"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useAuthQuery } from "~/hooks/common/useBizQuery"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import { useTOTPModalWrapper } from "~/modules/profile/hooks"
import { Balance } from "~/modules/wallet/balance"
@ -69,7 +69,7 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
const powerPrice = useAuthQuery(
defineQuery(["power-price"], async () => {
const res = await apiClient.wallets["power-price"].$get()
const res = await followClient.api.wallets.powerPrice()
return res.data
}),
)
@ -87,13 +87,11 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
TOTPCode?: string
}) => {
const amountBigInt = from(amount, 18)[0]
await apiClient.wallets.transactions.withdraw.$post({
json: {
address,
amount: amountBigInt.toString(),
toRss3,
TOTPCode,
},
await followClient.api.wallets.transactions.withdraw({
address,
amount: amountBigInt.toString(),
toRss3,
TOTPCode,
})
},
})
@ -113,7 +111,7 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
if (mutation.isSuccess) {
toast.success(t("wallet.withdraw.success"))
walletActions.get().invalidate()
walletActions.transactions.get().invalidate()
walletActions.transactions.get({}).invalidate()
dismiss()
}
}, [mutation.isSuccess, t, dismiss])

View File

@ -1,7 +1,7 @@
import { LoadingCircle } from "@follow/components/ui/loading/index.js"
import { Tabs, TabsList, TabsTrigger } from "@follow/components/ui/tabs/index.jsx"
import { TransactionTypes } from "@follow/models/types"
import { useWhoami } from "@follow/store/user/hooks"
import { TransactionTypes } from "@follow-app/client-sdk"
import { useState } from "react"
import { useTranslation } from "react-i18next"

View File

@ -13,9 +13,9 @@ import {
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.jsx"
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
import type { TransactionTypes } from "@follow/models"
import { useWhoami } from "@follow/store/user/hooks"
import { cn } from "@follow/utils/utils"
import type { TransactionType } from "@follow-app/client-sdk"
import { useTranslation } from "react-i18next"
import { RelativeTime } from "~/components/ui/datetime"
@ -30,7 +30,7 @@ export const TxTable = ({ className, type }: ComponentType<TxTableProps>) => {
const user = useWhoami()
const transactions = useWalletTransactions({
fromOrToUserId: user?.id,
type: type === "all" ? undefined : (type as (typeof TransactionTypes)[number]),
type: type === "all" ? undefined : (type as TransactionType),
})
return (

View File

@ -10,7 +10,7 @@ import { useAsyncModal } from "~/components/ui/modal/helper/useAsyncModal"
import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useAuthQuery } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import { getFetchErrorInfo } from "~/lib/error-parser"
@ -23,9 +23,7 @@ const LazyUserProfileModalContent = lazy(() =>
export const useUserSubscriptionsQuery = (userId: string | undefined) => {
const subscriptions = useAuthQuery(
defineQuery(["subscriptions", "group", userId], async () => {
const res = await apiClient.subscriptions.$get({
query: { userId },
})
const res = await followClient.api.subscriptions.get({ userId })
const groupFolder = {} as Record<string, typeof res.data>
for (const subscription of res.data || []) {

View File

@ -106,17 +106,16 @@ export const ProfileSettingForm = ({
handle: values.handle,
image: values.image,
name: values.name,
// @ts-expect-error
bio: values.bio,
website: values.website,
socialLinks: values.socialLinks,
socialLinks: values.socialLinks as any,
}),
onError: (error) => {
toastFetchError(error)
},
onSuccess: (_, variables) => {
if (user && variables) {
userActions.updateWhoami({ ...variables })
userActions.updateWhoami({ ...variables } as any)
}
toast(t("profile.updateSuccess"), {
duration: 3000,

View File

@ -4,12 +4,12 @@ import { ActionButton, Button } from "@follow/components/ui/button/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js"
import type { ExtractBizResponse } from "@follow/models"
import { usePrefetchUser, useUserById, useWhoami } from "@follow/store/user/hooks"
import { getAvatarUrl } from "@follow/utils"
import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { getStorageNS } from "@follow/utils/ns"
import { cn } from "@follow/utils/utils"
import type { ListWithStats } from "@follow-app/client-sdk"
import { useQuery } from "@tanstack/react-query"
import { useAtom } from "jotai"
import { atomWithStorage } from "jotai/utils"
@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next"
import { m } from "~/components/common/Motion"
import { useCurrentModal } from "~/components/ui/modal/stacked/hooks"
import { useFollow } from "~/hooks/biz/useFollow"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { UrlBuilder } from "~/lib/url-builder"
import { FeedIcon } from "~/modules/feed/feed-icon"
@ -63,7 +63,7 @@ const pickUserData = <
}
}
const ListCard = memo(({ list }: { list: List }) => {
const ListCard = memo(({ list }: { list: ListWithStats }) => {
return (
<div className="group/card border-fill bg-material-ultra-thin hover:border-fill-secondary relative overflow-hidden rounded-lg border transition-all duration-200">
<a
@ -77,7 +77,7 @@ const ListCard = memo(({ list }: { list: List }) => {
<div className="shrink-0">
<FeedIcon
fallback
feed={list}
target={list as any}
className="border-fill-secondary size-10 rounded-lg border"
noMargin
/>
@ -364,15 +364,13 @@ const useUserListsQuery = (userId: string) => {
return useQuery({
queryKey: ["lists", userId],
queryFn: async () => {
const res = await apiClient.lists.list.$get({ query: { userId } })
const res = await followClient.api.lists.list({ userId })
return res.data
},
})
}
type List = ExtractBizResponse<typeof apiClient.lists.list.$get>["data"][number]
const Lists = ({ lists }: { lists: List[] }) => {
const Lists = ({ lists }: { lists: ListWithStats[] }) => {
const { t } = useTranslation()
if (!lists || lists.length === 0) return null
return (

View File

@ -5,10 +5,14 @@ import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avata
import { Button } from "@follow/components/ui/button/index.js"
import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
import type { SubscriptionModel } from "@follow/models/types"
import { useIsSubscribed } from "@follow/store/subscription/hooks"
import { usePrefetchUser, useUserById } from "@follow/store/user/hooks"
import { cn } from "@follow/utils/utils"
import type {
InboxSubscriptionResponse,
ListSubscriptionResponse,
SubscriptionWithFeed,
} from "@follow-app/client-sdk"
import { AnimatePresence } from "motion/react"
import type { FC } from "react"
import { memo, useState } from "react"
@ -71,7 +75,7 @@ export const SubscriptionItems = ({
export const SubscriptionGroup: FC<{
category: string
subscriptions: SubscriptionModel[]
subscriptions: (SubscriptionWithFeed | ListSubscriptionResponse | InboxSubscriptionResponse)[]
itemStyle: ItemVariant
}> = memo(({ category, subscriptions, itemStyle }) => {
const [isOpened, setIsOpened] = useState(true)
@ -114,7 +118,7 @@ export const SubscriptionGroup: FC<{
})
const SubscriptionItem: FC<{
subscription: SubscriptionModel
subscription: SubscriptionWithFeed | ListSubscriptionResponse | InboxSubscriptionResponse
variant: ItemVariant
}> = ({ subscription, variant }) => {
@ -154,7 +158,7 @@ const SubscriptionItem: FC<{
target="_blank"
onClick={isMobile ? handleFollow : undefined}
>
<FeedIcon feed={subscription.feeds} size={22} className="mr-3" />
<FeedIcon target={subscription.feeds as any} size={22} className="mr-3" />
<div
className={cn(
"w-0 flex-1 grow",

View File

@ -9,8 +9,8 @@ import {
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/Input.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import type { RSSHubModel } from "@follow/models"
import { whoami } from "@follow/store/user/getters"
import type { RSSHubListItem } from "@follow-app/client-sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
@ -28,7 +28,7 @@ export function AddModalContent({
instance,
}: {
dismiss: () => void
instance?: RSSHubModel
instance?: RSSHubListItem
}) {
const { t } = useTranslation("settings")
const addRSSHubMutation = useAddRSSHubMutation()

View File

@ -3,14 +3,14 @@ import { useMutation } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { Queries } from "~/queries"
export const ConfirmDeleteModalContent = ({ id, dismiss }: { dismiss: () => void; id: string }) => {
const { t } = useTranslation("settings")
const deleteMutation = useMutation({
mutationFn: () => {
return apiClient.rsshub.$delete({ json: { id } })
return followClient.api.rsshub.delete({ id })
},
onSuccess: () => {
Queries.rsshub.list().invalidate()

View File

@ -9,8 +9,8 @@ import {
FormMessage,
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/Input.js"
import type { RSSHubModel } from "@follow/models"
import { whoami } from "@follow/store/user/getters"
import type { RSSHubListItem } from "@follow-app/client-sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
@ -29,7 +29,7 @@ export function SetModalContent({
instance,
}: {
dismiss: () => void
instance: RSSHubModel
instance: RSSHubListItem
}) {
const { t } = useTranslation("settings")
const setRSSHubMutation = useSetRSSHubMutation()

View File

@ -2,6 +2,7 @@ import type { AISettings, GeneralSettings, UISettings } from "@follow/shared/set
import { EventBus } from "@follow/utils/event-bus"
import { getStorageNS } from "@follow/utils/ns"
import { isEmptyObject, sleep } from "@follow/utils/utils"
import type { SettingsTab } from "@follow-app/client-sdk"
import { omit } from "es-toolkit/compat"
import type { PrimitiveAtom } from "jotai"
@ -12,7 +13,7 @@ import {
getGeneralSettings,
} from "~/atoms/settings/general"
import { __uiSettingAtom, getUISettings, uiServerSyncWhiteListKeys } from "~/atoms/settings/ui"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { jotaiStore } from "~/lib/jotai"
import { settings } from "~/queries/settings"
@ -171,11 +172,10 @@ class SettingSyncQueue {
if (isEmptyObject(json)) {
continue
}
const promise = apiClient.settings[":tab"]
.$patch({
param: {
tab,
},
const promise = followClient.api.settings
.update({
tab: tab as SettingsTab,
json,
})
.then(() => {
@ -199,10 +199,9 @@ class SettingSyncQueue {
const promises = [] as Promise<any>[]
for (const tab in localSettingGetterMap) {
const payload = localSettingGetterMap[tab]()
const promise = apiClient.settings[":tab"].$patch({
param: {
tab,
},
const promise = followClient.api.settings.update({
tab: tab as SettingsTab,
json: payload,
})
@ -215,10 +214,8 @@ class SettingSyncQueue {
const payload = localSettingGetterMap[tab]()
this.chain = this.chain.finally(() =>
apiClient.settings[":tab"].$patch({
param: {
tab,
},
followClient.api.settings.update({
tab: tab as SettingsTab,
json: payload,
}),
)

View File

@ -451,7 +451,7 @@ const FeedListItem = memo(
<Checkbox checked={selected} onCheckedChange={(checked) => onSelect(id, !!checked)} />
</div>
<div className="flex min-w-0 items-center gap-1">
<FeedIcon feed={feed} size={16} />
<FeedIcon target={feed} size={16} />
<div className="flex min-w-0 flex-col">
{feed?.errorAt ? (
<Tooltip>
@ -591,7 +591,7 @@ const FeedClaimedSection = () => {
href={UrlBuilder.shareFeed(row.feed.id)}
className="flex items-center"
>
<FeedIcon fallback feed={row.feed} size={16} />
<FeedIcon fallback target={row.feed} size={16} />
<EllipsisHorizontalTextWithTooltip className="inline-block max-w-[200px] truncate">
{row.feed.title}
</EllipsisHorizontalTextWithTooltip>

View File

@ -16,6 +16,7 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.jsx"
import type { CreateInvitationRequest } from "@follow-app/client-sdk"
import { useMutation } from "@tanstack/react-query"
import dayjs from "dayjs"
import { Trans, useTranslation } from "react-i18next"
@ -25,7 +26,7 @@ import { useServerConfigs } from "~/atoms/server-configs"
import { CopyButton } from "~/components/ui/button/CopyButton"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useAuthQuery } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { copyToClipboard } from "~/lib/clipboard"
import { toastFetchError } from "~/lib/error-parser"
import { usePresentUserProfileModal, useTOTPModalWrapper } from "~/modules/profile/hooks"
@ -172,8 +173,8 @@ const ConfirmModalContent = ({ dismiss }: { dismiss: () => void }) => {
const { t } = useTranslation("settings")
const newInvitation = useMutation({
mutationKey: ["newInvitation"],
mutationFn: (values: Parameters<typeof apiClient.invitations.new.$post>[0]["json"]) =>
apiClient.invitations.new.$post({ json: values }),
mutationFn: (values: CreateInvitationRequest) =>
followClient.api.invitations.create({ TOTPCode: values.TOTPCode }),
onError(err) {
toastFetchError(err)
},

View File

@ -1,7 +1,8 @@
import { Card, CardContent, CardHeader } from "@follow/components/ui/card/index.jsx"
import { FeedViewType, views } from "@follow/constants"
import type { EntryModelSimple, FeedModel } from "@follow/models"
import type { FeedModel } from "@follow/store/feed/types"
import { cn } from "@follow/utils/utils"
import type { ParsedEntry } from "@follow-app/client-sdk"
import { cloneElement } from "react"
import { useI18n } from "~/hooks/common"
@ -17,7 +18,7 @@ export const ViewSelectorRadioGroup = ({
className,
...rest
}: {
entries?: EntryModelSimple[]
entries?: ParsedEntry[]
feed?: FeedModel
view?: number
} & React.InputHTMLAttributes<HTMLInputElement> & {

View File

@ -193,7 +193,7 @@ const FeedItemImpl = ({ view, feedId, className, isPreview }: FeedItemProps) =>
{...contextMenuProps}
>
<div className={cn("flex min-w-0 items-center", isFeed && feed.errorAt && "text-red")}>
<FeedIcon fallback feed={feed} size={16} />
<FeedIcon fallback target={feed} size={16} />
<FeedTitle feed={feed} />
{isFeed && (
<ErrorTooltip errorAt={feed.errorAt} errorMessage={feed.errorMessage}>
@ -319,7 +319,7 @@ const ListItemImpl: Component<ListItemProps> = ({
{...contextMenuProps}
>
<div className="flex min-w-0 flex-1 items-center">
<FeedIcon fallback feed={list} size={iconSize} className="mask mask-squircle" />
<FeedIcon fallback target={list} size={iconSize} className="mask mask-squircle" />
<EllipsisHorizontalTextWithTooltip className="truncate">
{getPreferredTitle(list)}
</EllipsisHorizontalTextWithTooltip>
@ -432,7 +432,7 @@ const InboxItemImpl: Component<InboxItemProps> = ({ view, inboxId, className, ic
{...contextMenuProps}
>
<div className={"flex min-w-0 items-center"}>
<FeedIcon fallback feed={inbox} size={iconSize} />
<FeedIcon fallback target={inbox} size={iconSize} />
<EllipsisHorizontalTextWithTooltip className="truncate">
{getPreferredTitle(inbox)}
</EllipsisHorizontalTextWithTooltip>

View File

@ -9,6 +9,7 @@ import {
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
import { SegmentGroup, SegmentItem } from "@follow/components/ui/segment/index.js"
import type { DiscoveryItem } from "@follow-app/client-sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
import { atom, useAtomValue, useStore } from "jotai"
@ -20,7 +21,7 @@ import { Link } from "react-router"
import { z } from "zod"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { DiscoverFeedCard } from "../discover/DiscoverFeedCard"
import { FeedForm } from "../discover/FeedForm"
@ -30,8 +31,6 @@ const formSchema = z.object({
type: z.enum(["search", "rss", "rsshub"]),
})
type DiscoverSearchData = Awaited<ReturnType<typeof apiClient.discover.$post>>["data"]
const typeConfig = {
search: {
label: "discover.any_url_or_keyword",
@ -69,7 +68,7 @@ export function SimpleDiscoverModal({ dismiss }: { dismiss: () => void }) {
const watchedType = form.watch("type")
const currentConfig = typeConfig[watchedType]
const discoverSearchDataAtom = useState(() => atom<DiscoverSearchData>())[0]
const discoverSearchDataAtom = useState(() => atom<DiscoveryItem[]>())[0]
const discoverSearchData = useAtomValue(discoverSearchDataAtom)
const mutation = useMutation({
@ -91,12 +90,9 @@ export function SimpleDiscoverModal({ dismiss }: { dismiss: () => void }) {
return []
}
// For search, use discover API
const { data } = await apiClient.discover.$post({
json: {
keyword: keyword.trim(),
target: "feeds",
},
const { data } = await followClient.api.discover.discover({
keyword: keyword.trim(),
target: "feeds",
})
jotaiStore.set(discoverSearchDataAtom, data)

View File

@ -8,7 +8,7 @@ import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { setUISetting, useUISettingKey } from "~/atoms/settings/ui"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { TrendingFeedCard } from "../discover/TrendingFeedCard"
@ -61,12 +61,10 @@ export function Trending({
const { data, isLoading } = useQuery({
queryKey: ["trending", lang, selectedView],
queryFn: async () => {
return await apiClient.trending.feeds.$get({
query: {
language: lang === "all" ? undefined : lang,
view: selectedView === "all" ? undefined : Number(selectedView),
limit,
},
return await followClient.api.trending.getFeeds({
language: lang === "all" ? undefined : lang,
view: selectedView === "all" ? undefined : Number(selectedView),
limit,
})
},
meta: {

View File

@ -6,6 +6,7 @@ import { useScrollElementUpdate } from "@follow/components/ui/scroll-area/hooks.
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/EllipsisWithTooltip.js"
import { CategoryMap, RSSHubCategories } from "@follow/constants"
import { cn, formatNumber } from "@follow/utils/utils"
import type { RSSHubAnalyticsResponse, RSSHubNamespace } from "@follow-app/client-sdk"
import { keepPreviousData } from "@tanstack/react-query"
import { memo, useCallback, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
@ -15,7 +16,6 @@ import { useUISettingKey } from "~/atoms/settings/ui"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useFollow } from "~/hooks/biz/useFollow"
import { useAuthQuery } from "~/hooks/common"
import type { apiClient } from "~/lib/api-fetch"
import { useSubViewTitle } from "~/modules/app-layout/subview/hooks"
import { RecommendationContent } from "~/modules/discover/RecommendationContent"
import { FeedIcon } from "~/modules/feed/feed-icon"
@ -28,8 +28,6 @@ const LanguageMap = {
cmn: "zh-CN",
} as const
type RouteData = Awaited<ReturnType<typeof apiClient.discover.rsshub.$get>>["data"]
export const Component = () => {
const { t } = useTranslation()
const lang = useUISettingKey("discoverLanguage")
@ -44,26 +42,23 @@ export const Component = () => {
}),
{
staleTime: 1000 * 60 * 60 * 24, // 1 day
placeholderData: keepPreviousData,
placeholderData: keepPreviousData as any as Record<string, RSSHubNamespace>,
meta: {
persist: true,
},
},
)
const data: RouteData = rsshubPopular.data as any
const { data } = rsshubPopular
const rsshubAnalytics = useAuthQuery(Queries.discover.rsshubAnalytics({ lang }), {
staleTime: 1000 * 60 * 60 * 24, // 1 day
placeholderData: keepPreviousData,
placeholderData: keepPreviousData as any as RSSHubAnalyticsResponse,
meta: {
persist: true,
},
})
const rsshubAnalyticsData: Awaited<
ReturnType<(typeof apiClient)["discover"]["rsshub-analytics"]["$get"]>
>["data"] = rsshubAnalytics.data as any
const { data: rsshubAnalyticsData } = rsshubAnalytics
const isLoading = rsshubPopular.isLoading || rsshubAnalytics.isLoading
@ -186,11 +181,9 @@ const RecommendationListItem = memo(
routePrefix,
rsshubAnalyticsData,
}: {
data: RouteData[string]
data: RSSHubNamespace
routePrefix: string
rsshubAnalyticsData: Awaited<
ReturnType<(typeof apiClient)["discover"]["rsshub-analytics"]["$get"]>
>["data"]
rsshubAnalyticsData: RSSHubAnalyticsResponse | undefined
}) => {
const { t } = useTranslation()
const { present } = useModalStack()
@ -377,7 +370,7 @@ const RouteItem = memo(
{analytics.topFeeds.slice(0, 2).map((feed: any) => (
<div key={feed.id} className="flex w-2/5 flex-1 items-center text-sm">
<FeedIcon
feed={feed}
target={feed}
className="mask-squircle mask shrink-0 rounded-none"
size={16}
/>

View File

@ -1,9 +1,9 @@
import { Logo } from "@follow/components/icons/logo.jsx"
import { Button } from "@follow/components/ui/button/index.js"
import { RSSHubLogo } from "@follow/components/ui/platform-icon/icons.js"
import type { RSSHubModel } from "@follow/models"
import { whoami } from "@follow/store/user/getters"
import { cn, formatNumber } from "@follow/utils/utils"
import type { RSSHubListItem } from "@follow-app/client-sdk"
import { memo, useCallback, useEffect } from "react"
import { useTranslation } from "react-i18next"
@ -80,7 +80,7 @@ export function Component() {
)
}
type InstanceItem = RSSHubModel | { id: string; isOfficial: true }
type InstanceItem = RSSHubListItem | { id: string; isOfficial: true }
const InstanceCard = memo(({ item }: { item: InstanceItem }) => {
const { t } = useTranslation("settings")
@ -91,7 +91,7 @@ const InstanceCard = memo(({ item }: { item: InstanceItem }) => {
const { present } = useModalStack()
const isOfficial = "isOfficial" in item && item.isOfficial
const instance = isOfficial ? ({} as Partial<RSSHubModel>) : (item as RSSHubModel)
const instance = isOfficial ? ({} as Partial<RSSHubListItem>) : (item as RSSHubListItem)
const isInUse = isOfficial
? !status?.data?.usage?.rsshubId
@ -198,7 +198,7 @@ const InstanceCard = memo(({ item }: { item: InstanceItem }) => {
present({
title: t("rsshub.table.edit"),
content: ({ dismiss }) => (
<AddModalContent dismiss={dismiss} instance={instance as RSSHubModel} />
<AddModalContent dismiss={dismiss} instance={instance as RSSHubListItem} />
),
})
}
@ -237,7 +237,7 @@ const InstanceCard = memo(({ item }: { item: InstanceItem }) => {
)}
</div>
) : (
<SelectInstanceButton instance={instance as RSSHubModel} />
<SelectInstanceButton instance={instance as RSSHubListItem} />
)}
</div>
</div>
@ -245,7 +245,7 @@ const InstanceCard = memo(({ item }: { item: InstanceItem }) => {
)
})
function List({ data }: { data?: RSSHubModel[] }) {
function List({ data }: { data?: RSSHubListItem[] }) {
const status = useAuthQuery(Queries.rsshub.status())
const sortedData: InstanceItem[] = [
@ -298,7 +298,7 @@ function List({ data }: { data?: RSSHubModel[] }) {
)
}
function SelectInstanceButton({ instance }: { instance: RSSHubModel }) {
function SelectInstanceButton({ instance }: { instance: RSSHubListItem }) {
const { t } = useTranslation("settings")
const { present } = useModalStack()
const status = useAuthQuery(Queries.rsshub.status())

View File

@ -14,7 +14,7 @@ export const UserProvider = () => {
useEffect(() => {
if (!session?.user) return
// @ts-expect-error FIXME
setIntegrationIdentify(session.user)
}, [session?.user])

View File

@ -3,7 +3,7 @@ import { initializeApp } from "firebase/app"
import { getMessaging, getToken } from "firebase/messaging"
import { setAppMessagingToken } from "./atoms/app"
import { apiClient } from "./lib/api-fetch"
import { followClient } from "./lib/api-client"
import { router } from "./router"
const firebaseConfig = env.VITE_FIREBASE_CONFIG ? JSON.parse(env.VITE_FIREBASE_CONFIG) : null
@ -13,7 +13,7 @@ export async function registerWebPushNotifications() {
return
}
try {
const actions = await apiClient.actions.$get()
const actions = await followClient.api.actions.get()
const rules = actions.data?.rules
const hasPushNotificationRule = rules?.some(
(rule) => rule.result.newEntryNotification && !rule.result.disabled,
@ -44,11 +44,9 @@ export async function registerWebPushNotifications() {
serviceWorkerRegistration: registration,
})
await apiClient.messaging.$post({
json: {
token,
channel: "web",
},
await followClient.api.messaging.createToken({
token,
channel: "web",
})
registerPushNotificationPostMessage()

View File

@ -1,4 +1,3 @@
import type { AuthSession } from "@follow/shared/hono"
import { whoamiQueryKey } from "@follow/store/user/hooks"
import { userSyncService } from "@follow/store/user/store"
import { tracker } from "@follow/tracker"
@ -58,18 +57,35 @@ export const useSession = (options?: { enabled?: boolean }) => {
const { error } = rest
const fetchError = error as FetchError
const getAuthStatus = ():
| "loading"
| "authenticated"
| "error"
| "unauthenticated"
| "unknown" => {
if (isLoading) {
return "loading"
}
if (fetchError) {
return "error"
}
if (data) {
return "authenticated"
}
if (data === null) {
return "unauthenticated"
}
return "unknown"
}
return {
session: data as AuthSession,
session: data,
...rest,
status: isLoading
? "loading"
: data
? "authenticated"
: fetchError
? "error"
: data === null
? "unauthenticated"
: "unknown",
status: getAuthStatus(),
} as const
}

View File

@ -1,4 +1,4 @@
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
export const discover = {
@ -11,40 +11,38 @@ export const discover = {
categories?: string
lang?: string
}) =>
defineQuery(["discover", "rsshub", "category", category, categories, lang], async () => {
const res = await apiClient.discover.rsshub.$get({
query: {
defineQuery(
["discover", "rsshub", "category", category, categories, lang],
async () => {
const res = await followClient.api.discover.rsshub({
category,
categories,
...(lang !== "all" && { lang }),
},
})
return res.data
}),
})
return res.data
},
{
rootKey: ["discover", "rsshub", "category"],
},
),
rsshubNamespace: ({ namespace }: { namespace: string }) =>
defineQuery(["discover", "rsshub", "namespace", namespace], async () => {
const res = await apiClient.discover.rsshub.$get({
query: {
namespace,
},
const res = await followClient.api.discover.rsshub({
namespace,
})
return res.data
}),
rsshubRoute: ({ route }: { route: string }) =>
defineQuery(["discover", "rsshub", "route", route], async () => {
const res = await apiClient.discover.rsshub.route.$get({
query: {
route,
},
const res = await followClient.api.discover.rsshubRoute({
route,
})
return res.data
}),
rsshubAnalytics: ({ lang }: { lang?: string }) =>
defineQuery(["discover", "rsshub", "analytics", lang], async () => {
const res = await apiClient.discover["rsshub-analytics"].$get({
query: {
...(lang !== "all" && { lang }),
},
const res = await followClient.api.discover.rsshubAnalytics({
...(lang !== "all" && { lang }),
})
return res.data
}),

View File

@ -1,5 +1,4 @@
import { useAuthQuery } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import { getEntriesParams } from "~/lib/utils"
@ -8,10 +7,8 @@ export const entries = {
defineQuery(
["entries-preview", id],
async () => {
const res = await apiClient.entries.preview.$get({
query: {
id,
},
const res = await followClient.api.entries.preview({
id,
})
return res.data
@ -54,15 +51,13 @@ export const entries = {
query.feedId = query.feedIdList[0]
delete query.feedIdList
}
return apiClient.entries["check-new"].$get({
query: {
insertedAfter: query.insertedAfter,
view: query.view,
feedId: query.feedId,
read: typeof query.read === "boolean" ? JSON.stringify(query.read) : undefined,
feedIdList: query.feedIdList,
},
}) as Promise<{ data: { has_new: boolean; lastest_at?: string } }>
return followClient.api.entries.checkNew({
insertedAfter: query.insertedAfter,
view: query.view,
feedId: query.feedId,
read: typeof query.read === "boolean" ? query.read : undefined,
feedIdList: query.feedIdList,
})
},
{
@ -70,8 +65,3 @@ export const entries = {
},
),
}
export const useEntriesPreview = ({ id }: { id?: string }) =>
useAuthQuery(entries.preview(id!), {
enabled: !!id,
})

View File

@ -8,7 +8,7 @@ import { toast } from "sonner"
import { ROUTE_FEED_IN_FOLDER, ROUTE_FEED_PENDING } from "~/constants"
import { useAuthQuery } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import { toastFetchError } from "~/lib/error-parser"
@ -29,7 +29,7 @@ export const feed = {
),
claimMessage: ({ feedId }: { feedId: string }) =>
defineQuery(["feed", "claimMessage", feedId], async () =>
apiClient.feeds.claim.message.$get({ query: { feedId } }).then((res) => {
followClient.api.feeds.claim.message({ feedId }).then((res) => {
res.data.json = JSON.stringify(JSON.parse(res.data.json), null, 2)
const $document = new DOMParser().parseFromString(res.data.xml, "text/xml")
res.data.xml = formatXml(new XMLSerializer().serializeToString($document))
@ -38,7 +38,7 @@ export const feed = {
),
claimedList: () =>
defineQuery(["feed", "claimedList"], async () => {
const res = await apiClient.feeds.claim.list.$get()
const res = await followClient.api.feeds.claim.list()
return res.data
}),
}
@ -73,7 +73,7 @@ export const useClaimFeedMutation = (feedId: string) =>
export const useRefreshFeedMutation = (feedId?: string) =>
useMutation({
mutationKey: ["refreshFeed", feedId],
mutationFn: () => apiClient.feeds.refresh.$get({ query: { id: feedId! } }),
mutationFn: () => followClient.api.feeds.refresh({ id: feedId! }),
async onError(err) {
toastFetchError(err)
},
@ -86,7 +86,7 @@ export const useResetFeed = () => {
return useMutation({
mutationFn: async (feedId: string) => {
toastIDRef.current = toast.loading(t("sidebar.feed_actions.resetting_feed"))
await apiClient.feeds.reset.$get({ query: { id: feedId } })
await followClient.api.feeds.reset({ id: feedId })
},
onSuccess: () => {
toast.success(

View File

@ -1,13 +1,13 @@
import { useMutation } from "@tanstack/react-query"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import type { MutationBaseProps } from "./types"
export const useInvitationMutation = ({ onError }: MutationBaseProps = {}) =>
useMutation({
mutationFn: (code: string) => apiClient.invitations.use.$post({ json: { code } }),
mutationFn: (code: string) => followClient.api.invitations.use({ code }),
onError: (error) => {
onError?.(error)
@ -17,13 +17,13 @@ export const useInvitationMutation = ({ onError }: MutationBaseProps = {}) =>
export const invitations = {
list: () =>
defineQuery(["invitations"], async () => {
const res = await apiClient.invitations.$get()
const res = await followClient.api.invitations.list()
return res.data
}),
limitation: () =>
defineQuery(["invitations", "limitation"], async () => {
const res = await apiClient.invitations.limitation.$get()
const res = await followClient.api.invitations.getLimitation()
return res.data
}),
}

View File

@ -1,12 +1,12 @@
import { useMutation } from "@tanstack/react-query"
import { useAuthQuery } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
export const messaging = {
list: () =>
defineQuery(["messaging"], () => apiClient.messaging.$get(), {
defineQuery(["messaging"], () => followClient.api.messaging.getTokens(), {
rootKey: ["messaging"],
}),
}
@ -16,9 +16,5 @@ export const useMessaging = () => useAuthQuery(messaging.list())
export const useTestMessaging = () =>
useMutation({
mutationFn: ({ channel }: { channel: string }) =>
apiClient.messaging.test.$get({
query: {
channel,
},
}),
followClient.api.messaging.testNotification({ channel }),
})

View File

@ -1,5 +1,5 @@
import { useAuthQuery } from "~/hooks/common/useBizQuery"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
export const referral = {
@ -7,7 +7,7 @@ export const referral = {
defineQuery(
["referral"],
async () => {
const res = await apiClient.referrals.$get()
const res = await followClient.api.referrals.getReferrals()
return res.data
},
{

View File

@ -1,7 +1,8 @@
import { userActions } from "@follow/store/user/store"
import type { RSSHubUseRequest } from "@follow-app/client-sdk"
import { useMutation } from "@tanstack/react-query"
import { apiClient } from "~/lib/api-fetch"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import { toastFetchError } from "~/lib/error-parser"
@ -9,8 +10,7 @@ import type { MutationBaseProps } from "./types"
export const useSetRSSHubMutation = ({ onError }: MutationBaseProps = {}) =>
useMutation({
mutationFn: (data: { id: string | null; durationInMonths?: number; TOTPCode?: string }) =>
apiClient.rsshub.use.$post({ json: data }),
mutationFn: (data: RSSHubUseRequest) => followClient.api.rsshub.use({ ...data }),
onSuccess: (_, variables) => {
rsshub.list().invalidate()
@ -38,12 +38,10 @@ export const useAddRSSHubMutation = ({ onError }: MutationBaseProps = {}) =>
accessKey?: string
id?: string
}) =>
apiClient.rsshub.$post({
json: {
baseUrl,
accessKey,
id,
},
followClient.api.rsshub.create({
baseUrl,
accessKey,
id,
}),
onSuccess: (_) => {
@ -59,7 +57,7 @@ export const useAddRSSHubMutation = ({ onError }: MutationBaseProps = {}) =>
export const useDeleteRSSHubMutation = ({ onError }: MutationBaseProps = {}) =>
useMutation({
mutationFn: (id: string) => apiClient.rsshub.$delete({ json: { id } }),
mutationFn: (id: string) => followClient.api.rsshub.delete({ id }),
onError: (error) => {
onError?.(error)
@ -70,13 +68,13 @@ export const useDeleteRSSHubMutation = ({ onError }: MutationBaseProps = {}) =>
export const rsshub = {
get: ({ id }: { id: string }) =>
defineQuery(["rsshub", "get", id], async () => {
const res = await apiClient.rsshub.$get({ query: { id } })
const res = await followClient.api.rsshub.get({ id })
return res.data
}),
list: () =>
defineQuery(["rsshub", "list"], async () => {
const res = await apiClient.rsshub.list.$get()
const res = await followClient.api.rsshub.list()
userActions.upsertMany(res.data.map((item) => item.owner).filter((item) => item !== null))
return res.data
@ -84,7 +82,7 @@ export const rsshub = {
status: () =>
defineQuery(["rsshub", "status"], async () => {
const res = await apiClient.rsshub.status.$get()
const res = await followClient.api.rsshub.status()
return res.data
}),
}

Some files were not shown because too many files have changed in this diff Show More