refactor(web): drop swr and migrate share/chat hooks to tanstack query (#30232)

Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
yyh 2025-12-29 14:04:01 +08:00 committed by GitHub
parent 0b1439fee4
commit 09be869f58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 984 additions and 105 deletions

View File

@ -187,7 +187,7 @@ const Template = useMemo(() => {
**When**: Component directly handles API calls, data transformation, or complex async operations.
**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks. Project is migrating from SWR to React Query.
**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks.
```typescript
// ❌ Before: API logic in component

View File

@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import * as React from 'react'
import { AppInitializer } from '@/app/components/app-initializer'
import AmplitudeProvider from '@/app/components/base/amplitude'
import GA, { GaType } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk'
@ -7,7 +8,6 @@ import GotoAnything from '@/app/components/goto-anything'
import Header from '@/app/components/header'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import SwrInitializer from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ModalContextProvider } from '@/context/modal-context'
@ -20,7 +20,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitializer>
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
@ -38,7 +38,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</EventEmitterContextProvider>
</AppContextProvider>
<Zendesk />
</SwrInitializer>
</AppInitializer>
</>
)
}

View File

@ -1,9 +1,9 @@
import type { ReactNode } from 'react'
import * as React from 'react'
import { AppInitializer } from '@/app/components/app-initializer'
import AmplitudeProvider from '@/app/components/base/amplitude'
import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import SwrInitor from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ModalContextProvider } from '@/context/modal-context'
@ -15,7 +15,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitor>
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
@ -30,7 +30,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</SwrInitor>
</AppInitializer>
</>
)
}

View File

@ -3,7 +3,6 @@
import type { ReactNode } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
import { SWRConfig } from 'swr'
import {
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
@ -11,12 +10,13 @@ import {
import { fetchSetupStatus } from '@/service/common'
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
type SwrInitializerProps = {
type AppInitializerProps = {
children: ReactNode
}
const SwrInitializer = ({
export const AppInitializer = ({
children,
}: SwrInitializerProps) => {
}: AppInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
// Tokens are now stored in cookies, no need to check localStorage
@ -69,20 +69,5 @@ const SwrInitializer = ({
})()
}, [isSetupFinished, router, pathname, searchParams])
return init
? (
<SWRConfig value={{
shouldRetryOnError: false,
revalidateOnFocus: false,
dedupingInterval: 60000,
focusThrottleInterval: 5000,
provider: () => new Map(),
}}
>
{children}
</SWRConfig>
)
: null
return init ? children : null
}
export default SwrInitializer

View File

@ -0,0 +1,270 @@
import type { ReactNode } from 'react'
import type { ChatConfig } from '../types'
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook, waitFor } from '@testing-library/react'
import { ToastProvider } from '@/app/components/base/toast'
import {
fetchChatList,
fetchConversations,
generationConversationName,
} from '@/service/share'
import { shareQueryKeys } from '@/service/use-share'
import { CONVERSATION_ID_INFO } from '../constants'
import { useChatWithHistory } from './hooks'
vi.mock('@/hooks/use-app-favicon', () => ({
useAppFavicon: vi.fn(),
}))
vi.mock('@/i18n-config/i18next-config', () => ({
changeLanguage: vi.fn().mockResolvedValue(undefined),
}))
const mockStoreState: {
appInfo: AppData | null
appMeta: AppMeta | null
appParams: ChatConfig | null
} = {
appInfo: null,
appMeta: null,
appParams: null,
}
const useWebAppStoreMock = vi.fn((selector?: (state: typeof mockStoreState) => unknown) => {
return selector ? selector(mockStoreState) : mockStoreState
})
vi.mock('@/context/web-app-context', () => ({
useWebAppStore: (selector?: (state: typeof mockStoreState) => unknown) => useWebAppStoreMock(selector),
}))
vi.mock('../utils', async () => {
const actual = await vi.importActual<typeof import('../utils')>('../utils')
return {
...actual,
getProcessedSystemVariablesFromUrlParams: vi.fn().mockResolvedValue({ user_id: 'user-1' }),
getRawInputsFromUrlParams: vi.fn().mockResolvedValue({}),
getRawUserVariablesFromUrlParams: vi.fn().mockResolvedValue({}),
}
})
vi.mock('@/service/share', () => ({
fetchChatList: vi.fn(),
fetchConversations: vi.fn(),
generationConversationName: vi.fn(),
fetchAppInfo: vi.fn(),
fetchAppMeta: vi.fn(),
fetchAppParams: vi.fn(),
getAppAccessModeByAppCode: vi.fn(),
delConversation: vi.fn(),
pinConversation: vi.fn(),
renameConversation: vi.fn(),
unpinConversation: vi.fn(),
updateFeedback: vi.fn(),
}))
const mockFetchConversations = vi.mocked(fetchConversations)
const mockFetchChatList = vi.mocked(fetchChatList)
const mockGenerationConversationName = vi.mocked(generationConversationName)
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const createWrapper = (queryClient: QueryClient) => {
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
<ToastProvider>{children}</ToastProvider>
</QueryClientProvider>
)
}
const renderWithClient = <T,>(hook: () => T) => {
const queryClient = createQueryClient()
const wrapper = createWrapper(queryClient)
return {
queryClient,
...renderHook(hook, { wrapper }),
}
}
const createConversationItem = (overrides: Partial<ConversationItem> = {}): ConversationItem => ({
id: 'conversation-1',
name: 'Conversation 1',
inputs: null,
introduction: '',
...overrides,
})
const createConversationData = (overrides: Partial<AppConversationData> = {}): AppConversationData => ({
data: [createConversationItem()],
has_more: false,
limit: 100,
...overrides,
})
const setConversationIdInfo = (appId: string, conversationId: string) => {
const value = {
[appId]: {
'user-1': conversationId,
'DEFAULT': conversationId,
},
}
localStorage.setItem(CONVERSATION_ID_INFO, JSON.stringify(value))
}
// Scenario: useChatWithHistory integrates share queries for conversations and chat list.
describe('useChatWithHistory', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.removeItem(CONVERSATION_ID_INFO)
mockStoreState.appInfo = {
app_id: 'app-1',
custom_config: null,
site: {
title: 'Test App',
default_language: 'en-US',
},
}
mockStoreState.appMeta = {
tool_icons: {},
}
mockStoreState.appParams = null
setConversationIdInfo('app-1', 'conversation-1')
})
afterEach(() => {
localStorage.removeItem(CONVERSATION_ID_INFO)
})
// Scenario: share query results populate conversation lists and trigger chat list fetch.
describe('Share queries', () => {
it('should load pinned, unpinned, and chat list data from share queries', async () => {
// Arrange
const pinnedData = createConversationData({
data: [createConversationItem({ id: 'pinned-1', name: 'Pinned' })],
})
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
mockFetchConversations.mockImplementation(async (_isInstalledApp, _appId, _lastId, pinned) => {
return pinned ? pinnedData : listData
})
mockFetchChatList.mockResolvedValue({ data: [] })
// Act
const { result } = renderWithClient(() => useChatWithHistory())
// Assert
await waitFor(() => {
expect(mockFetchConversations).toHaveBeenCalledWith(false, 'app-1', undefined, true, 100)
})
await waitFor(() => {
expect(mockFetchConversations).toHaveBeenCalledWith(false, 'app-1', undefined, false, 100)
})
await waitFor(() => {
expect(mockFetchChatList).toHaveBeenCalledWith('conversation-1', false, 'app-1')
})
expect(result.current.pinnedConversationList).toEqual(pinnedData.data)
expect(result.current.conversationList).toEqual(listData.data)
})
})
// Scenario: completion invalidates share caches and merges generated names.
describe('New conversation completion', () => {
it('should invalidate share conversations and apply generated name', async () => {
// Arrange
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
const generatedConversation = createConversationItem({
id: 'conversation-new',
name: 'Generated',
})
mockFetchConversations.mockResolvedValue(listData)
mockFetchChatList.mockResolvedValue({ data: [] })
mockGenerationConversationName.mockResolvedValue(generatedConversation)
const { result, queryClient } = renderWithClient(() => useChatWithHistory())
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
// Act
act(() => {
result.current.handleNewConversationCompleted('conversation-new')
})
// Assert
await waitFor(() => {
expect(mockGenerationConversationName).toHaveBeenCalledWith(false, 'app-1', 'conversation-new')
})
await waitFor(() => {
expect(result.current.conversationList[0]).toEqual(generatedConversation)
})
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: shareQueryKeys.conversations })
})
})
// Scenario: chat list queries stop when reload key is cleared.
describe('Chat list gating', () => {
it('should not refetch chat list when newConversationId matches current conversation', async () => {
// Arrange
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
mockFetchConversations.mockResolvedValue(listData)
mockFetchChatList.mockResolvedValue({ data: [] })
mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-1' }))
const { result } = renderWithClient(() => useChatWithHistory())
await waitFor(() => {
expect(mockFetchChatList).toHaveBeenCalledTimes(1)
})
// Act
act(() => {
result.current.handleNewConversationCompleted('conversation-1')
})
// Assert
await waitFor(() => {
expect(result.current.chatShouldReloadKey).toBe('')
})
expect(mockFetchChatList).toHaveBeenCalledTimes(1)
})
})
// Scenario: conversation id updates persist to localStorage.
describe('Conversation id persistence', () => {
it('should store new conversation id in localStorage after completion', async () => {
// Arrange
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
mockFetchConversations.mockResolvedValue(listData)
mockFetchChatList.mockResolvedValue({ data: [] })
mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-new' }))
const { result } = renderWithClient(() => useChatWithHistory())
// Act
act(() => {
result.current.handleNewConversationCompleted('conversation-new')
})
// Assert
await waitFor(() => {
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
const parsed = storedValue ? JSON.parse(storedValue) : {}
const storedUserId = parsed['app-1']?.['user-1']
const storedDefaultId = parsed['app-1']?.DEFAULT
expect([storedUserId, storedDefaultId]).toContain('conversation-new')
})
})
})
})

View File

@ -20,7 +20,6 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import { useToastContext } from '@/app/components/base/toast'
import { InputVarType } from '@/app/components/workflow/types'
@ -29,14 +28,17 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
import { changeLanguage } from '@/i18n-config/i18next-config'
import {
delConversation,
fetchChatList,
fetchConversations,
generationConversationName,
pinConversation,
renameConversation,
unpinConversation,
updateFeedback,
} from '@/service/share'
import {
useInvalidateShareConversations,
useShareChatList,
useShareConversationName,
useShareConversations,
} from '@/service/use-share'
import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { CONVERSATION_ID_INFO } from '../constants'
@ -174,21 +176,42 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
return currentConversationId
}, [currentConversationId, newConversationId])
const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(
appId ? ['appConversationData', isInstalledApp, appId, true] : null,
() => fetchConversations(isInstalledApp, appId, undefined, true, 100),
{ revalidateOnFocus: false, revalidateOnReconnect: false },
)
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(
appId ? ['appConversationData', isInstalledApp, appId, false] : null,
() => fetchConversations(isInstalledApp, appId, undefined, false, 100),
{ revalidateOnFocus: false, revalidateOnReconnect: false },
)
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(
chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null,
() => fetchChatList(chatShouldReloadKey, isInstalledApp, appId),
{ revalidateOnFocus: false, revalidateOnReconnect: false },
)
const { data: appPinnedConversationData } = useShareConversations({
isInstalledApp,
appId,
pinned: true,
limit: 100,
}, {
enabled: !!appId,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
const {
data: appConversationData,
isLoading: appConversationDataLoading,
} = useShareConversations({
isInstalledApp,
appId,
pinned: false,
limit: 100,
}, {
enabled: !!appId,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
const {
data: appChatListData,
isLoading: appChatListDataLoading,
} = useShareChatList({
conversationId: chatShouldReloadKey,
isInstalledApp,
appId,
}, {
enabled: !!chatShouldReloadKey,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
const invalidateShareConversations = useInvalidateShareConversations()
const [clearChatList, setClearChatList] = useState(false)
const [isResponding, setIsResponding] = useState(false)
@ -309,7 +332,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
handleNewConversationInputsChange(conversationInputs)
}, [handleNewConversationInputsChange, inputsForms])
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
const { data: newConversation } = useShareConversationName({
conversationId: newConversationId,
isInstalledApp,
appId,
}, {
refetchOnWindowFocus: false,
})
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
useEffect(() => {
if (appConversationData?.data && !appConversationDataLoading)
@ -429,9 +458,8 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setClearChatList(true)
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList, inputsForms])
const handleUpdateConversationList = useCallback(() => {
mutateAppConversationData()
mutateAppPinnedConversationData()
}, [mutateAppConversationData, mutateAppPinnedConversationData])
invalidateShareConversations()
}, [invalidateShareConversations])
const handlePinConversation = useCallback(async (conversationId: string) => {
await pinConversation(isInstalledApp, appId, conversationId)
@ -518,8 +546,8 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setNewConversationId(newConversationId)
handleConversationIdInfoChange(newConversationId)
setShowNewConversationItemInList(false)
mutateAppConversationData()
}, [mutateAppConversationData, handleConversationIdInfoChange])
invalidateShareConversations()
}, [handleConversationIdInfoChange, invalidateShareConversations])
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)

View File

@ -0,0 +1,257 @@
import type { ReactNode } from 'react'
import type { ChatConfig } from '../types'
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook, waitFor } from '@testing-library/react'
import { ToastProvider } from '@/app/components/base/toast'
import {
fetchChatList,
fetchConversations,
generationConversationName,
} from '@/service/share'
import { shareQueryKeys } from '@/service/use-share'
import { CONVERSATION_ID_INFO } from '../constants'
import { useEmbeddedChatbot } from './hooks'
vi.mock('@/i18n-config/i18next-config', () => ({
changeLanguage: vi.fn().mockResolvedValue(undefined),
}))
const mockStoreState: {
appInfo: AppData | null
appMeta: AppMeta | null
appParams: ChatConfig | null
embeddedConversationId: string | null
embeddedUserId: string | null
} = {
appInfo: null,
appMeta: null,
appParams: null,
embeddedConversationId: null,
embeddedUserId: null,
}
const useWebAppStoreMock = vi.fn((selector?: (state: typeof mockStoreState) => unknown) => {
return selector ? selector(mockStoreState) : mockStoreState
})
vi.mock('@/context/web-app-context', () => ({
useWebAppStore: (selector?: (state: typeof mockStoreState) => unknown) => useWebAppStoreMock(selector),
}))
vi.mock('../utils', async () => {
const actual = await vi.importActual<typeof import('../utils')>('../utils')
return {
...actual,
getProcessedInputsFromUrlParams: vi.fn().mockResolvedValue({}),
getProcessedSystemVariablesFromUrlParams: vi.fn().mockResolvedValue({}),
getProcessedUserVariablesFromUrlParams: vi.fn().mockResolvedValue({}),
}
})
vi.mock('@/service/share', () => ({
fetchChatList: vi.fn(),
fetchConversations: vi.fn(),
generationConversationName: vi.fn(),
fetchAppInfo: vi.fn(),
fetchAppMeta: vi.fn(),
fetchAppParams: vi.fn(),
getAppAccessModeByAppCode: vi.fn(),
updateFeedback: vi.fn(),
}))
const mockFetchConversations = vi.mocked(fetchConversations)
const mockFetchChatList = vi.mocked(fetchChatList)
const mockGenerationConversationName = vi.mocked(generationConversationName)
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const createWrapper = (queryClient: QueryClient) => {
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
<ToastProvider>{children}</ToastProvider>
</QueryClientProvider>
)
}
const renderWithClient = <T,>(hook: () => T) => {
const queryClient = createQueryClient()
const wrapper = createWrapper(queryClient)
return {
queryClient,
...renderHook(hook, { wrapper }),
}
}
const createConversationItem = (overrides: Partial<ConversationItem> = {}): ConversationItem => ({
id: 'conversation-1',
name: 'Conversation 1',
inputs: null,
introduction: '',
...overrides,
})
const createConversationData = (overrides: Partial<AppConversationData> = {}): AppConversationData => ({
data: [createConversationItem()],
has_more: false,
limit: 100,
...overrides,
})
// Scenario: useEmbeddedChatbot integrates share queries for conversations and chat list.
describe('useEmbeddedChatbot', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.removeItem(CONVERSATION_ID_INFO)
mockStoreState.appInfo = {
app_id: 'app-1',
custom_config: null,
site: {
title: 'Test App',
default_language: 'en-US',
},
}
mockStoreState.appMeta = {
tool_icons: {},
}
mockStoreState.appParams = null
mockStoreState.embeddedConversationId = 'conversation-1'
mockStoreState.embeddedUserId = 'embedded-user-1'
})
afterEach(() => {
localStorage.removeItem(CONVERSATION_ID_INFO)
})
// Scenario: share query results populate conversation lists and trigger chat list fetch.
describe('Share queries', () => {
it('should load pinned, unpinned, and chat list data from share queries', async () => {
// Arrange
const pinnedData = createConversationData({
data: [createConversationItem({ id: 'pinned-1', name: 'Pinned' })],
})
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
mockFetchConversations.mockImplementation(async (_isInstalledApp, _appId, _lastId, pinned) => {
return pinned ? pinnedData : listData
})
mockFetchChatList.mockResolvedValue({ data: [] })
// Act
const { result } = renderWithClient(() => useEmbeddedChatbot())
// Assert
await waitFor(() => {
expect(mockFetchConversations).toHaveBeenCalledWith(false, 'app-1', undefined, true, 100)
})
await waitFor(() => {
expect(mockFetchConversations).toHaveBeenCalledWith(false, 'app-1', undefined, false, 100)
})
await waitFor(() => {
expect(mockFetchChatList).toHaveBeenCalledWith('conversation-1', false, 'app-1')
})
expect(result.current.pinnedConversationList).toEqual(pinnedData.data)
expect(result.current.conversationList).toEqual(listData.data)
})
})
// Scenario: completion invalidates share caches and merges generated names.
describe('New conversation completion', () => {
it('should invalidate share conversations and apply generated name', async () => {
// Arrange
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
const generatedConversation = createConversationItem({
id: 'conversation-new',
name: 'Generated',
})
mockFetchConversations.mockResolvedValue(listData)
mockFetchChatList.mockResolvedValue({ data: [] })
mockGenerationConversationName.mockResolvedValue(generatedConversation)
const { result, queryClient } = renderWithClient(() => useEmbeddedChatbot())
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
// Act
act(() => {
result.current.handleNewConversationCompleted('conversation-new')
})
// Assert
await waitFor(() => {
expect(mockGenerationConversationName).toHaveBeenCalledWith(false, 'app-1', 'conversation-new')
})
await waitFor(() => {
expect(result.current.conversationList[0]).toEqual(generatedConversation)
})
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: shareQueryKeys.conversations })
})
})
// Scenario: chat list queries stop when reload key is cleared.
describe('Chat list gating', () => {
it('should not refetch chat list when newConversationId matches current conversation', async () => {
// Arrange
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
mockFetchConversations.mockResolvedValue(listData)
mockFetchChatList.mockResolvedValue({ data: [] })
mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-1' }))
const { result } = renderWithClient(() => useEmbeddedChatbot())
await waitFor(() => {
expect(mockFetchChatList).toHaveBeenCalledTimes(1)
})
// Act
act(() => {
result.current.handleNewConversationCompleted('conversation-1')
})
// Assert
await waitFor(() => {
expect(result.current.chatShouldReloadKey).toBe('')
})
expect(mockFetchChatList).toHaveBeenCalledTimes(1)
})
})
// Scenario: conversation id updates persist to localStorage.
describe('Conversation id persistence', () => {
it('should store new conversation id in localStorage after completion', async () => {
// Arrange
const listData = createConversationData({
data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
})
mockFetchConversations.mockResolvedValue(listData)
mockFetchChatList.mockResolvedValue({ data: [] })
mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-new' }))
const { result } = renderWithClient(() => useEmbeddedChatbot())
// Act
act(() => {
result.current.handleNewConversationCompleted('conversation-new')
})
// Assert
await waitFor(() => {
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
const parsed = storedValue ? JSON.parse(storedValue) : {}
const storedUserId = parsed['app-1']?.['embedded-user-1']
const storedDefaultId = parsed['app-1']?.DEFAULT
expect([storedUserId, storedDefaultId]).toContain('conversation-new')
})
})
})
})

View File

@ -19,18 +19,18 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useToastContext } from '@/app/components/base/toast'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { InputVarType } from '@/app/components/workflow/types'
import { useWebAppStore } from '@/context/web-app-context'
import { changeLanguage } from '@/i18n-config/i18next-config'
import { updateFeedback } from '@/service/share'
import {
fetchChatList,
fetchConversations,
generationConversationName,
updateFeedback,
} from '@/service/share'
useInvalidateShareConversations,
useShareChatList,
useShareConversationName,
useShareConversations,
} from '@/service/use-share'
import { TransferMethod } from '@/types/app'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import { CONVERSATION_ID_INFO } from '../constants'
@ -137,9 +137,30 @@ export const useEmbeddedChatbot = () => {
return currentConversationId
}, [currentConversationId, newConversationId])
const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const { data: appPinnedConversationData } = useShareConversations({
isInstalledApp,
appId,
pinned: true,
limit: 100,
})
const {
data: appConversationData,
isLoading: appConversationDataLoading,
} = useShareConversations({
isInstalledApp,
appId,
pinned: false,
limit: 100,
})
const {
data: appChatListData,
isLoading: appChatListDataLoading,
} = useShareChatList({
conversationId: chatShouldReloadKey,
isInstalledApp,
appId,
})
const invalidateShareConversations = useInvalidateShareConversations()
const [clearChatList, setClearChatList] = useState(false)
const [isResponding, setIsResponding] = useState(false)
@ -259,7 +280,13 @@ export const useEmbeddedChatbot = () => {
handleNewConversationInputsChange(conversationInputs)
}, [handleNewConversationInputsChange, inputsForms])
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
const { data: newConversation } = useShareConversationName({
conversationId: newConversationId,
isInstalledApp,
appId,
}, {
refetchOnWindowFocus: false,
})
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
useEffect(() => {
if (appConversationData?.data && !appConversationDataLoading)
@ -379,8 +406,8 @@ export const useEmbeddedChatbot = () => {
setNewConversationId(newConversationId)
handleConversationIdInfoChange(newConversationId)
setShowNewConversationItemInList(false)
mutateAppConversationData()
}, [mutateAppConversationData, handleConversationIdInfoChange])
invalidateShareConversations()
}, [handleConversationIdInfoChange, invalidateShareConversations])
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)

View File

@ -76,7 +76,7 @@ const config: KnipConfig = {
// Browser initialization (runs on client startup)
'app/components/browser-initializer.tsx!',
'app/components/sentry-initializer.tsx!',
'app/components/swr-initializer.tsx!',
'app/components/app-initializer.tsx!',
// i18n initialization (server and client)
'app/components/i18n.tsx!',

View File

@ -138,7 +138,6 @@
"semver": "^7.7.3",
"sharp": "^0.33.5",
"sortablejs": "^1.15.6",
"swr": "^2.3.6",
"tailwind-merge": "^2.6.0",
"tldts": "^7.0.17",
"use-context-selector": "^2.0.0",

View File

@ -330,9 +330,6 @@ importers:
sortablejs:
specifier: ^1.15.6
version: 1.15.6
swr:
specifier: ^2.3.6
version: 2.3.7(react@19.2.3)
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
@ -8096,11 +8093,6 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
swr@2.3.7:
resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@ -17682,12 +17674,6 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
swr@2.3.7(react@19.2.3):
dependencies:
dequal: 2.0.3
react: 19.2.3
use-sync-external-store: 1.6.0(react@19.2.3)
symbol-tree@3.2.4: {}
synckit@0.11.11:

View File

@ -46,7 +46,6 @@ Features Detected:
${analysis.hasEvents ? '✓' : '✗'} Event handlers
${analysis.hasRouter ? '✓' : '✗'} Next.js routing
${analysis.hasAPI ? '✓' : '✗'} API calls
${analysis.hasSWR ? '✓' : '✗'} SWR data fetching
${analysis.hasReactQuery ? '✓' : '✗'} React Query
${analysis.hasAhooks ? '✓' : '✗'} ahooks
${analysis.hasForwardRef ? '✓' : '✗'} Ref forwarding (forwardRef)
@ -236,7 +235,7 @@ Create the test file at: ${testPath}
// ===== API Calls =====
if (analysis.hasAPI) {
guidelines.push('🌐 API calls detected:')
guidelines.push(' - Mock API calls/hooks (useSWR, useQuery, fetch, etc.)')
guidelines.push(' - Mock API calls/hooks (useQuery, useMutation, fetch, etc.)')
guidelines.push(' - Test loading, success, and error states')
guidelines.push(' - Focus on component behavior, not the data fetching lib')
}

View File

@ -21,6 +21,7 @@ export class ComponentAnalyzer {
const resolvedPath = absolutePath ?? path.resolve(process.cwd(), filePath)
const fileName = path.basename(filePath, path.extname(filePath))
const lineCount = code.split('\n').length
const hasReactQuery = /\buse(?:Query|Queries|InfiniteQuery|SuspenseQuery|SuspenseInfiniteQuery|Mutation)\b/.test(code)
// Calculate complexity metrics
const { total: rawComplexity, max: rawMaxComplexity } = this.calculateCognitiveComplexity(code)
@ -44,14 +45,13 @@ export class ComponentAnalyzer {
hasMemo: code.includes('useMemo'),
hasEvents: /on[A-Z]\w+/.test(code),
hasRouter: code.includes('useRouter') || code.includes('usePathname'),
hasAPI: code.includes('service/') || code.includes('fetch(') || code.includes('useSWR'),
hasAPI: code.includes('service/') || code.includes('fetch(') || hasReactQuery,
hasForwardRef: code.includes('forwardRef'),
hasComponentMemo: /React\.memo|memo\(/.test(code),
hasSuspense: code.includes('Suspense') || /\blazy\(/.test(code),
hasPortal: code.includes('createPortal'),
hasImperativeHandle: code.includes('useImperativeHandle'),
hasSWR: code.includes('useSWR'),
hasReactQuery: code.includes('useQuery') || code.includes('useMutation'),
hasReactQuery,
hasAhooks: code.includes('from \'ahooks\''),
complexity,
maxComplexity,

View File

@ -123,7 +123,6 @@ Usage: ${analysis.usageCount} reference${analysis.usageCount !== 1
${analysis.hasRouter ? '✓' : '✗'} Next.js routing
${analysis.hasAPI ? '✓' : '✗'} API calls
${analysis.hasReactQuery ? '✓' : '✗'} React Query
${analysis.hasSWR ? '✓' : '✗'} SWR (should migrate to React Query)
${analysis.hasAhooks ? '✓' : '✗'} ahooks
@ -150,7 +149,7 @@ ${this.buildRequirements(analysis)}
Follow Dify project conventions:
- Place extracted hooks in \`hooks/\` subdirectory or as \`use-<feature>.ts\`
- Use React Query (\`@tanstack/react-query\`) for data fetching, not SWR
- Use React Query (\`@tanstack/react-query\`) for data fetching
- Follow existing patterns in \`web/service/use-*.ts\` for API hooks
- Keep each new file under 300 lines
- Maintain TypeScript strict typing
@ -173,12 +172,8 @@ After refactoring, verify:
}
// Priority 2: Extract API/data logic
if (analysis.hasAPI && (analysis.hasEffects || analysis.hasSWR)) {
if (analysis.hasSWR) {
actions.push('🔄 MIGRATE SWR TO REACT QUERY: Replace useSWR with useQuery from @tanstack/react-query')
}
if (analysis.hasAPI)
actions.push('🌐 EXTRACT DATA HOOK: Move API calls and data fetching logic into a dedicated hook using React Query')
}
// Priority 3: Split large components
if (analysis.lineCount > 300) {

View File

@ -1,4 +1,3 @@
import type { Fetcher } from 'swr'
import type { AnnotationCreateResponse, AnnotationEnableStatus, AnnotationItemBasic, EmbeddingModelConfig } from '@/app/components/app/annotation/type'
import { ANNOTATION_DEFAULT } from '@/config'
import { del, get, post } from './base'
@ -44,11 +43,11 @@ export const addAnnotation = (appId: string, body: AnnotationItemBasic) => {
return post<AnnotationCreateResponse>(`apps/${appId}/annotations`, { body })
}
export const annotationBatchImport: Fetcher<{ job_id: string, job_status: string }, { url: string, body: FormData }> = ({ url, body }) => {
export const annotationBatchImport = ({ url, body }: { url: string, body: FormData }): Promise<{ job_id: string, job_status: string }> => {
return post<{ job_id: string, job_status: string }>(url, { body }, { bodyStringify: false, deleteContentType: true })
}
export const checkAnnotationBatchImportProgress: Fetcher<{ job_id: string, job_status: string }, { jobID: string, appId: string }> = ({ jobID, appId }) => {
export const checkAnnotationBatchImportProgress = ({ jobID, appId }: { jobID: string, appId: string }): Promise<{ job_id: string, job_status: string }> => {
return get<{ job_id: string, job_status: string }>(`/apps/${appId}/annotations/batch-import-status/${jobID}`)
}

View File

@ -12,7 +12,6 @@ export const fetchAppDetail = ({ url, id }: { url: string, id: string }): Promis
return get<AppDetailResponse>(`${url}/${id}`)
}
// Direct API call function for non-SWR usage
export const fetchAppDetailDirect = async ({ url, id }: { url: string, id: string }): Promise<AppDetailResponse> => {
return get<AppDetailResponse>(`${url}/${id}`)
}

View File

@ -1,4 +1,3 @@
import type { Fetcher } from 'swr'
import type {
MarketplaceCollectionPluginsResponse,
MarketplaceCollectionsResponse,
@ -82,11 +81,11 @@ export const fetchPluginInfoFromMarketPlace = async ({
return getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${org}/${name}`)
}
export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse, { url: string }> = ({ url }) => {
export const fetchMarketplaceCollections = ({ url }: { url: string }): Promise<MarketplaceCollectionsResponse> => {
return get<MarketplaceCollectionsResponse>(url)
}
export const fetchMarketplaceCollectionPlugins: Fetcher<MarketplaceCollectionPluginsResponse, { url: string }> = ({ url }) => {
export const fetchMarketplaceCollectionPlugins = ({ url }: { url: string }): Promise<MarketplaceCollectionPluginsResponse> => {
return get<MarketplaceCollectionPluginsResponse>(url)
}

View File

@ -55,7 +55,7 @@ export const commonQueryKeys = {
] as const,
notionBinding: (code?: string | null) => [NAME_SPACE, 'notion-binding', code] as const,
modelParameterRules: (provider?: string, model?: string) => [NAME_SPACE, 'model-parameter-rules', provider, model] as const,
langGeniusVersion: (currentVersion?: string | null) => [NAME_SPACE, 'lang-genius-version', currentVersion] as const,
langGeniusVersion: (currentVersion?: string | null) => [NAME_SPACE, 'langgenius-version', currentVersion] as const,
forgotPasswordValidity: (token?: string | null) => [NAME_SPACE, 'forgot-password-validity', token] as const,
dataSourceIntegrates: [NAME_SPACE, 'data-source-integrates'] as const,
}

View File

@ -0,0 +1,231 @@
import type { ReactNode } from 'react'
import type { AppConversationData, ConversationItem } from '@/models/share'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook, waitFor } from '@testing-library/react'
import {
fetchChatList,
fetchConversations,
generationConversationName,
} from './share'
import {
shareQueryKeys,
useInvalidateShareConversations,
useShareChatList,
useShareConversationName,
useShareConversations,
} from './use-share'
vi.mock('./share', () => ({
fetchChatList: vi.fn(),
fetchConversations: vi.fn(),
generationConversationName: vi.fn(),
fetchAppInfo: vi.fn(),
fetchAppMeta: vi.fn(),
fetchAppParams: vi.fn(),
getAppAccessModeByAppCode: vi.fn(),
}))
const mockFetchConversations = vi.mocked(fetchConversations)
const mockFetchChatList = vi.mocked(fetchChatList)
const mockGenerationConversationName = vi.mocked(generationConversationName)
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const createWrapper = (queryClient: QueryClient) => {
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
const renderShareHook = <T,>(hook: () => T) => {
const queryClient = createQueryClient()
const wrapper = createWrapper(queryClient)
return {
queryClient,
...renderHook(hook, { wrapper }),
}
}
const createConversationItem = (overrides: Partial<ConversationItem> = {}): ConversationItem => ({
id: 'conversation-1',
name: 'Conversation 1',
inputs: null,
introduction: 'Intro',
...overrides,
})
const createConversationData = (overrides: Partial<AppConversationData> = {}): AppConversationData => ({
data: [createConversationItem()],
has_more: false,
limit: 20,
...overrides,
})
// Scenario: share conversation list queries behave consistently with params and enablement.
describe('useShareConversations', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should fetch conversations when enabled for non-installed apps', async () => {
// Arrange
const params = {
isInstalledApp: false,
appId: undefined,
pinned: true,
limit: 50,
}
const response = createConversationData()
mockFetchConversations.mockResolvedValueOnce(response)
// Act
const { result, queryClient } = renderShareHook(() => useShareConversations(params))
// Assert
await waitFor(() => {
expect(mockFetchConversations).toHaveBeenCalledWith(false, undefined, undefined, true, 50)
})
await waitFor(() => {
expect(result.current.data).toEqual(response)
})
expect(queryClient.getQueryCache().find({ queryKey: shareQueryKeys.conversationList(params) })).toBeDefined()
})
it('should not fetch conversations when installed app lacks appId', async () => {
// Arrange
const params = {
isInstalledApp: true,
appId: undefined,
}
// Act
const { result } = renderShareHook(() => useShareConversations(params))
// Assert
await waitFor(() => {
expect(result.current.fetchStatus).toBe('idle')
})
expect(mockFetchConversations).not.toHaveBeenCalled()
})
})
// Scenario: chat list queries respect conversation ID and app installation rules.
describe('useShareChatList', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should fetch chat list when conversationId is provided', async () => {
// Arrange
const params = {
conversationId: 'conversation-1',
isInstalledApp: true,
appId: 'app-1',
}
const response = { data: [] }
mockFetchChatList.mockResolvedValueOnce(response)
// Act
const { result } = renderShareHook(() => useShareChatList(params))
// Assert
await waitFor(() => {
expect(mockFetchChatList).toHaveBeenCalledWith('conversation-1', true, 'app-1')
})
await waitFor(() => {
expect(result.current.data).toEqual(response)
})
})
it('should not fetch chat list when conversationId is empty', async () => {
// Arrange
const params = {
conversationId: '',
isInstalledApp: false,
appId: undefined,
}
// Act
const { result } = renderShareHook(() => useShareChatList(params))
// Assert
await waitFor(() => {
expect(result.current.fetchStatus).toBe('idle')
})
expect(mockFetchChatList).not.toHaveBeenCalled()
})
})
// Scenario: conversation name queries follow enabled flags and installation constraints.
describe('useShareConversationName', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should fetch conversation name when enabled and conversationId exists', async () => {
// Arrange
const params = {
conversationId: 'conversation-2',
isInstalledApp: false,
appId: undefined,
}
const response = createConversationItem({ id: 'conversation-2', name: 'Generated' })
mockGenerationConversationName.mockResolvedValueOnce(response)
// Act
const { result } = renderShareHook(() => useShareConversationName(params))
// Assert
await waitFor(() => {
expect(mockGenerationConversationName).toHaveBeenCalledWith(false, undefined, 'conversation-2')
})
await waitFor(() => {
expect(result.current.data).toEqual(response)
})
})
it('should not fetch conversation name when disabled via options', async () => {
// Arrange
const params = {
conversationId: 'conversation-3',
isInstalledApp: false,
appId: undefined,
}
// Act
const { result } = renderShareHook(() => useShareConversationName(params, { enabled: false }))
// Assert
await waitFor(() => {
expect(result.current.fetchStatus).toBe('idle')
})
expect(mockGenerationConversationName).not.toHaveBeenCalled()
})
})
// Scenario: invalidation helper clears share conversation caches.
describe('useInvalidateShareConversations', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should invalidate share conversations query key when invoked', () => {
// Arrange
const { result, queryClient } = renderShareHook(() => useInvalidateShareConversations())
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
// Act
act(() => {
result.current()
})
// Assert
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: shareQueryKeys.conversations })
})
})

View File

@ -1,11 +1,58 @@
import type { AppConversationData, ConversationItem } from '@/models/share'
import { useQuery } from '@tanstack/react-query'
import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share'
import {
fetchAppInfo,
fetchAppMeta,
fetchAppParams,
fetchChatList,
fetchConversations,
generationConversationName,
getAppAccessModeByAppCode,
} from './share'
import { useInvalid } from './use-base'
const NAME_SPACE = 'webapp'
type ShareConversationsParams = {
isInstalledApp: boolean
appId?: string
lastId?: string
pinned?: boolean
limit?: number
}
type ShareChatListParams = {
conversationId: string
isInstalledApp: boolean
appId?: string
}
type ShareConversationNameParams = {
conversationId: string
isInstalledApp: boolean
appId?: string
}
type ShareQueryOptions = {
enabled?: boolean
refetchOnWindowFocus?: boolean
refetchOnReconnect?: boolean
}
export const shareQueryKeys = {
appAccessMode: (code: string | null) => [NAME_SPACE, 'appAccessMode', code] as const,
appInfo: [NAME_SPACE, 'appInfo'] as const,
appParams: [NAME_SPACE, 'appParams'] as const,
appMeta: [NAME_SPACE, 'appMeta'] as const,
conversations: [NAME_SPACE, 'conversations'] as const,
conversationList: (params: ShareConversationsParams) => [NAME_SPACE, 'conversations', params] as const,
chatList: (params: ShareChatListParams) => [NAME_SPACE, 'chatList', params] as const,
conversationName: (params: ShareConversationNameParams) => [NAME_SPACE, 'conversationName', params] as const,
}
export const useGetWebAppAccessModeByCode = (code: string | null) => {
return useQuery({
queryKey: [NAME_SPACE, 'appAccessMode', code],
queryKey: shareQueryKeys.appAccessMode(code),
queryFn: () => getAppAccessModeByAppCode(code!),
enabled: !!code,
staleTime: 0, // backend change the access mode may cause the logic error. Because /permission API is no cached.
@ -15,7 +62,7 @@ export const useGetWebAppAccessModeByCode = (code: string | null) => {
export const useGetWebAppInfo = () => {
return useQuery({
queryKey: [NAME_SPACE, 'appInfo'],
queryKey: shareQueryKeys.appInfo,
queryFn: () => {
return fetchAppInfo()
},
@ -24,7 +71,7 @@ export const useGetWebAppInfo = () => {
export const useGetWebAppParams = () => {
return useQuery({
queryKey: [NAME_SPACE, 'appParams'],
queryKey: shareQueryKeys.appParams,
queryFn: () => {
return fetchAppParams(false)
},
@ -33,9 +80,67 @@ export const useGetWebAppParams = () => {
export const useGetWebAppMeta = () => {
return useQuery({
queryKey: [NAME_SPACE, 'appMeta'],
queryKey: shareQueryKeys.appMeta,
queryFn: () => {
return fetchAppMeta(false)
},
})
}
export const useShareConversations = (params: ShareConversationsParams, options: ShareQueryOptions = {}) => {
const {
enabled = true,
refetchOnReconnect,
refetchOnWindowFocus,
} = options
const isEnabled = enabled && (!params.isInstalledApp || !!params.appId)
return useQuery<AppConversationData>({
queryKey: shareQueryKeys.conversationList(params),
queryFn: () => fetchConversations(
params.isInstalledApp,
params.appId,
params.lastId,
params.pinned,
params.limit,
),
enabled: isEnabled,
refetchOnReconnect,
refetchOnWindowFocus,
})
}
export const useShareChatList = (params: ShareChatListParams, options: ShareQueryOptions = {}) => {
const {
enabled = true,
refetchOnReconnect,
refetchOnWindowFocus,
} = options
const isEnabled = enabled && (!params.isInstalledApp || !!params.appId) && !!params.conversationId
return useQuery({
queryKey: shareQueryKeys.chatList(params),
queryFn: () => fetchChatList(params.conversationId, params.isInstalledApp, params.appId),
enabled: isEnabled,
refetchOnReconnect,
refetchOnWindowFocus,
})
}
export const useShareConversationName = (params: ShareConversationNameParams, options: ShareQueryOptions = {}) => {
const {
enabled = true,
refetchOnReconnect,
refetchOnWindowFocus,
} = options
const isEnabled = enabled && (!params.isInstalledApp || !!params.appId) && !!params.conversationId
return useQuery<ConversationItem>({
queryKey: shareQueryKeys.conversationName(params),
queryFn: () => generationConversationName(params.isInstalledApp, params.appId, params.conversationId),
enabled: isEnabled,
refetchOnReconnect,
refetchOnWindowFocus,
})
}
export const useInvalidateShareConversations = () => {
return useInvalid(shareQueryKeys.conversations)
}