mirror of
https://github.com/hanzo-io/team.git
synced 2026-10-30 18:56:23 +00:00
177 lines
6.1 KiB
TypeScript
177 lines
6.1 KiB
TypeScript
import { useMemo, useCallback, useContext } from 'react'
|
|
import { View, ActivityIndicator, Text } from 'react-native'
|
|
import { FrappeConfig, FrappeContext, FrappeError, useSWRInfinite } from 'frappe-react-sdk'
|
|
import ErrorBanner from '@components/common/ErrorBanner'
|
|
import { ThreadMessage } from './ThreadTabs'
|
|
import ThreadsOutlineIcon from '@assets/icons/ThreadsOutlineIcon.svg'
|
|
import { useGetCurrentWorkspace } from '@hooks/useGetCurrentWorkspace'
|
|
import useUnreadThreadsCount from '@hooks/useUnreadThreadsCount'
|
|
import { LegendList } from '@legendapp/list'
|
|
import { useColorScheme } from "@hooks/useColorScheme"
|
|
import ThreadPreviewBox from './ThreadPreviewBox'
|
|
|
|
type Props = {
|
|
/** Whether to fetch AI threads */
|
|
aiThreads?: 0 | 1,
|
|
/** Content to search for */
|
|
content?: string,
|
|
/** Channel to filter by */
|
|
channel?: string,
|
|
/** Endpoint to fetch threads from. Defaults to `raven.api.threads.get_all_threads` */
|
|
endpoint?: string,
|
|
/** Whether to only show unread threads */
|
|
onlyShowUnread?: boolean
|
|
}
|
|
|
|
const PAGE_SIZE = 5
|
|
|
|
type GetThreadsReturnType = {
|
|
message: ThreadMessage[]
|
|
}
|
|
|
|
type GetThreadsSWRKey = [string, {
|
|
is_ai_thread: 0 | 1,
|
|
workspace: string,
|
|
content: string,
|
|
channel_id: string,
|
|
startAfter: number,
|
|
onlyShowUnread: boolean
|
|
}]
|
|
|
|
const ThreadsList = ({ aiThreads, content, channel, endpoint = "raven.api.threads.get_all_threads", onlyShowUnread }: Props) => {
|
|
|
|
const { workspace } = useGetCurrentWorkspace()
|
|
|
|
const { data: unreadThreads } = useUnreadThreadsCount()
|
|
|
|
const unreadThreadsMap = useMemo(() => {
|
|
return unreadThreads?.message.reduce((acc, thread) => {
|
|
acc[thread.name] = thread.unread_count
|
|
return acc
|
|
}, {} as Record<string, number>)
|
|
}, [unreadThreads])
|
|
|
|
const { call } = useContext(FrappeContext) as FrappeConfig
|
|
|
|
const { data, size, isLoading, setSize, error } = useSWRInfinite<GetThreadsReturnType, FrappeError>(
|
|
(pageIndex, previousPageData) => {
|
|
if (previousPageData && !previousPageData.message.length) return null
|
|
const startAfter = pageIndex * PAGE_SIZE
|
|
return [endpoint, {
|
|
is_ai_thread: aiThreads,
|
|
workspace: workspace,
|
|
content: content,
|
|
channel_id: channel,
|
|
startAfter,
|
|
onlyShowUnread
|
|
}] // SWR key
|
|
}, (swrKey: GetThreadsSWRKey) => {
|
|
return call.get<GetThreadsReturnType>(endpoint, {
|
|
is_ai_thread: swrKey[1].is_ai_thread,
|
|
workspace: swrKey[1].workspace,
|
|
content: swrKey[1].content,
|
|
channel_id: swrKey[1].channel_id,
|
|
start_after: swrKey[1].startAfter,
|
|
limit: PAGE_SIZE,
|
|
only_show_unread: swrKey[1].onlyShowUnread
|
|
})
|
|
}, {
|
|
revalidateOnFocus: false,
|
|
revalidateIfStale: true,
|
|
})
|
|
|
|
const isEmpty = data?.[0]?.message?.length === 0
|
|
|
|
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined')
|
|
|
|
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.message?.length < PAGE_SIZE)
|
|
|
|
const threads = data?.flatMap((page) => page.message) ?? []
|
|
|
|
const loadMore = useCallback(() => {
|
|
if (!isReachingEnd && !isLoadingMore) {
|
|
setSize(size + 1)
|
|
}
|
|
}, [isReachingEnd, isLoadingMore, setSize, size])
|
|
|
|
if (isLoading) {
|
|
return <View className="flex-1 justify-center items-center h-full">
|
|
<ActivityIndicator />
|
|
</View>
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<ErrorBanner error={error} />
|
|
)
|
|
}
|
|
|
|
return (
|
|
<LegendList
|
|
data={threads ?? []}
|
|
keyExtractor={(item) => item.name}
|
|
renderItem={({ item }) => (
|
|
<View role='listitem'>
|
|
<ThreadPreviewBox
|
|
thread={item}
|
|
unreadCount={unreadThreadsMap?.[item.name] ?? 0}
|
|
/>
|
|
</View>
|
|
)}
|
|
onEndReached={loadMore}
|
|
onEndReachedThreshold={0.1}
|
|
ListFooterComponent={
|
|
<View className='flex flex-row justify-center items-center'>
|
|
{isLoadingMore && <ActivityIndicator />}
|
|
</View>
|
|
}
|
|
contentContainerStyle={{
|
|
paddingBottom: 64
|
|
}}
|
|
extraData={unreadThreadsMap}
|
|
ListEmptyComponent={<EmptyStateForThreads isFiltered={onlyShowUnread} searchText={content} />}
|
|
estimatedItemSize={250}
|
|
style={{ flexGrow: 1 }}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const EmptyStateForThreads = ({ isFiltered = false, searchText }: { isFiltered?: boolean, searchText?: string }) => {
|
|
|
|
const content = useMemo(() => {
|
|
if (searchText && !isFiltered) {
|
|
return {
|
|
title: "No results found",
|
|
description: `No threads match your search for "${searchText}". Try a different search term.`
|
|
}
|
|
} else if (isFiltered) {
|
|
return {
|
|
title: "You're all caught up",
|
|
description: 'There are no unread threads to show. Clear the filter to see all threads.'
|
|
}
|
|
} else {
|
|
return {
|
|
title: 'No threads yet',
|
|
description: 'Threads help keep conversations organized. Reply to any message to start a new thread or use the thread icon on messages to join existing discussions.'
|
|
}
|
|
}
|
|
}, [isFiltered, searchText])
|
|
|
|
const { colors } = useColorScheme()
|
|
|
|
return (
|
|
<View className="flex flex-col gap-2 bg-background p-4">
|
|
<View className="flex flex-row items-center gap-2">
|
|
<ThreadsOutlineIcon fill={colors.icon} height={20} width={20} />
|
|
<Text className="text-foreground text-base font-medium">
|
|
{content.title}
|
|
</Text>
|
|
</View>
|
|
<Text className="text-sm text-foreground/60">
|
|
{content.description}
|
|
</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export default ThreadsList |