[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>zio/stable
parent
d61b366b26
commit
538ca8dff1
|
@ -50,6 +50,7 @@
|
||||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atproto-labs/api": "^0.12.8-clipclops.0",
|
||||||
"@atproto/api": "^0.12.5",
|
"@atproto/api": "^0.12.5",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
||||||
import {FAB} from '#/view/com/util/fab/FAB'
|
import {FAB} from '#/view/com/util/fab/FAB'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
@ -17,7 +18,6 @@ import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
import {useGetChatFromMembers} from '../../screens/Messages/Temp/query/query'
|
|
||||||
import {Button} from '../Button'
|
import {Button} from '../Button'
|
||||||
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope'
|
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope'
|
||||||
import {ListMaybePlaceholder} from '../Lists'
|
import {ListMaybePlaceholder} from '../Lists'
|
||||||
|
@ -33,9 +33,9 @@ export function NewChat({
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const {mutate: createChat} = useGetChatFromMembers({
|
const {mutate: createChat} = useGetConvoForMembers({
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
onNewChat(data.chat.id)
|
onNewChat(data.convo.id)
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
Toast.show(error.message)
|
Toast.show(error.message)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, {useCallback, useMemo} from 'react'
|
import React, {useCallback, useMemo} from 'react'
|
||||||
import {StyleProp, TextStyle, View} from 'react-native'
|
import {StyleProp, TextStyle, View} from 'react-native'
|
||||||
|
import {ChatBskyConvoDefs} from '@atproto-labs/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
@ -7,14 +8,16 @@ import {useSession} from '#/state/session'
|
||||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
|
||||||
|
|
||||||
export function MessageItem({
|
export function MessageItem({
|
||||||
item,
|
item,
|
||||||
next,
|
next,
|
||||||
}: {
|
}: {
|
||||||
item: TempDmChatDefs.MessageView
|
item: ChatBskyConvoDefs.MessageView
|
||||||
next: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage | null
|
next:
|
||||||
|
| ChatBskyConvoDefs.MessageView
|
||||||
|
| ChatBskyConvoDefs.DeletedMessageView
|
||||||
|
| null
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
@ -22,7 +25,7 @@ export function MessageItem({
|
||||||
const isFromSelf = item.sender?.did === currentAccount?.did
|
const isFromSelf = item.sender?.did === currentAccount?.did
|
||||||
|
|
||||||
const isNextFromSelf =
|
const isNextFromSelf =
|
||||||
TempDmChatDefs.isMessageView(next) &&
|
ChatBskyConvoDefs.isMessageView(next) &&
|
||||||
next.sender?.did === currentAccount?.did
|
next.sender?.did === currentAccount?.did
|
||||||
|
|
||||||
const isLastInGroup = useMemo(() => {
|
const isLastInGroup = useMemo(() => {
|
||||||
|
@ -32,7 +35,7 @@ export function MessageItem({
|
||||||
}
|
}
|
||||||
|
|
||||||
// or, if there's a 10 minute gap between this message and the next
|
// 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 thisDate = new Date(item.sentAt)
|
||||||
const nextDate = new Date(next.sentAt)
|
const nextDate = new Date(next.sentAt)
|
||||||
|
|
||||||
|
@ -88,7 +91,7 @@ function Metadata({
|
||||||
isLastInGroup,
|
isLastInGroup,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
message: TempDmChatDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
isLastInGroup: boolean
|
isLastInGroup: boolean
|
||||||
style: StyleProp<TextStyle>
|
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 {FlatList, View, ViewToken} from 'react-native'
|
||||||
import {Alert} from 'react-native'
|
|
||||||
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
|
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 {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
||||||
import {MessageItem} from '#/screens/Messages/Conversation/MessageItem'
|
import {MessageItem} from '#/screens/Messages/Conversation/MessageItem'
|
||||||
import {
|
|
||||||
useChat,
|
|
||||||
useChatLogQuery,
|
|
||||||
useSendMessageMutation,
|
|
||||||
} from '#/screens/Messages/Temp/query/query'
|
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
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}) {
|
function MaybeLoader({isLoading}: {isLoading: boolean}) {
|
||||||
return (
|
return (
|
||||||
|
@ -34,47 +25,43 @@ function MaybeLoader({isLoading}: {isLoading: boolean}) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItem({item}: {item: MessageWithNext}) {
|
function renderItem({item}: {item: ConvoItem}) {
|
||||||
if (TempDmChatDefs.isMessageView(item.message))
|
if (item.type === 'message') {
|
||||||
return <MessageItem item={item.message} next={item.next} />
|
return <MessageItem item={item.message} next={item.nextMessage} />
|
||||||
|
} else if (item.type === 'deleted-message') {
|
||||||
if (TempDmChatDefs.isDeletedMessage(item)) return <Text>Deleted message</Text>
|
return <Text>Deleted message</Text>
|
||||||
|
} else if (item.type === 'pending-message') {
|
||||||
|
return <Text>{item.message.text}</Text>
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
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() {
|
function onScrollToEndFailed() {
|
||||||
// Placeholder function. You have to give FlatList something or else it will error.
|
// 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)
|
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
|
// We use this to know if we should scroll after a new clop is added to the list
|
||||||
const isAtBottom = useRef(false)
|
const isAtBottom = useRef(false)
|
||||||
|
|
||||||
// Because the viewableItemsChanged callback won't have access to the updated state, we use a ref to store the
|
// Because the viewableItemsChanged callback won't have access to the updated state, we use a ref to store the
|
||||||
// total number of clops
|
// total number of clops
|
||||||
// TODO this needs to be set to whatever the initial number of messages is
|
// TODO this needs to be set to whatever the initial number of messages is
|
||||||
const totalMessages = useRef(10)
|
// const totalMessages = useRef(10)
|
||||||
|
|
||||||
// TODO later
|
// TODO later
|
||||||
|
|
||||||
const [_, setShowSpinner] = useState(false)
|
|
||||||
|
|
||||||
// Query Data
|
|
||||||
const {data: chat} = useChat(chatId)
|
|
||||||
const {mutate: sendMessage} = useSendMessageMutation(chatId)
|
|
||||||
useChatLogQuery()
|
|
||||||
|
|
||||||
const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
|
const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
(info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
|
(info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
|
||||||
|
@ -93,23 +80,11 @@ export function MessagesList({chatId}: {chatId: string}) {
|
||||||
if (isAtBottom.current) {
|
if (isAtBottom.current) {
|
||||||
flatListRef.current?.scrollToOffset({offset: 0, animated: true})
|
flatListRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
isFetching.current = false
|
|
||||||
setShowSpinner(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onEndReached = useCallback(() => {
|
const onEndReached = useCallback(() => {
|
||||||
if (isFetching.current) return
|
chat.service.fetchMessageHistory()
|
||||||
isFetching.current = true
|
}, [chat])
|
||||||
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)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onInputFocus = useCallback(() => {
|
const onInputFocus = useCallback(() => {
|
||||||
if (!isAtBottom.current) {
|
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 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 (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={{flex: 1, marginBottom: isWeb ? 20 : 85}}
|
style={{flex: 1, marginBottom: isWeb ? 20 : 85}}
|
||||||
behavior="padding"
|
behavior="padding"
|
||||||
keyboardVerticalOffset={70}
|
keyboardVerticalOffset={70}
|
||||||
contentContainerStyle={{flex: 1}}>
|
contentContainerStyle={{flex: 1}}>
|
||||||
|
{chat.state.status === ConvoStatus.Ready && (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={messages}
|
data={chat.state.items}
|
||||||
keyExtractor={item => item.message.id}
|
keyExtractor={item => item.key}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
contentContainerStyle={{paddingHorizontal: 10}}
|
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
|
// 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
|
// dealing with text. But whenever we have images or other media and things are taller, we will want to lower
|
||||||
// this...probably.
|
// this...probably.
|
||||||
initialNumToRender={20}
|
initialNumToRender={20}
|
||||||
// Same with the max to render per batch. Let's be safe for now though.
|
// Same with the max to render per batch. Let's be safe for now though.
|
||||||
maxToRenderPerBatch={25}
|
maxToRenderPerBatch={25}
|
||||||
removeClippedSubviews={true}
|
inverted={true}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onScrollToIndexFailed={onScrollToEndFailed}
|
onScrollToIndexFailed={onScrollToEndFailed}
|
||||||
onContentSizeChange={onContentSizeChange}
|
onContentSizeChange={onContentSizeChange}
|
||||||
onViewableItemsChanged={onViewableItemsChanged}
|
onViewableItemsChanged={onViewableItemsChanged}
|
||||||
viewabilityConfig={viewabilityConfig}
|
viewabilityConfig={viewabilityConfig}
|
||||||
maintainVisibleContentPosition={{
|
maintainVisibleContentPosition={{
|
||||||
minIndexForVisible: 1,
|
minIndexForVisible: 0,
|
||||||
}}
|
}}
|
||||||
ListFooterComponent={<MaybeLoader isLoading={false} />}
|
ListFooterComponent={
|
||||||
|
<MaybeLoader isLoading={chat.state.isFetchingHistory} />
|
||||||
|
}
|
||||||
|
removeClippedSubviews={true}
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
keyboardDismissMode="none"
|
keyboardDismissMode="none"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={{paddingHorizontal: 10}}>
|
<View style={{paddingHorizontal: 10}}>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSendMessage={onSendMessage}
|
onSendMessage={text => {
|
||||||
|
chat.service.sendMessage({
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
}}
|
||||||
onFocus={onInputFocus}
|
onFocus={onInputFocus}
|
||||||
onBlur={onInputBlur}
|
onBlur={onInputBlur}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,13 +9,13 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
|
import {useConvoQuery} from '#/state/queries/messages/conversation'
|
||||||
import {BACK_HITSLOP} from 'lib/constants'
|
import {BACK_HITSLOP} from 'lib/constants'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from 'state/session'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
||||||
import {useChatQuery} from '#/screens/Messages/Temp/query/query'
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {DotGrid_Stroke2_Corner0_Rounded} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded} from '#/components/icons/DotGrid'
|
||||||
|
@ -29,11 +29,11 @@ type Props = NativeStackScreenProps<
|
||||||
>
|
>
|
||||||
export function MessagesConversationScreen({route}: Props) {
|
export function MessagesConversationScreen({route}: Props) {
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
const chatId = route.params.conversation
|
const convoId = route.params.conversation
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const myDid = currentAccount?.did
|
const myDid = currentAccount?.did
|
||||||
|
|
||||||
const {data: chat, isError: isError} = useChatQuery(chatId)
|
const {data: chat, isError: isError} = useConvoQuery(convoId)
|
||||||
const otherProfile = React.useMemo(() => {
|
const otherProfile = React.useMemo(() => {
|
||||||
return chat?.members?.find(m => m.did !== myDid)
|
return chat?.members?.find(m => m.did !== myDid)
|
||||||
}, [chat?.members, myDid])
|
}, [chat?.members, myDid])
|
||||||
|
@ -51,7 +51,7 @@ export function MessagesConversationScreen({route}: Props) {
|
||||||
return (
|
return (
|
||||||
<CenteredView style={{flex: 1}} sideBorders>
|
<CenteredView style={{flex: 1}} sideBorders>
|
||||||
<Header profile={otherProfile} />
|
<Header profile={otherProfile} />
|
||||||
<MessagesList chatId={chatId} />
|
<MessagesList convoId={convoId} />
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React, {useCallback, useMemo, useState} from 'react'
|
import React, {useCallback, useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {ChatBskyConvoDefs} from '@atproto-labs/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
@ -11,24 +12,22 @@ import {MessagesTabNavigatorParams} from '#/lib/routes/types'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {useListConvos} from '#/state/queries/messages/list-converations'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
import {useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {DialogControlProps, useDialogControl} from '#/components/Dialog'
|
import {DialogControlProps, useDialogControl} from '#/components/Dialog'
|
||||||
|
import {NewChat} from '#/components/dms/NewChat'
|
||||||
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
|
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
|
||||||
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
|
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
|
||||||
import {NewChat} from '../../../components/dms/NewChat'
|
|
||||||
import {ClipClopGate} from '../gate'
|
import {ClipClopGate} from '../gate'
|
||||||
import {useListChats} from '../Temp/query/query'
|
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'>
|
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'>
|
||||||
export function MessagesListScreen({navigation}: Props) {
|
export function MessagesListScreen({navigation}: Props) {
|
||||||
|
@ -59,13 +58,13 @@ export function MessagesListScreen({navigation}: Props) {
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useListChats()
|
} = useListConvos()
|
||||||
|
|
||||||
const isError = !!error
|
const isError = !!error
|
||||||
|
|
||||||
const conversations = useMemo(() => {
|
const conversations = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
return data.pages.flatMap(page => page.chats)
|
return data.pages.flatMap(page => page.convos)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [data])
|
}, [data])
|
||||||
|
@ -99,9 +98,12 @@ export function MessagesListScreen({navigation}: Props) {
|
||||||
navigation.navigate('MessagesSettings')
|
navigation.navigate('MessagesSettings')
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
const renderItem = useCallback(({item}: {item: TempDmChatDefs.ChatView}) => {
|
const renderItem = useCallback(
|
||||||
return <ChatListItem key={item.id} chat={item} />
|
({item}: {item: ChatBskyConvoDefs.ConvoView}) => {
|
||||||
}, [])
|
return <ChatListItem key={item.id} convo={item} />
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
if (!gate('dms')) return <ClipClopGate />
|
if (!gate('dms')) return <ClipClopGate />
|
||||||
|
@ -119,7 +121,7 @@ export function MessagesListScreen({navigation}: Props) {
|
||||||
errorMessage={cleanError(error)}
|
errorMessage={cleanError(error)}
|
||||||
onRetry={isError ? refetch : undefined}
|
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 t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
let lastMessage = _(msg`No messages yet`)
|
let lastMessage = _(msg`No messages yet`)
|
||||||
let lastMessageSentAt: string | null = null
|
let lastMessageSentAt: string | null = null
|
||||||
if (TempDmChatDefs.isMessageView(chat.lastMessage)) {
|
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
||||||
if (chat.lastMessage.sender?.did === currentAccount?.did) {
|
if (convo.lastMessage.sender?.did === currentAccount?.did) {
|
||||||
lastMessage = _(msg`You: ${chat.lastMessage.text}`)
|
lastMessage = _(msg`You: ${convo.lastMessage.text}`)
|
||||||
} else {
|
} 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`)
|
lastMessage = _(msg`Message deleted`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherUser = chat.members.find(
|
const otherUser = convo.members.find(
|
||||||
member => member.did !== currentAccount?.did,
|
member => member.did !== currentAccount?.did,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -194,7 +196,7 @@ function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/messages/${chat.id}`} style={a.flex_1}>
|
<Link to={`/messages/${convo.id}`} style={a.flex_1}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -211,7 +213,8 @@ function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<Text numberOfLines={1} style={[a.text_md, a.leading_normal]}>
|
<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}
|
{otherUser.displayName || otherUser.handle}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
{lastMessageSentAt ? (
|
{lastMessageSentAt ? (
|
||||||
|
@ -233,14 +236,14 @@ function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
|
||||||
style={[
|
style={[
|
||||||
a.text_sm,
|
a.text_sm,
|
||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
chat.unreadCount > 0
|
convo.unreadCount > 0
|
||||||
? a.font_bold
|
? a.font_bold
|
||||||
: t.atoms.text_contrast_medium,
|
: t.atoms.text_contrast_medium,
|
||||||
]}>
|
]}>
|
||||||
{lastMessage}
|
{lastMessage}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{chat.unreadCount > 0 && (
|
{convo.unreadCount > 0 && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.flex_0,
|
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
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import {describe, it} from '@jest/globals'
|
||||||
|
|
||||||
|
describe(`#/state/dms/client`, () => {
|
||||||
|
describe(`ChatsService`, () => {
|
||||||
|
describe(`unread count`, () => {
|
||||||
|
it.todo(`marks a chat as read, decrements total unread count`)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`log processing`, () => {
|
||||||
|
/*
|
||||||
|
* We receive a new chat log AND messages for it in the same batch. We
|
||||||
|
* need to first initialize the chat, then process the received logs.
|
||||||
|
*/
|
||||||
|
describe(`handles new chats and subsequent messages received in same log batch`, () => {
|
||||||
|
it.todo(`receives new chat and messages`)
|
||||||
|
it.todo(
|
||||||
|
`receives new chat, new messages come in while still initializing new chat`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`reset state`, () => {
|
||||||
|
it.todo(`after period of inactivity, rehydrates entirely fresh state`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`ChatService`, () => {
|
||||||
|
describe(`history fetching`, () => {
|
||||||
|
it.todo(`fetches initial chat history`)
|
||||||
|
it.todo(`fetches additional chat history`)
|
||||||
|
it.todo(`handles history fetch failure`)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`optimistic updates`, () => {
|
||||||
|
it.todo(`adds sending messages`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,442 @@
|
||||||
|
import {
|
||||||
|
BskyAgent,
|
||||||
|
ChatBskyConvoDefs,
|
||||||
|
ChatBskyConvoSendMessage,
|
||||||
|
} from '@atproto-labs/api'
|
||||||
|
import {EventEmitter} from 'eventemitter3'
|
||||||
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
|
export type ConvoParams = {
|
||||||
|
convoId: string
|
||||||
|
agent: BskyAgent
|
||||||
|
__tempFromUserDid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConvoStatus {
|
||||||
|
Uninitialized = 'uninitialized',
|
||||||
|
Initializing = 'initializing',
|
||||||
|
Ready = 'ready',
|
||||||
|
Error = 'error',
|
||||||
|
Destroyed = 'destroyed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConvoItem =
|
||||||
|
| {
|
||||||
|
type: 'message'
|
||||||
|
key: string
|
||||||
|
message: ChatBskyConvoDefs.MessageView
|
||||||
|
nextMessage:
|
||||||
|
| ChatBskyConvoDefs.MessageView
|
||||||
|
| ChatBskyConvoDefs.DeletedMessageView
|
||||||
|
| null
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'deleted-message'
|
||||||
|
key: string
|
||||||
|
message: ChatBskyConvoDefs.DeletedMessageView
|
||||||
|
nextMessage:
|
||||||
|
| ChatBskyConvoDefs.MessageView
|
||||||
|
| ChatBskyConvoDefs.DeletedMessageView
|
||||||
|
| null
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'pending-message'
|
||||||
|
key: string
|
||||||
|
message: ChatBskyConvoSendMessage.InputSchema['message']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConvoState =
|
||||||
|
| {
|
||||||
|
status: ConvoStatus.Uninitialized
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: ConvoStatus.Initializing
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: ConvoStatus.Ready
|
||||||
|
items: ConvoItem[]
|
||||||
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
|
isFetchingHistory: boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: ConvoStatus.Error
|
||||||
|
error: any
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: ConvoStatus.Destroyed
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Convo {
|
||||||
|
private convoId: string
|
||||||
|
private agent: BskyAgent
|
||||||
|
private __tempFromUserDid: string
|
||||||
|
|
||||||
|
private status: ConvoStatus = ConvoStatus.Uninitialized
|
||||||
|
private error: any
|
||||||
|
private convo: ChatBskyConvoDefs.ConvoView | undefined
|
||||||
|
private historyCursor: string | undefined | null = undefined
|
||||||
|
private isFetchingHistory = false
|
||||||
|
private eventsCursor: string | undefined = undefined
|
||||||
|
|
||||||
|
private pastMessages: Map<
|
||||||
|
string,
|
||||||
|
ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView
|
||||||
|
> = new Map()
|
||||||
|
private newMessages: Map<
|
||||||
|
string,
|
||||||
|
ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView
|
||||||
|
> = new Map()
|
||||||
|
private pendingMessages: Map<
|
||||||
|
string,
|
||||||
|
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
|
||||||
|
> = new Map()
|
||||||
|
|
||||||
|
private pendingEventIngestion: Promise<void> | undefined
|
||||||
|
|
||||||
|
constructor(params: ConvoParams) {
|
||||||
|
this.convoId = params.convoId
|
||||||
|
this.agent = params.agent
|
||||||
|
this.__tempFromUserDid = params.__tempFromUserDid
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
if (this.status !== 'uninitialized') return
|
||||||
|
this.status = ConvoStatus.Initializing
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.agent.api.chat.bsky.convo.getConvo(
|
||||||
|
{
|
||||||
|
convoId: this.convoId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: this.__tempFromUserDid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const {convo} = response.data
|
||||||
|
|
||||||
|
this.convo = convo
|
||||||
|
this.status = ConvoStatus.Ready
|
||||||
|
|
||||||
|
this.commit()
|
||||||
|
|
||||||
|
await this.fetchMessageHistory()
|
||||||
|
|
||||||
|
this.pollEvents()
|
||||||
|
} catch (e) {
|
||||||
|
this.status = ConvoStatus.Error
|
||||||
|
this.error = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pollEvents() {
|
||||||
|
if (this.status === ConvoStatus.Destroyed) return
|
||||||
|
if (this.pendingEventIngestion) return
|
||||||
|
setTimeout(async () => {
|
||||||
|
this.pendingEventIngestion = this.ingestLatestEvents()
|
||||||
|
await this.pendingEventIngestion
|
||||||
|
this.pendingEventIngestion = undefined
|
||||||
|
this.pollEvents()
|
||||||
|
}, 5e3)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMessageHistory() {
|
||||||
|
if (this.status === ConvoStatus.Destroyed) return
|
||||||
|
// reached end
|
||||||
|
if (this.historyCursor === null) return
|
||||||
|
if (this.isFetchingHistory) return
|
||||||
|
|
||||||
|
this.isFetchingHistory = true
|
||||||
|
this.commit()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Delay if paginating while scrolled.
|
||||||
|
*
|
||||||
|
* TODO why does the FlatList jump without this delay?
|
||||||
|
*
|
||||||
|
* Tbh it feels a little more natural with a slight delay.
|
||||||
|
*/
|
||||||
|
if (this.pastMessages.size > 0) {
|
||||||
|
await new Promise(y => setTimeout(y, 500))
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.agent.api.chat.bsky.convo.getMessages(
|
||||||
|
{
|
||||||
|
cursor: this.historyCursor,
|
||||||
|
convoId: this.convoId,
|
||||||
|
limit: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: this.__tempFromUserDid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const {cursor, messages} = response.data
|
||||||
|
|
||||||
|
this.historyCursor = cursor || null
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (
|
||||||
|
ChatBskyConvoDefs.isMessageView(message) ||
|
||||||
|
ChatBskyConvoDefs.isDeletedMessageView(message)
|
||||||
|
) {
|
||||||
|
this.pastMessages.set(message.id, message)
|
||||||
|
|
||||||
|
// set to latest rev
|
||||||
|
if (
|
||||||
|
message.rev > (this.eventsCursor = this.eventsCursor || message.rev)
|
||||||
|
) {
|
||||||
|
this.eventsCursor = message.rev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetchingHistory = false
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
async ingestLatestEvents() {
|
||||||
|
if (this.status === ConvoStatus.Destroyed) return
|
||||||
|
|
||||||
|
const response = await this.agent.api.chat.bsky.convo.getLog(
|
||||||
|
{
|
||||||
|
cursor: this.eventsCursor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: this.__tempFromUserDid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const {logs} = response.data
|
||||||
|
|
||||||
|
for (const log of logs) {
|
||||||
|
/*
|
||||||
|
* If there's a rev, we should handle it. If there's not a rev, we don't
|
||||||
|
* know what it is.
|
||||||
|
*/
|
||||||
|
if (typeof log.rev === 'string') {
|
||||||
|
/*
|
||||||
|
* We only care about new events
|
||||||
|
*/
|
||||||
|
if (log.rev > (this.eventsCursor = this.eventsCursor || log.rev)) {
|
||||||
|
/*
|
||||||
|
* Update rev regardless of if it's a log type we care about or not
|
||||||
|
*/
|
||||||
|
this.eventsCursor = log.rev
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is VERY important. We don't want to insert any messages from
|
||||||
|
* your other chats.
|
||||||
|
*
|
||||||
|
* TODO there may be a better way to handle this
|
||||||
|
*/
|
||||||
|
if (log.convoId !== this.convoId) continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
ChatBskyConvoDefs.isLogCreateMessage(log) &&
|
||||||
|
ChatBskyConvoDefs.isMessageView(log.message)
|
||||||
|
) {
|
||||||
|
if (this.newMessages.has(log.message.id)) {
|
||||||
|
// Trust the log as the source of truth on ordering
|
||||||
|
// TODO test this
|
||||||
|
this.newMessages.delete(log.message.id)
|
||||||
|
}
|
||||||
|
this.newMessages.set(log.message.id, log.message)
|
||||||
|
} else if (
|
||||||
|
ChatBskyConvoDefs.isLogDeleteMessage(log) &&
|
||||||
|
ChatBskyConvoDefs.isDeletedMessageView(log.message)
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
* Update if we have this in state. If we don't, don't worry about it.
|
||||||
|
*/
|
||||||
|
if (this.pastMessages.has(log.message.id)) {
|
||||||
|
/*
|
||||||
|
* For now, we remove deleted messages from the thread, if we receive one.
|
||||||
|
*
|
||||||
|
* To support them, it'd look something like this:
|
||||||
|
* this.pastMessages.set(log.message.id, log.message)
|
||||||
|
*/
|
||||||
|
this.pastMessages.delete(log.message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
|
||||||
|
if (this.status === ConvoStatus.Destroyed) return
|
||||||
|
// Ignore empty messages for now since they have no other purpose atm
|
||||||
|
if (!message.text) return
|
||||||
|
|
||||||
|
const tempId = nanoid()
|
||||||
|
|
||||||
|
this.pendingMessages.set(tempId, {
|
||||||
|
id: tempId,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
this.commit()
|
||||||
|
|
||||||
|
await new Promise(y => setTimeout(y, 500))
|
||||||
|
const response = await this.agent.api.chat.bsky.convo.sendMessage(
|
||||||
|
{
|
||||||
|
convoId: this.convoId,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encoding: 'application/json',
|
||||||
|
headers: {
|
||||||
|
Authorization: this.__tempFromUserDid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const res = response.data
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Insert into `newMessages` as soon as we have a real ID. That way, when
|
||||||
|
* we get an event log back, we can replace in situ.
|
||||||
|
*/
|
||||||
|
this.newMessages.set(res.id, {
|
||||||
|
...res,
|
||||||
|
$type: 'chat.bsky.convo.defs#messageView',
|
||||||
|
sender: this.convo?.members.find(m => m.did === this.__tempFromUserDid),
|
||||||
|
})
|
||||||
|
this.pendingMessages.delete(tempId)
|
||||||
|
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Items in reverse order, since FlatList inverts
|
||||||
|
*/
|
||||||
|
get items(): ConvoItem[] {
|
||||||
|
const items: ConvoItem[] = []
|
||||||
|
|
||||||
|
// `newMessages` is in insertion order, unshift to reverse
|
||||||
|
this.newMessages.forEach(m => {
|
||||||
|
if (ChatBskyConvoDefs.isMessageView(m)) {
|
||||||
|
items.unshift({
|
||||||
|
type: 'message',
|
||||||
|
key: m.id,
|
||||||
|
message: m,
|
||||||
|
nextMessage: null,
|
||||||
|
})
|
||||||
|
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
|
||||||
|
items.unshift({
|
||||||
|
type: 'deleted-message',
|
||||||
|
key: m.id,
|
||||||
|
message: m,
|
||||||
|
nextMessage: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// `newMessages` is in insertion order, unshift to reverse
|
||||||
|
this.pendingMessages.forEach(m => {
|
||||||
|
items.unshift({
|
||||||
|
type: 'pending-message',
|
||||||
|
key: m.id,
|
||||||
|
message: m.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pastMessages.forEach(m => {
|
||||||
|
if (ChatBskyConvoDefs.isMessageView(m)) {
|
||||||
|
items.push({
|
||||||
|
type: 'message',
|
||||||
|
key: m.id,
|
||||||
|
message: m,
|
||||||
|
nextMessage: null,
|
||||||
|
})
|
||||||
|
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
|
||||||
|
items.push({
|
||||||
|
type: 'deleted-message',
|
||||||
|
key: m.id,
|
||||||
|
message: m,
|
||||||
|
nextMessage: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return items.map((item, i) => {
|
||||||
|
let nextMessage = null
|
||||||
|
|
||||||
|
if (
|
||||||
|
ChatBskyConvoDefs.isMessageView(item.message) ||
|
||||||
|
ChatBskyConvoDefs.isDeletedMessageView(item.message)
|
||||||
|
) {
|
||||||
|
const next = items[i - 1]
|
||||||
|
if (
|
||||||
|
next &&
|
||||||
|
(ChatBskyConvoDefs.isMessageView(next.message) ||
|
||||||
|
ChatBskyConvoDefs.isDeletedMessageView(next.message))
|
||||||
|
) {
|
||||||
|
nextMessage = next.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
nextMessage,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.status = ConvoStatus.Destroyed
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
get state(): ConvoState {
|
||||||
|
switch (this.status) {
|
||||||
|
case ConvoStatus.Initializing: {
|
||||||
|
return {
|
||||||
|
status: ConvoStatus.Initializing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ConvoStatus.Ready: {
|
||||||
|
return {
|
||||||
|
status: ConvoStatus.Ready,
|
||||||
|
items: this.items,
|
||||||
|
convo: this.convo!,
|
||||||
|
isFetchingHistory: this.isFetchingHistory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ConvoStatus.Error: {
|
||||||
|
return {
|
||||||
|
status: ConvoStatus.Error,
|
||||||
|
error: this.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ConvoStatus.Destroyed: {
|
||||||
|
return {
|
||||||
|
status: ConvoStatus.Destroyed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
status: ConvoStatus.Uninitialized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _emitter = new EventEmitter()
|
||||||
|
|
||||||
|
private commit() {
|
||||||
|
this._emitter.emit('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: 'update', cb: () => void) {
|
||||||
|
this._emitter.on(event, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: 'update', cb: () => void) {
|
||||||
|
this._emitter.off(event, cb)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {BskyAgent} from '@atproto-labs/api'
|
||||||
|
|
||||||
|
import {Convo, ConvoParams} from '#/state/messages/convo'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
|
|
||||||
|
const ChatContext = React.createContext<{
|
||||||
|
service: Convo
|
||||||
|
state: Convo['state']
|
||||||
|
}>({
|
||||||
|
// @ts-ignore
|
||||||
|
service: null,
|
||||||
|
// @ts-ignore
|
||||||
|
state: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useChat() {
|
||||||
|
return React.useContext(ChatContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatProvider({
|
||||||
|
children,
|
||||||
|
convoId,
|
||||||
|
}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) {
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
const [service] = React.useState(
|
||||||
|
() =>
|
||||||
|
new Convo({
|
||||||
|
convoId,
|
||||||
|
agent: new BskyAgent({
|
||||||
|
service: serviceUrl,
|
||||||
|
}),
|
||||||
|
__tempFromUserDid: getAgent().session?.did!,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const [state, setState] = React.useState(service.state)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
service.initialize()
|
||||||
|
}, [service])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const update = () => setState(service.state)
|
||||||
|
service.on('update', update)
|
||||||
|
return () => {
|
||||||
|
service.destroy()
|
||||||
|
}
|
||||||
|
}, [service])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatContext.Provider value={{state, service}}>
|
||||||
|
{children}
|
||||||
|
</ChatContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import {BskyAgent} from '@atproto-labs/api'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
|
import {useHeaders} from './temp-headers'
|
||||||
|
|
||||||
|
const RQKEY_ROOT = 'convo'
|
||||||
|
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
|
||||||
|
|
||||||
|
export function useConvoQuery(convoId: string) {
|
||||||
|
const headers = useHeaders()
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: RQKEY(convoId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
const {data} = await agent.api.chat.bsky.convo.getConvo(
|
||||||
|
{convoId},
|
||||||
|
{headers},
|
||||||
|
)
|
||||||
|
return data.convo
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {BskyAgent, ChatBskyConvoGetConvoForMembers} from '@atproto-labs/api'
|
||||||
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
|
import {RQKEY as CONVO_KEY} from './conversation'
|
||||||
|
import {useHeaders} from './temp-headers'
|
||||||
|
|
||||||
|
export function useGetConvoForMembers({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess?: (data: ChatBskyConvoGetConvoForMembers.OutputSchema) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const headers = useHeaders()
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (members: string[]) => {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
const {data} = await agent.api.chat.bsky.convo.getConvoForMembers(
|
||||||
|
{members: members},
|
||||||
|
{headers},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
onSuccess: data => {
|
||||||
|
queryClient.setQueryData(CONVO_KEY(data.convo.id), data.convo)
|
||||||
|
onSuccess?.(data)
|
||||||
|
},
|
||||||
|
onError,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import {BskyAgent} from '@atproto-labs/api'
|
||||||
|
import {useInfiniteQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
|
import {useHeaders} from './temp-headers'
|
||||||
|
|
||||||
|
export const RQKEY = ['convo-list']
|
||||||
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
|
export function useListConvos() {
|
||||||
|
const headers = useHeaders()
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: RQKEY,
|
||||||
|
queryFn: async ({pageParam}) => {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
const {data} = await agent.api.chat.bsky.convo.listConvos(
|
||||||
|
{cursor: pageParam},
|
||||||
|
{headers},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
initialPageParam: undefined as RQPageParam,
|
||||||
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
|
// toy auth
|
||||||
|
export const useHeaders = () => {
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
return {
|
||||||
|
get Authorization() {
|
||||||
|
return currentAccount!.did
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,195 +0,0 @@
|
||||||
import {
|
|
||||||
AppBskyActorDefs,
|
|
||||||
AppBskyEmbedRecord,
|
|
||||||
AppBskyRichtextFacet,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {ValidationResult} from '@atproto/lexicon'
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
id?: string
|
|
||||||
text: string
|
|
||||||
/** Annotations of text (mentions, URLs, hashtags, etc) */
|
|
||||||
facets?: AppBskyRichtextFacet.Main[]
|
|
||||||
embed?: AppBskyEmbedRecord.Main | {$type: string; [k: string]: unknown}
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMessage(v: unknown): v is Message {
|
|
||||||
return isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#message'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateMessage(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageView {
|
|
||||||
id: string
|
|
||||||
rev: string
|
|
||||||
text: string
|
|
||||||
/** Annotations of text (mentions, URLs, hashtags, etc) */
|
|
||||||
facets?: AppBskyRichtextFacet.Main[]
|
|
||||||
embed?: AppBskyEmbedRecord.Main | {$type: string; [k: string]: unknown}
|
|
||||||
sender?: MessageViewSender
|
|
||||||
sentAt: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMessageView(v: unknown): v is MessageView {
|
|
||||||
return (
|
|
||||||
isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#messageView'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateMessageView(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeletedMessage {
|
|
||||||
id: string
|
|
||||||
rev?: string
|
|
||||||
sender?: MessageViewSender
|
|
||||||
sentAt: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDeletedMessage(v: unknown): v is DeletedMessage {
|
|
||||||
return (
|
|
||||||
isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#deletedMessage'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateDeletedMessage(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageViewSender {
|
|
||||||
did: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMessageViewSender(v: unknown): v is MessageViewSender {
|
|
||||||
return (
|
|
||||||
isObj(v) &&
|
|
||||||
hasProp(v, '$type') &&
|
|
||||||
v.$type === 'temp.dm.defs#messageViewSender'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateMessageViewSender(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatView {
|
|
||||||
id: string
|
|
||||||
rev: string
|
|
||||||
members: AppBskyActorDefs.ProfileViewBasic[]
|
|
||||||
lastMessage?:
|
|
||||||
| MessageView
|
|
||||||
| DeletedMessage
|
|
||||||
| {$type: string; [k: string]: unknown}
|
|
||||||
unreadCount: number
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isChatView(v: unknown): v is ChatView {
|
|
||||||
return isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#chatView'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateChatView(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IncomingMessageSetting =
|
|
||||||
| 'all'
|
|
||||||
| 'none'
|
|
||||||
| 'following'
|
|
||||||
| (string & {})
|
|
||||||
|
|
||||||
export interface LogBeginChat {
|
|
||||||
rev: string
|
|
||||||
chatId: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLogBeginChat(v: unknown): v is LogBeginChat {
|
|
||||||
return (
|
|
||||||
isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#logBeginChat'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateLogBeginChat(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LogCreateMessage {
|
|
||||||
rev: string
|
|
||||||
chatId: string
|
|
||||||
message: MessageView | DeletedMessage | {$type: string; [k: string]: unknown}
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLogCreateMessage(v: unknown): v is LogCreateMessage {
|
|
||||||
return (
|
|
||||||
isObj(v) &&
|
|
||||||
hasProp(v, '$type') &&
|
|
||||||
v.$type === 'temp.dm.defs#logCreateMessage'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateLogCreateMessage(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LogDeleteMessage {
|
|
||||||
rev: string
|
|
||||||
chatId: string
|
|
||||||
message: MessageView | DeletedMessage | {$type: string; [k: string]: unknown}
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLogDeleteMessage(v: unknown): v is LogDeleteMessage {
|
|
||||||
return (
|
|
||||||
isObj(v) &&
|
|
||||||
hasProp(v, '$type') &&
|
|
||||||
v.$type === 'temp.dm.defs#logDeleteMessage'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateLogDeleteMessage(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isObj(v: unknown): v is Record<string, unknown> {
|
|
||||||
return typeof v === 'object' && v !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasProp<K extends PropertyKey>(
|
|
||||||
data: object,
|
|
||||||
prop: K,
|
|
||||||
): data is Record<K, unknown> {
|
|
||||||
return prop in data
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
chatId: string
|
|
||||||
messageId: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OutputSchema = TempDmDefs.DeletedMessage
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {
|
|
||||||
chatId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InputSchema = undefined
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
chat: TempDmDefs.ChatView
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {
|
|
||||||
members: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InputSchema = undefined
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
chat: TempDmDefs.ChatView
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {
|
|
||||||
cursor?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InputSchema = undefined
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
cursor?: string
|
|
||||||
logs: (
|
|
||||||
| TempDmDefs.LogBeginChat
|
|
||||||
| TempDmDefs.LogCreateMessage
|
|
||||||
| TempDmDefs.LogDeleteMessage
|
|
||||||
| {$type: string; [k: string]: unknown}
|
|
||||||
)[]
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {
|
|
||||||
chatId: string
|
|
||||||
limit?: number
|
|
||||||
cursor?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InputSchema = undefined
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
cursor?: string
|
|
||||||
messages: (
|
|
||||||
| TempDmDefs.MessageView
|
|
||||||
| TempDmDefs.DeletedMessage
|
|
||||||
| {$type: string; [k: string]: unknown}
|
|
||||||
)[]
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export type InputSchema = undefined
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
allowIncoming: TempDmDefs.IncomingMessageSetting
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
chatId: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {
|
|
||||||
limit?: number
|
|
||||||
cursor?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InputSchema = undefined
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
cursor?: string
|
|
||||||
chats: TempDmDefs.ChatView[]
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
chatId: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
chatId: string
|
|
||||||
message: TempDmDefs.Message
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OutputSchema = TempDmDefs.MessageView
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import {ValidationResult} from '@atproto/lexicon'
|
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
items: BatchItem[]
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
items: TempDmDefs.MessageView[]
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BatchItem {
|
|
||||||
chatId: string
|
|
||||||
message: TempDmDefs.Message
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isBatchItem(v: unknown): v is BatchItem {
|
|
||||||
return (
|
|
||||||
isObj(v) &&
|
|
||||||
hasProp(v, '$type') &&
|
|
||||||
v.$type === 'temp.dm.sendMessageBatch#batchItem'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateBatchItem(v: unknown): ValidationResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isObj(v: unknown): v is Record<string, unknown> {
|
|
||||||
return typeof v === 'object' && v !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasProp<K extends PropertyKey>(
|
|
||||||
data: object,
|
|
||||||
prop: K,
|
|
||||||
): data is Record<K, unknown> {
|
|
||||||
return prop in data
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
chatId: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
chatId: string
|
|
||||||
messageId?: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OutputSchema = TempDmDefs.ChatView
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import {Headers, XRPCError} from '@atproto/xrpc'
|
|
||||||
|
|
||||||
import * as TempDmDefs from './defs'
|
|
||||||
|
|
||||||
export interface QueryParams {}
|
|
||||||
|
|
||||||
export interface InputSchema {
|
|
||||||
allowIncoming?: TempDmDefs.IncomingMessageSetting
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputSchema {
|
|
||||||
allowIncoming: TempDmDefs.IncomingMessageSetting
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallOptions {
|
|
||||||
headers?: Headers
|
|
||||||
qp?: QueryParams
|
|
||||||
encoding: 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
success: boolean
|
|
||||||
headers: Headers
|
|
||||||
data: OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toKnownErr(e: any) {
|
|
||||||
if (e instanceof XRPCError) {
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
12
yarn.lock
12
yarn.lock
|
@ -34,6 +34,18 @@
|
||||||
jsonpointer "^5.0.0"
|
jsonpointer "^5.0.0"
|
||||||
leven "^3.1.0"
|
leven "^3.1.0"
|
||||||
|
|
||||||
|
"@atproto-labs/api@^0.12.8-clipclops.0":
|
||||||
|
version "0.12.8-clipclops.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto-labs/api/-/api-0.12.8-clipclops.0.tgz#1c5d41d3396e439a0b645f7e1ccf500cc4b42580"
|
||||||
|
integrity sha512-YYDtWWk6BR+aRBVja/1v+gceNK81lkmF5bi6O4pTmJhFt/321XATx/ql8uTWta4VnVThoFeNPG6nLr7hs8b9cA==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/common-web" "^0.3.0"
|
||||||
|
"@atproto/lexicon" "^0.4.0"
|
||||||
|
"@atproto/syntax" "^0.3.0"
|
||||||
|
"@atproto/xrpc" "^0.5.0"
|
||||||
|
multiformats "^9.9.0"
|
||||||
|
tlds "^1.234.0"
|
||||||
|
|
||||||
"@atproto/api@^0.12.3":
|
"@atproto/api@^0.12.3":
|
||||||
version "0.12.3"
|
version "0.12.3"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.3.tgz#5b7b1c7d4210ee9315961504900c8409395cbb17"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.3.tgz#5b7b1c7d4210ee9315961504900c8409395cbb17"
|
||||||
|
|
Loading…
Reference in New Issue