[Clipclops] 2 Clipped 2 Clopped (#3796)
* Add new pkg * copy queries over to new file * useConvoQuery * useListConvos * Use useListConvos * extract useConvoQuery * useGetConvoForMembers * Delete unused * exract useListConvos * Replace imports * Messages/List/index.tsx * extract getconvoformembers * MessageItem * delete chatLog and rename query.ts * Update import * Clipclop service (#3794) * Add Chat service * Better handle deletions * Rollback unneeded changes * Better insertion order * Use clipclops * don't show FAB if error * clean up imports * Update Convo service * Remove temp files --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
parent
d61b366b26
commit
538ca8dff1
30 changed files with 752 additions and 1130 deletions
|
@ -1,5 +1,6 @@
|
|||
import React, {useCallback, useMemo} from 'react'
|
||||
import {StyleProp, TextStyle, View} from 'react-native'
|
||||
import {ChatBskyConvoDefs} from '@atproto-labs/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
@ -7,14 +8,16 @@ import {useSession} from '#/state/session'
|
|||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||
|
||||
export function MessageItem({
|
||||
item,
|
||||
next,
|
||||
}: {
|
||||
item: TempDmChatDefs.MessageView
|
||||
next: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage | null
|
||||
item: ChatBskyConvoDefs.MessageView
|
||||
next:
|
||||
| ChatBskyConvoDefs.MessageView
|
||||
| ChatBskyConvoDefs.DeletedMessageView
|
||||
| null
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
|
@ -22,7 +25,7 @@ export function MessageItem({
|
|||
const isFromSelf = item.sender?.did === currentAccount?.did
|
||||
|
||||
const isNextFromSelf =
|
||||
TempDmChatDefs.isMessageView(next) &&
|
||||
ChatBskyConvoDefs.isMessageView(next) &&
|
||||
next.sender?.did === currentAccount?.did
|
||||
|
||||
const isLastInGroup = useMemo(() => {
|
||||
|
@ -32,7 +35,7 @@ export function MessageItem({
|
|||
}
|
||||
|
||||
// or, if there's a 10 minute gap between this message and the next
|
||||
if (TempDmChatDefs.isMessageView(next)) {
|
||||
if (ChatBskyConvoDefs.isMessageView(next)) {
|
||||
const thisDate = new Date(item.sentAt)
|
||||
const nextDate = new Date(next.sentAt)
|
||||
|
||||
|
@ -88,7 +91,7 @@ function Metadata({
|
|||
isLastInGroup,
|
||||
style,
|
||||
}: {
|
||||
message: TempDmChatDefs.MessageView
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
isLastInGroup: boolean
|
||||
style: StyleProp<TextStyle>
|
||||
}) {
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
import React, {useCallback, useMemo, useRef, useState} from 'react'
|
||||
import React, {useCallback, useMemo, useRef} from 'react'
|
||||
import {FlatList, View, ViewToken} from 'react-native'
|
||||
import {Alert} from 'react-native'
|
||||
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useChat} from '#/state/messages'
|
||||
import {ChatProvider} from '#/state/messages'
|
||||
import {ConvoItem, ConvoStatus} from '#/state/messages/convo'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
||||
import {MessageItem} from '#/screens/Messages/Conversation/MessageItem'
|
||||
import {
|
||||
useChat,
|
||||
useChatLogQuery,
|
||||
useSendMessageMutation,
|
||||
} from '#/screens/Messages/Temp/query/query'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||
|
||||
type MessageWithNext = {
|
||||
message: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage
|
||||
next: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage | null
|
||||
}
|
||||
|
||||
function MaybeLoader({isLoading}: {isLoading: boolean}) {
|
||||
return (
|
||||
|
@ -34,47 +25,43 @@ function MaybeLoader({isLoading}: {isLoading: boolean}) {
|
|||
)
|
||||
}
|
||||
|
||||
function renderItem({item}: {item: MessageWithNext}) {
|
||||
if (TempDmChatDefs.isMessageView(item.message))
|
||||
return <MessageItem item={item.message} next={item.next} />
|
||||
|
||||
if (TempDmChatDefs.isDeletedMessage(item)) return <Text>Deleted message</Text>
|
||||
function renderItem({item}: {item: ConvoItem}) {
|
||||
if (item.type === 'message') {
|
||||
return <MessageItem item={item.message} next={item.nextMessage} />
|
||||
} else if (item.type === 'deleted-message') {
|
||||
return <Text>Deleted message</Text>
|
||||
} else if (item.type === 'pending-message') {
|
||||
return <Text>{item.message.text}</Text>
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO rm
|
||||
// TEMP: This is a temporary function to generate unique keys for mutation placeholders
|
||||
const generateUniqueKey = () => `_${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
function onScrollToEndFailed() {
|
||||
// Placeholder function. You have to give FlatList something or else it will error.
|
||||
}
|
||||
|
||||
export function MessagesList({chatId}: {chatId: string}) {
|
||||
export function MessagesList({convoId}: {convoId: string}) {
|
||||
return (
|
||||
<ChatProvider convoId={convoId}>
|
||||
<MessagesListInner />
|
||||
</ChatProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function MessagesListInner() {
|
||||
const chat = useChat()
|
||||
const flatListRef = useRef<FlatList>(null)
|
||||
|
||||
// Whenever we reach the end (visually the top), we don't want to keep calling it. We will set `isFetching` to true
|
||||
// once the request for new posts starts. Then, we will change it back to false after the content size changes.
|
||||
const isFetching = useRef(false)
|
||||
|
||||
// We use this to know if we should scroll after a new clop is added to the list
|
||||
const isAtBottom = useRef(false)
|
||||
|
||||
// Because the viewableItemsChanged callback won't have access to the updated state, we use a ref to store the
|
||||
// total number of clops
|
||||
// TODO this needs to be set to whatever the initial number of messages is
|
||||
const totalMessages = useRef(10)
|
||||
// const totalMessages = useRef(10)
|
||||
|
||||
// TODO later
|
||||
|
||||
const [_, setShowSpinner] = useState(false)
|
||||
|
||||
// Query Data
|
||||
const {data: chat} = useChat(chatId)
|
||||
const {mutate: sendMessage} = useSendMessageMutation(chatId)
|
||||
useChatLogQuery()
|
||||
|
||||
const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
|
||||
return [
|
||||
(info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
|
||||
|
@ -93,23 +80,11 @@ export function MessagesList({chatId}: {chatId: string}) {
|
|||
if (isAtBottom.current) {
|
||||
flatListRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||
}
|
||||
|
||||
isFetching.current = false
|
||||
setShowSpinner(false)
|
||||
}, [])
|
||||
|
||||
const onEndReached = useCallback(() => {
|
||||
if (isFetching.current) return
|
||||
isFetching.current = true
|
||||
setShowSpinner(true)
|
||||
|
||||
// Eventually we will add more here when we hit the top through RQuery
|
||||
// We wouldn't actually use a timeout, but there would be a delay while loading
|
||||
setTimeout(() => {
|
||||
// Do something
|
||||
setShowSpinner(false)
|
||||
}, 1000)
|
||||
}, [])
|
||||
chat.service.fetchMessageHistory()
|
||||
}, [chat])
|
||||
|
||||
const onInputFocus = useCallback(() => {
|
||||
if (!isAtBottom.current) {
|
||||
|
@ -117,84 +92,51 @@ export function MessagesList({chatId}: {chatId: string}) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
async (message: string) => {
|
||||
if (!message) return
|
||||
|
||||
try {
|
||||
sendMessage({
|
||||
message,
|
||||
tempId: generateUniqueKey(),
|
||||
})
|
||||
} catch (e: any) {
|
||||
Alert.alert(e.toString())
|
||||
}
|
||||
},
|
||||
[sendMessage],
|
||||
)
|
||||
|
||||
const onInputBlur = useCallback(() => {}, [])
|
||||
|
||||
const messages = useMemo(() => {
|
||||
if (!chat) return []
|
||||
|
||||
const filtered = chat.messages
|
||||
.filter(
|
||||
(
|
||||
message,
|
||||
): message is
|
||||
| TempDmChatDefs.MessageView
|
||||
| TempDmChatDefs.DeletedMessage => {
|
||||
return (
|
||||
TempDmChatDefs.isMessageView(message) ||
|
||||
TempDmChatDefs.isDeletedMessage(message)
|
||||
)
|
||||
},
|
||||
)
|
||||
.reduce((acc, message) => {
|
||||
// convert [n1, n2, n3, ...] to [{message: n1, next: n2}, {message: n2, next: n3}, {message: n3, next: n4}, ...]
|
||||
|
||||
return [...acc, {message, next: acc.at(-1)?.message ?? null}]
|
||||
}, [] as MessageWithNext[])
|
||||
totalMessages.current = filtered.length
|
||||
|
||||
return filtered
|
||||
}, [chat])
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={{flex: 1, marginBottom: isWeb ? 20 : 85}}
|
||||
behavior="padding"
|
||||
keyboardVerticalOffset={70}
|
||||
contentContainerStyle={{flex: 1}}>
|
||||
<FlatList
|
||||
data={messages}
|
||||
keyExtractor={item => item.message.id}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{paddingHorizontal: 10}}
|
||||
inverted={true}
|
||||
// In the future, we might want to adjust this value. Not very concerning right now as long as we are only
|
||||
// dealing with text. But whenever we have images or other media and things are taller, we will want to lower
|
||||
// this...probably.
|
||||
initialNumToRender={20}
|
||||
// Same with the max to render per batch. Let's be safe for now though.
|
||||
maxToRenderPerBatch={25}
|
||||
removeClippedSubviews={true}
|
||||
onEndReached={onEndReached}
|
||||
onScrollToIndexFailed={onScrollToEndFailed}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 1,
|
||||
}}
|
||||
ListFooterComponent={<MaybeLoader isLoading={false} />}
|
||||
ref={flatListRef}
|
||||
keyboardDismissMode="none"
|
||||
/>
|
||||
{chat.state.status === ConvoStatus.Ready && (
|
||||
<FlatList
|
||||
data={chat.state.items}
|
||||
keyExtractor={item => item.key}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{paddingHorizontal: 10}}
|
||||
// In the future, we might want to adjust this value. Not very concerning right now as long as we are only
|
||||
// dealing with text. But whenever we have images or other media and things are taller, we will want to lower
|
||||
// this...probably.
|
||||
initialNumToRender={20}
|
||||
// Same with the max to render per batch. Let's be safe for now though.
|
||||
maxToRenderPerBatch={25}
|
||||
inverted={true}
|
||||
onEndReached={onEndReached}
|
||||
onScrollToIndexFailed={onScrollToEndFailed}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0,
|
||||
}}
|
||||
ListFooterComponent={
|
||||
<MaybeLoader isLoading={chat.state.isFetchingHistory} />
|
||||
}
|
||||
removeClippedSubviews={true}
|
||||
ref={flatListRef}
|
||||
keyboardDismissMode="none"
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={{paddingHorizontal: 10}}>
|
||||
<MessageInput
|
||||
onSendMessage={onSendMessage}
|
||||
onSendMessage={text => {
|
||||
chat.service.sendMessage({
|
||||
text,
|
||||
})
|
||||
}}
|
||||
onFocus={onInputFocus}
|
||||
onBlur={onInputBlur}
|
||||
/>
|
||||
|
|
|
@ -9,13 +9,13 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
|||
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {useConvoQuery} from '#/state/queries/messages/conversation'
|
||||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useSession} from 'state/session'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
||||
import {useChatQuery} from '#/screens/Messages/Temp/query/query'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded} from '#/components/icons/DotGrid'
|
||||
|
@ -29,11 +29,11 @@ type Props = NativeStackScreenProps<
|
|||
>
|
||||
export function MessagesConversationScreen({route}: Props) {
|
||||
const gate = useGate()
|
||||
const chatId = route.params.conversation
|
||||
const convoId = route.params.conversation
|
||||
const {currentAccount} = useSession()
|
||||
const myDid = currentAccount?.did
|
||||
|
||||
const {data: chat, isError: isError} = useChatQuery(chatId)
|
||||
const {data: chat, isError: isError} = useConvoQuery(convoId)
|
||||
const otherProfile = React.useMemo(() => {
|
||||
return chat?.members?.find(m => m.did !== myDid)
|
||||
}, [chat?.members, myDid])
|
||||
|
@ -51,7 +51,7 @@ export function MessagesConversationScreen({route}: Props) {
|
|||
return (
|
||||
<CenteredView style={{flex: 1}} sideBorders>
|
||||
<Header profile={otherProfile} />
|
||||
<MessagesList chatId={chatId} />
|
||||
<MessagesList convoId={convoId} />
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ChatBskyConvoDefs} from '@atproto-labs/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
@ -11,24 +12,22 @@ import {MessagesTabNavigatorParams} from '#/lib/routes/types'
|
|||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {useListConvos} from '#/state/queries/messages/list-converations'
|
||||
import {useSession} from '#/state/session'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {DialogControlProps, useDialogControl} from '#/components/Dialog'
|
||||
import {NewChat} from '#/components/dms/NewChat'
|
||||
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
|
||||
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
|
||||
import {Link} from '#/components/Link'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||
import {NewChat} from '../../../components/dms/NewChat'
|
||||
import {ClipClopGate} from '../gate'
|
||||
import {useListChats} from '../Temp/query/query'
|
||||
|
||||
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'>
|
||||
export function MessagesListScreen({navigation}: Props) {
|
||||
|
@ -59,13 +58,13 @@ export function MessagesListScreen({navigation}: Props) {
|
|||
fetchNextPage,
|
||||
error,
|
||||
refetch,
|
||||
} = useListChats()
|
||||
} = useListConvos()
|
||||
|
||||
const isError = !!error
|
||||
|
||||
const conversations = useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages.flatMap(page => page.chats)
|
||||
return data.pages.flatMap(page => page.convos)
|
||||
}
|
||||
return []
|
||||
}, [data])
|
||||
|
@ -99,9 +98,12 @@ export function MessagesListScreen({navigation}: Props) {
|
|||
navigation.navigate('MessagesSettings')
|
||||
}, [navigation])
|
||||
|
||||
const renderItem = useCallback(({item}: {item: TempDmChatDefs.ChatView}) => {
|
||||
return <ChatListItem key={item.id} chat={item} />
|
||||
}, [])
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: ChatBskyConvoDefs.ConvoView}) => {
|
||||
return <ChatListItem key={item.id} convo={item} />
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const gate = useGate()
|
||||
if (!gate('dms')) return <ClipClopGate />
|
||||
|
@ -119,7 +121,7 @@ export function MessagesListScreen({navigation}: Props) {
|
|||
errorMessage={cleanError(error)}
|
||||
onRetry={isError ? refetch : undefined}
|
||||
/>
|
||||
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||
{!isError && <NewChat onNewChat={onNewChat} control={newChatControl} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -166,26 +168,26 @@ export function MessagesListScreen({navigation}: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
|
||||
function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
let lastMessage = _(msg`No messages yet`)
|
||||
let lastMessageSentAt: string | null = null
|
||||
if (TempDmChatDefs.isMessageView(chat.lastMessage)) {
|
||||
if (chat.lastMessage.sender?.did === currentAccount?.did) {
|
||||
lastMessage = _(msg`You: ${chat.lastMessage.text}`)
|
||||
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
||||
if (convo.lastMessage.sender?.did === currentAccount?.did) {
|
||||
lastMessage = _(msg`You: ${convo.lastMessage.text}`)
|
||||
} else {
|
||||
lastMessage = chat.lastMessage.text
|
||||
lastMessage = convo.lastMessage.text
|
||||
}
|
||||
lastMessageSentAt = chat.lastMessage.sentAt
|
||||
lastMessageSentAt = convo.lastMessage.sentAt
|
||||
}
|
||||
if (TempDmChatDefs.isDeletedMessage(chat.lastMessage)) {
|
||||
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
|
||||
lastMessage = _(msg`Message deleted`)
|
||||
}
|
||||
|
||||
const otherUser = chat.members.find(
|
||||
const otherUser = convo.members.find(
|
||||
member => member.did !== currentAccount?.did,
|
||||
)
|
||||
|
||||
|
@ -194,7 +196,7 @@ function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Link to={`/messages/${chat.id}`} style={a.flex_1}>
|
||||
<Link to={`/messages/${convo.id}`} style={a.flex_1}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
|
@ -211,7 +213,8 @@ function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
|
|||
</View>
|
||||
<View style={[a.flex_1]}>
|
||||
<Text numberOfLines={1} style={[a.text_md, a.leading_normal]}>
|
||||
<Text style={[t.atoms.text, chat.unreadCount > 0 && a.font_bold]}>
|
||||
<Text
|
||||
style={[t.atoms.text, convo.unreadCount > 0 && a.font_bold]}>
|
||||
{otherUser.displayName || otherUser.handle}
|
||||
</Text>{' '}
|
||||
{lastMessageSentAt ? (
|
||||
|
@ -233,14 +236,14 @@ function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
|
|||
style={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
chat.unreadCount > 0
|
||||
convo.unreadCount > 0
|
||||
? a.font_bold
|
||||
: t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{lastMessage}
|
||||
</Text>
|
||||
</View>
|
||||
{chat.unreadCount > 0 && (
|
||||
{convo.unreadCount > 0 && (
|
||||
<View
|
||||
style={[
|
||||
a.flex_0,
|
||||
|
|
|
@ -1,305 +0,0 @@
|
|||
import {
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||
import * as TempDmChatGetChat from '#/temp/dm/getChat'
|
||||
import * as TempDmChatGetChatForMembers from '#/temp/dm/getChatForMembers'
|
||||
import * as TempDmChatGetChatLog from '#/temp/dm/getChatLog'
|
||||
import * as TempDmChatGetChatMessages from '#/temp/dm/getChatMessages'
|
||||
import * as TempDmChatListChats from '#/temp/dm/listChats'
|
||||
import {useDmServiceUrlStorage} from '../useDmServiceUrlStorage'
|
||||
|
||||
/**
|
||||
* TEMPORARY, PLEASE DO NOT JUDGE ME REACT QUERY OVERLORDS 🙏
|
||||
* (and do not try this at home)
|
||||
*/
|
||||
|
||||
const useHeaders = () => {
|
||||
const {currentAccount} = useSession()
|
||||
return {
|
||||
get Authorization() {
|
||||
return currentAccount!.did
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Chat = {
|
||||
chatId: string
|
||||
messages: TempDmChatGetChatMessages.OutputSchema['messages']
|
||||
lastCursor?: string
|
||||
lastRev?: string
|
||||
}
|
||||
|
||||
export function useChat(chatId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
const headers = useHeaders()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['chat', chatId],
|
||||
queryFn: async () => {
|
||||
const currentChat = queryClient.getQueryData(['chat', chatId])
|
||||
|
||||
if (currentChat) {
|
||||
return currentChat as Chat
|
||||
}
|
||||
|
||||
const messagesResponse = await fetch(
|
||||
`${serviceUrl}/xrpc/temp.dm.getChatMessages?chatId=${chatId}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
)
|
||||
|
||||
if (!messagesResponse.ok) throw new Error('Failed to fetch messages')
|
||||
|
||||
const messagesJson =
|
||||
(await messagesResponse.json()) as TempDmChatGetChatMessages.OutputSchema
|
||||
|
||||
const chatResponse = await fetch(
|
||||
`${serviceUrl}/xrpc/temp.dm.getChat?chatId=${chatId}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
)
|
||||
|
||||
if (!chatResponse.ok) throw new Error('Failed to fetch chat')
|
||||
|
||||
const chatJson =
|
||||
(await chatResponse.json()) as TempDmChatGetChat.OutputSchema
|
||||
|
||||
queryClient.setQueryData(['chatQuery', chatId], chatJson.chat)
|
||||
|
||||
const newChat = {
|
||||
chatId,
|
||||
messages: messagesJson.messages,
|
||||
lastCursor: messagesJson.cursor,
|
||||
lastRev: chatJson.chat.rev,
|
||||
} satisfies Chat
|
||||
|
||||
queryClient.setQueryData(['chat', chatId], newChat)
|
||||
|
||||
return newChat
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface SendMessageMutationVariables {
|
||||
message: string
|
||||
tempId: string
|
||||
}
|
||||
|
||||
export function createTempId() {
|
||||
return Math.random().toString(36).substring(7).toString()
|
||||
}
|
||||
|
||||
export function useSendMessageMutation(chatId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
const headers = useHeaders()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
|
||||
return useMutation<
|
||||
TempDmChatDefs.Message,
|
||||
Error,
|
||||
SendMessageMutationVariables,
|
||||
unknown
|
||||
>({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
mutationFn: async ({message, tempId}) => {
|
||||
const response = await fetch(
|
||||
`${serviceUrl}/xrpc/temp.dm.sendMessage?chatId=${chatId}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
message: {
|
||||
text: message,
|
||||
},
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) throw new Error('Failed to send message')
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onMutate: async variables => {
|
||||
queryClient.setQueryData(['chat', chatId], (prev: Chat) => {
|
||||
return {
|
||||
...prev,
|
||||
messages: [
|
||||
{
|
||||
$type: 'temp.dm.defs#messageView',
|
||||
id: variables.tempId,
|
||||
text: variables.message,
|
||||
sender: {did: headers.Authorization}, // TODO a real DID get
|
||||
sentAt: new Date().toISOString(),
|
||||
},
|
||||
...prev.messages,
|
||||
],
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: (result, variables) => {
|
||||
queryClient.setQueryData(['chat', chatId], (prev: Chat) => {
|
||||
return {
|
||||
...prev,
|
||||
messages: prev.messages.map(m =>
|
||||
m.id === variables.tempId ? {...m, id: result.id} : m,
|
||||
),
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: (_, variables) => {
|
||||
console.log(_)
|
||||
queryClient.setQueryData(['chat', chatId], (prev: Chat) => ({
|
||||
...prev,
|
||||
messages: prev.messages.filter(m => m.id !== variables.tempId),
|
||||
}))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useChatLogQuery() {
|
||||
const queryClient = useQueryClient()
|
||||
const headers = useHeaders()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['chatLog'],
|
||||
queryFn: async () => {
|
||||
const prevLog = queryClient.getQueryData([
|
||||
'chatLog',
|
||||
]) as TempDmChatGetChatLog.OutputSchema
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${serviceUrl}/xrpc/temp.dm.getChatLog?cursor=${
|
||||
prevLog?.cursor ?? ''
|
||||
}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch chat log')
|
||||
|
||||
const json =
|
||||
(await response.json()) as TempDmChatGetChatLog.OutputSchema
|
||||
|
||||
if (json.logs.length > 0) {
|
||||
queryClient.invalidateQueries({queryKey: ['chats']})
|
||||
}
|
||||
|
||||
for (const log of json.logs) {
|
||||
if (TempDmChatDefs.isLogCreateMessage(log)) {
|
||||
queryClient.setQueryData(['chat', log.chatId], (prev: Chat) => {
|
||||
// TODO hack filter out duplicates
|
||||
if (prev?.messages.find(m => m.id === log.message.id)) return
|
||||
|
||||
return {
|
||||
...prev,
|
||||
messages: [log.message, ...prev.messages],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return json
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
refetchInterval: 5000,
|
||||
})
|
||||
}
|
||||
|
||||
export function useGetChatFromMembers({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: TempDmChatGetChatForMembers.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const headers = useHeaders()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (members: string[]) => {
|
||||
const response = await fetch(
|
||||
`${serviceUrl}/xrpc/temp.dm.getChatForMembers?members=${members.join(
|
||||
',',
|
||||
)}`,
|
||||
{headers},
|
||||
)
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch chat')
|
||||
|
||||
return (await response.json()) as TempDmChatGetChatForMembers.OutputSchema
|
||||
},
|
||||
onSuccess: data => {
|
||||
queryClient.setQueryData(['chat', data.chat.id], {
|
||||
chatId: data.chat.id,
|
||||
messages: [],
|
||||
lastRev: data.chat.rev,
|
||||
})
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError,
|
||||
})
|
||||
}
|
||||
|
||||
export function useListChats() {
|
||||
const headers = useHeaders()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['chats'],
|
||||
queryFn: async ({pageParam}) => {
|
||||
const response = await fetch(
|
||||
`${serviceUrl}/xrpc/temp.dm.listChats${
|
||||
pageParam ? `?cursor=${pageParam}` : ''
|
||||
}`,
|
||||
{headers},
|
||||
)
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch chats')
|
||||
|
||||
return (await response.json()) as TempDmChatListChats.OutputSchema
|
||||
},
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
})
|
||||
}
|
||||
|
||||
export function useChatQuery(chatId: string) {
|
||||
const headers = useHeaders()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['chatQuery', chatId],
|
||||
queryFn: async () => {
|
||||
const chatResponse = await fetch(
|
||||
`${serviceUrl}/xrpc/temp.dm.getChat?chatId=${chatId}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
)
|
||||
|
||||
if (!chatResponse.ok) throw new Error('Failed to fetch chat')
|
||||
|
||||
const json = (await chatResponse.json()) as TempDmChatGetChat.OutputSchema
|
||||
return json.chat
|
||||
},
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue