diff --git a/package.json b/package.json index 874a55c1..9df20604 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { + "@atproto-labs/api": "^0.12.8-clipclops.0", "@atproto/api": "^0.12.5", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", diff --git a/src/components/dms/NewChat.tsx b/src/components/dms/NewChat.tsx index 3ad1ce7e..55285dae 100644 --- a/src/components/dms/NewChat.tsx +++ b/src/components/dms/NewChat.tsx @@ -9,6 +9,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isWeb} from '#/platform/detection' 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 {FAB} from '#/view/com/util/fab/FAB' 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 TextField from '#/components/forms/TextField' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {useGetChatFromMembers} from '../../screens/Messages/Temp/query/query' import {Button} from '../Button' import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope' import {ListMaybePlaceholder} from '../Lists' @@ -33,9 +33,9 @@ export function NewChat({ const t = useTheme() const {_} = useLingui() - const {mutate: createChat} = useGetChatFromMembers({ + const {mutate: createChat} = useGetConvoForMembers({ onSuccess: data => { - onNewChat(data.chat.id) + onNewChat(data.convo.id) }, onError: error => { Toast.show(error.message) diff --git a/src/screens/Messages/Conversation/MessageItem.tsx b/src/screens/Messages/Conversation/MessageItem.tsx index 85e1c5f3..ba10978e 100644 --- a/src/screens/Messages/Conversation/MessageItem.tsx +++ b/src/screens/Messages/Conversation/MessageItem.tsx @@ -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 }) { diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 25aaf28c..ca32a6d6 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -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 - - if (TempDmChatDefs.isDeletedMessage(item)) return Deleted message +function renderItem({item}: {item: ConvoItem}) { + if (item.type === 'message') { + return + } else if (item.type === 'deleted-message') { + return Deleted message + } else if (item.type === 'pending-message') { + return {item.message.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 ( + + + + ) +} + +export function MessagesListInner() { + const chat = useChat() const flatListRef = useRef(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; changed: Array}) => { @@ -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 ( - 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={} - ref={flatListRef} - keyboardDismissMode="none" - /> + {chat.state.status === ConvoStatus.Ready && ( + 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={ + + } + removeClippedSubviews={true} + ref={flatListRef} + keyboardDismissMode="none" + /> + )} + { + chat.service.sendMessage({ + text, + }) + }} onFocus={onInputFocus} onBlur={onInputBlur} /> diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx index d0691385..9c5fd791 100644 --- a/src/screens/Messages/Conversation/index.tsx +++ b/src/screens/Messages/Conversation/index.tsx @@ -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 (
- + ) } diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx index ccf0d204..65a2ff1e 100644 --- a/src/screens/Messages/List/index.tsx +++ b/src/screens/Messages/List/index.tsx @@ -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 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 - }, []) + const renderItem = useCallback( + ({item}: {item: ChatBskyConvoDefs.ConvoView}) => { + return + }, + [], + ) const gate = useGate() if (!gate('dms')) return @@ -119,7 +121,7 @@ export function MessagesListScreen({navigation}: Props) { errorMessage={cleanError(error)} onRetry={isError ? refetch : undefined} /> - + {!isError && } ) } @@ -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 ( - + {({hovered, pressed}) => ( - 0 && a.font_bold]}> + 0 && a.font_bold]}> {otherUser.displayName || otherUser.handle} {' '} {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} - {chat.unreadCount > 0 && ( + {convo.unreadCount > 0 && ( { - 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 - }, - }) -} diff --git a/src/state/messages/__tests__/client.test.ts b/src/state/messages/__tests__/client.test.ts new file mode 100644 index 00000000..cab1d902 --- /dev/null +++ b/src/state/messages/__tests__/client.test.ts @@ -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`) + }) + }) +}) diff --git a/src/state/messages/convo.ts b/src/state/messages/convo.ts new file mode 100644 index 00000000..a1de1dbe --- /dev/null +++ b/src/state/messages/convo.ts @@ -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 | 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) + } +} diff --git a/src/state/messages/index.tsx b/src/state/messages/index.tsx new file mode 100644 index 00000000..c5991525 --- /dev/null +++ b/src/state/messages/index.tsx @@ -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 & {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 ( + + {children} + + ) +} diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts new file mode 100644 index 00000000..9456861d --- /dev/null +++ b/src/state/queries/messages/conversation.ts @@ -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 + }, + }) +} diff --git a/src/state/queries/messages/get-convo-for-members.ts b/src/state/queries/messages/get-convo-for-members.ts new file mode 100644 index 00000000..8a58a98d --- /dev/null +++ b/src/state/queries/messages/get-convo-for-members.ts @@ -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, + }) +} diff --git a/src/state/queries/messages/list-converations.ts b/src/state/queries/messages/list-converations.ts new file mode 100644 index 00000000..19f2674b --- /dev/null +++ b/src/state/queries/messages/list-converations.ts @@ -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, + }) +} diff --git a/src/state/queries/messages/temp-headers.ts b/src/state/queries/messages/temp-headers.ts new file mode 100644 index 00000000..9e46e8a6 --- /dev/null +++ b/src/state/queries/messages/temp-headers.ts @@ -0,0 +1,11 @@ +import {useSession} from '#/state/session' + +// toy auth +export const useHeaders = () => { + const {currentAccount} = useSession() + return { + get Authorization() { + return currentAccount!.did + }, + } +} diff --git a/src/temp/dm/defs.ts b/src/temp/dm/defs.ts deleted file mode 100644 index 91f68365..00000000 --- a/src/temp/dm/defs.ts +++ /dev/null @@ -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 { - return typeof v === 'object' && v !== null -} - -export function hasProp( - data: object, - prop: K, -): data is Record { - return prop in data -} diff --git a/src/temp/dm/deleteMessage.ts b/src/temp/dm/deleteMessage.ts deleted file mode 100644 index d9fa1f9c..00000000 --- a/src/temp/dm/deleteMessage.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/getChat.ts b/src/temp/dm/getChat.ts deleted file mode 100644 index d0a7b891..00000000 --- a/src/temp/dm/getChat.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/getChatForMembers.ts b/src/temp/dm/getChatForMembers.ts deleted file mode 100644 index 0c9962c8..00000000 --- a/src/temp/dm/getChatForMembers.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/getChatLog.ts b/src/temp/dm/getChatLog.ts deleted file mode 100644 index 9d310d90..00000000 --- a/src/temp/dm/getChatLog.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/getChatMessages.ts b/src/temp/dm/getChatMessages.ts deleted file mode 100644 index 54ae2191..00000000 --- a/src/temp/dm/getChatMessages.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/getUserSettings.ts b/src/temp/dm/getUserSettings.ts deleted file mode 100644 index 792c697b..00000000 --- a/src/temp/dm/getUserSettings.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/leaveChat.ts b/src/temp/dm/leaveChat.ts deleted file mode 100644 index e116f277..00000000 --- a/src/temp/dm/leaveChat.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/listChats.ts b/src/temp/dm/listChats.ts deleted file mode 100644 index 0f9cb0c6..00000000 --- a/src/temp/dm/listChats.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/muteChat.ts b/src/temp/dm/muteChat.ts deleted file mode 100644 index e116f277..00000000 --- a/src/temp/dm/muteChat.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/sendMessage.ts b/src/temp/dm/sendMessage.ts deleted file mode 100644 index 24a4cf73..00000000 --- a/src/temp/dm/sendMessage.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/sendMessageBatch.ts b/src/temp/dm/sendMessageBatch.ts deleted file mode 100644 index c2ce1d82..00000000 --- a/src/temp/dm/sendMessageBatch.ts +++ /dev/null @@ -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 { - return typeof v === 'object' && v !== null -} - -export function hasProp( - data: object, - prop: K, -): data is Record { - return prop in data -} diff --git a/src/temp/dm/unmuteChat.ts b/src/temp/dm/unmuteChat.ts deleted file mode 100644 index e116f277..00000000 --- a/src/temp/dm/unmuteChat.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/updateChatRead.ts b/src/temp/dm/updateChatRead.ts deleted file mode 100644 index 7eec7e4a..00000000 --- a/src/temp/dm/updateChatRead.ts +++ /dev/null @@ -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 -} diff --git a/src/temp/dm/updateUserSettings.ts b/src/temp/dm/updateUserSettings.ts deleted file mode 100644 index f88122f5..00000000 --- a/src/temp/dm/updateUserSettings.ts +++ /dev/null @@ -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 -} diff --git a/yarn.lock b/yarn.lock index 5d62929d..8c014e28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,18 @@ jsonpointer "^5.0.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": version "0.12.3" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.3.tgz#5b7b1c7d4210ee9315961504900c8409395cbb17"