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:
parent
1e2835faed
commit
d4b1333617
|
|
@ -46,7 +46,6 @@ updates:
|
|||
patterns:
|
||||
- immer
|
||||
- re-resizable
|
||||
- hono
|
||||
- electron-context-menu
|
||||
- "@mozilla/readability"
|
||||
- daisyui
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
packages/internal/shared/src/hono.ts
|
||||
pnpm-lock.yaml
|
||||
|
||||
CHANGELOG.md
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ const AIDisplayFeedPartBase = ({
|
|||
<div className="flex">
|
||||
<FeedIcon
|
||||
disableFadeIn
|
||||
feed={{
|
||||
target={{
|
||||
type: "feed",
|
||||
title,
|
||||
id: feedId,
|
||||
|
||||
image,
|
||||
siteUrl,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -55,10 +55,9 @@ const Item = memo(
|
|||
<div className="flex">
|
||||
<FeedIcon
|
||||
disableFadeIn
|
||||
feed={{
|
||||
target={{
|
||||
type: "feed",
|
||||
title,
|
||||
id: feedId,
|
||||
image,
|
||||
siteUrl,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
@ -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]">
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { RSSHubParameter, RSSHubParameterObject } from "./types"
|
||||
import type { RSSHubParameter, RSSHubParameterObject } from "@follow/models/rsshub"
|
||||
|
||||
export const normalizeRSSHubParameters = (
|
||||
parameters: RSSHubParameter,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>>({})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 || []} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 || []) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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> & {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const UserProvider = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!session?.user) return
|
||||
// @ts-expect-error FIXME
|
||||
|
||||
setIntegrationIdentify(session.user)
|
||||
}, [session?.user])
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue