From df4d6f14e63b8665016e7c30b3afe9dafe9af79a Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Fri, 17 May 2024 10:09:27 -0600 Subject: [PATCH] feat: Reactions Added Reaction support for messages Added persistance handling Refactored providers Removed conversation screen --- App.tsx | 81 +--- package.json | 4 +- src/components/ConversationMessageContent.tsx | 210 ----------- src/components/Message.tsx | 62 +++ .../ConversationMessageContent.tsx | 134 +++++++ .../MessageOptionsContainer.tsx | 112 ++++++ .../messageContent/TextMessageContent.tsx | 70 ++++ src/consts/ContentTypes.ts | 11 +- src/context/ClientContext.tsx | 71 +--- src/context/ConversationContext.tsx | 24 -- src/context/GroupContext.tsx | 19 + src/i18n/locales/en.json | 1 + src/providers/ClientProvider.tsx | 65 ++++ src/providers/NativeBaseProvider.tsx | 29 ++ src/providers/QueryClientProvider.tsx | 30 ++ src/providers/ThirdwebProvider.tsx | 34 ++ src/providers/WagmiProvider.tsx | 19 + src/providers/index.tsx | 20 + src/queries/useGroupMessagesQuery.ts | 73 +++- src/screens/ConversationScreen.tsx | 353 ------------------ src/screens/GroupScreen.tsx | 114 +++--- src/screens/OnboardingConnectWalletScreen.tsx | 5 +- src/screens/SearchScreen.tsx | 24 +- src/services/mmkvStorage.ts | 4 +- src/services/queryClient.ts | 9 + yarn.lock | 46 ++- 26 files changed, 805 insertions(+), 819 deletions(-) delete mode 100644 src/components/ConversationMessageContent.tsx create mode 100644 src/components/Message.tsx create mode 100644 src/components/messageContent/ConversationMessageContent.tsx create mode 100644 src/components/messageContent/MessageOptionsContainer.tsx create mode 100644 src/components/messageContent/TextMessageContent.tsx delete mode 100644 src/context/ConversationContext.tsx create mode 100644 src/context/GroupContext.tsx create mode 100644 src/providers/ClientProvider.tsx create mode 100644 src/providers/NativeBaseProvider.tsx create mode 100644 src/providers/QueryClientProvider.tsx create mode 100644 src/providers/ThirdwebProvider.tsx create mode 100644 src/providers/WagmiProvider.tsx create mode 100644 src/providers/index.tsx delete mode 100644 src/screens/ConversationScreen.tsx diff --git a/App.tsx b/App.tsx index 936bb84..6e8fb93 100644 --- a/App.tsx +++ b/App.tsx @@ -1,87 +1,14 @@ import './src/polyfills'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; -import {Ethereum} from '@thirdweb-dev/chains'; -import { - ThirdwebProvider, - localWallet, - metamaskWallet, - walletConnect, -} from '@thirdweb-dev/react-native'; -import {NativeBaseProvider, extendTheme} from 'native-base'; import React from 'react'; -import Config from 'react-native-config'; -import {mainnet} from 'viem/chains'; -import {WagmiConfig, configureChains, createConfig} from 'wagmi'; -import {publicProvider} from 'wagmi/providers/public'; -import {ClientProvider} from './src/context/ClientContext'; import {AppNavigation} from './src/navigation/AppNavigation'; -import {colors} from './src/theme/colors'; - -const {publicClient, webSocketPublicClient} = configureChains( - [mainnet], - [publicProvider()], -); - -const config = createConfig({ - autoConnect: true, - publicClient, - webSocketPublicClient, -}); - -const queryClient = new QueryClient(); +import {Providers} from './src/providers'; function App(): React.JSX.Element { - const newColorTheme = { - primary: { - 900: colors.actionPrimary, - 800: colors.actionPrimary, - 700: colors.actionPrimary, - 600: colors.actionPrimary, - 500: colors.actionPrimary, - 400: colors.actionPrimary, - 300: colors.actionPrimary, - }, - brand: { - 900: colors.actionPrimary, - 800: colors.actionPrimary, - 700: colors.actionPrimary, - 600: colors.actionPrimary, - 500: colors.actionPrimary, - 400: colors.actionPrimary, - 300: colors.actionPrimary, - }, - }; - const theme = extendTheme({colors: newColorTheme}); return ( - - - - - - - - - - - + + + ); } diff --git a/package.json b/package.json index 34748ff..df205f1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "@react-native-community/netinfo": "^11.2.1", "@react-navigation/native": "^6.1.9", "@react-navigation/native-stack": "^6.9.17", - "@tanstack/react-query": "^5.17.19", + "@tanstack/query-sync-storage-persister": "^5.36.1", + "@tanstack/react-query": "^5.36.2", + "@tanstack/react-query-persist-client": "^5.36.2", "@thirdweb-dev/react-native": "^0.5.4", "@thirdweb-dev/react-native-compat": "^0.5.4", "@xmtp/frames-client": "^0.5.1", diff --git a/src/components/ConversationMessageContent.tsx b/src/components/ConversationMessageContent.tsx deleted file mode 100644 index 25bc444..0000000 --- a/src/components/ConversationMessageContent.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { - DecodedMessage, - GroupChangeContent, - RemoteAttachmentContent, -} from '@xmtp/react-native-sdk'; -import {Button, Container} from 'native-base'; -import React, { - FC, - PropsWithChildren, - useCallback, - useContext, - useState, -} from 'react'; -import {Pressable, useWindowDimensions} from 'react-native'; -import FastImage from 'react-native-fast-image'; -import {ContentTypes, SupportedContentTypes} from '../consts/ContentTypes'; -import {ConversationContext} from '../context/ConversationContext'; -import {useFrame} from '../hooks/useFrame'; -import {translate} from '../i18n'; -import {colors} from '../theme/colors'; -import {formatAddress} from '../utils/formatAddress'; -import {ImageMessage} from './ImageMessage'; -import {Button as AppButton} from './common/Button'; -import {Text} from './common/Text'; - -interface ConversationMessageContentProps { - message: DecodedMessage; - isMe: boolean; -} - -const OptionsContainer: FC< - PropsWithChildren<{isMe: boolean; messageId: string}> -> = ({children, isMe, messageId}) => { - const [shown, setShown] = useState(false); - const {setReaction, setReply} = useContext(ConversationContext); - - const handleReactPress = useCallback(() => { - setReaction(messageId); - }, [setReaction, messageId]); - - const handleReplyPress = useCallback(() => { - setReply(messageId); - }, [setReply, messageId]); - - return ( - setShown(prev => !prev)}> - - {children} - {shown && ( - - - Reply - - - React - - - )} - - - ); -}; - -const TextMessage = ({ - isMe, - message, -}: { - isMe: boolean; - message: DecodedMessage; -}) => { - const {width} = useWindowDimensions(); - const {frameData, postFrame} = useFrame(message.content() as string); - if (!frameData) { - return ( - - - {message.content() as string} - - - ); - } - - return ( - - - - {frameData.buttons.map((it, id) => ( - postFrame(id + 1)}> - {it} - - ))} - - - ); -}; - -export const ConversationMessageContent: FC< - ConversationMessageContentProps -> = ({message, isMe}) => { - if (message.contentTypeId === ContentTypes.Text) { - return ( - - - - ); - } - - if (message.contentTypeId === ContentTypes.RemoteStaticAttachment) { - return ( - - - - - - ); - } - - if (message.contentTypeId === ContentTypes.GroupMembershipChange) { - const content = message.content() as GroupChangeContent; - let text = ''; - if (content?.membersAdded.length > 0) { - if (content?.membersAdded.length > 1) { - text = translate('group_add_plural', { - initiatedByAddress: formatAddress( - content?.membersAdded[0].initiatedByAddress ?? '', - ), - addressCount: String(content?.membersAdded.length), - }); - } else { - text = translate('group_add_single', { - initiatedByAddress: formatAddress( - content?.membersAdded[0].initiatedByAddress, - ), - address: formatAddress(content?.membersAdded[0].address), - }); - } - } else if (content?.membersRemoved.length > 0) { - if (content?.membersRemoved.length > 1) { - text = translate('group_remove_plural', { - initiatedByAddress: formatAddress( - content?.membersRemoved[0].initiatedByAddress, - ), - addressCount: String(content?.membersRemoved.length), - }); - } else { - text = translate('group_remove_single', { - initiatedByAddress: formatAddress( - content?.membersRemoved[0].initiatedByAddress, - ), - address: formatAddress(content?.membersRemoved[0].address), - }); - } - } - - return ( - - - {text} - - - ); - } - - // TODO: Add support for other content types - return null; -}; diff --git a/src/components/Message.tsx b/src/components/Message.tsx new file mode 100644 index 0000000..9d2c9dc --- /dev/null +++ b/src/components/Message.tsx @@ -0,0 +1,62 @@ +import {DecodedMessage} from '@xmtp/react-native-sdk'; +import {Box, VStack} from 'native-base'; +import React, {FC} from 'react'; +import {Pressable} from 'react-native'; +import {ContentTypes, SupportedContentTypes} from '../consts/ContentTypes'; +import {MessageIdReactionsMapping} from '../queries/useGroupMessagesQuery'; +import {mmkvStorage} from '../services/mmkvStorage'; +import {colors} from '../theme/colors'; +import {formatAddress} from '../utils/formatAddress'; +import {getMessageTimeDisplay} from '../utils/getMessageTimeDisplay'; +import {Text} from './common/Text'; +import {ConversationMessageContent} from './messageContent/ConversationMessageContent'; + +export interface MessageProps { + message: DecodedMessage; + isMe: boolean; + reactions: MessageIdReactionsMapping[string]; +} + +export const Message: FC = ({message, isMe, reactions}) => { + if (message.contentTypeId === ContentTypes.Reaction) { + return null; + } + return ( + + + + {!isMe && ( + + + {mmkvStorage.getEnsName(message.senderAddress) ?? + formatAddress(message.senderAddress)} + + + )} + + + {getMessageTimeDisplay(message.sent)} + + + + + ); +}; diff --git a/src/components/messageContent/ConversationMessageContent.tsx b/src/components/messageContent/ConversationMessageContent.tsx new file mode 100644 index 0000000..1b3f8f5 --- /dev/null +++ b/src/components/messageContent/ConversationMessageContent.tsx @@ -0,0 +1,134 @@ +import { + DecodedMessage, + GroupChangeContent, + RemoteAttachmentContent, +} from '@xmtp/react-native-sdk'; +import {Container} from 'native-base'; +import React, {FC, useMemo} from 'react'; +import {ContentTypes, SupportedContentTypes} from '../../consts/ContentTypes'; +import {translate} from '../../i18n'; +import {MessageIdReactionsMapping} from '../../queries/useGroupMessagesQuery'; +import {colors} from '../../theme/colors'; +import {formatAddress} from '../../utils/formatAddress'; +import {ImageMessage} from '../ImageMessage'; +import {Text} from '../common/Text'; +import {MessageOptionsContainer} from './MessageOptionsContainer'; +import {TextMessageContent} from './TextMessageContent'; + +interface ConversationMessageContentProps { + message: DecodedMessage; + isMe: boolean; + reactions: MessageIdReactionsMapping[string]; +} + +interface ReactionItem { + content: string; + count: number; + addedByUser: boolean; +} + +export type ReactionItems = ReactionItem[]; + +export const ConversationMessageContent: FC< + ConversationMessageContentProps +> = ({message, isMe, reactions}) => { + const reacts = useMemo(() => { + const arr: ReactionItems = []; + for (const content of reactions.keys()) { + const value = reactions.get(content); + if (!value) { + continue; + } + const addedByUser = value.addedByUser; + const count = value.count; + arr.push({content, count, addedByUser}); + } + return arr; + }, [reactions]); + + if (message.contentTypeId === ContentTypes.Text) { + return ( + + + + ); + } + + if (message.contentTypeId === ContentTypes.RemoteStaticAttachment) { + return ( + + + + + + ); + } + + if (message.contentTypeId === ContentTypes.GroupMembershipChange) { + const content = message.content() as GroupChangeContent; + let text = ''; + if (content?.membersAdded.length > 0) { + if (content?.membersAdded.length > 1) { + text = translate('group_add_plural', { + initiatedByAddress: formatAddress( + content?.membersAdded[0].initiatedByAddress ?? '', + ), + addressCount: String(content?.membersAdded.length), + }); + } else { + text = translate('group_add_single', { + initiatedByAddress: formatAddress( + content?.membersAdded[0].initiatedByAddress, + ), + address: formatAddress(content?.membersAdded[0].address), + }); + } + } else if (content?.membersRemoved.length > 0) { + if (content?.membersRemoved.length > 1) { + text = translate('group_remove_plural', { + initiatedByAddress: formatAddress( + content?.membersRemoved[0].initiatedByAddress, + ), + addressCount: String(content?.membersRemoved.length), + }); + } else { + text = translate('group_remove_single', { + initiatedByAddress: formatAddress( + content?.membersRemoved[0].initiatedByAddress, + ), + address: formatAddress(content?.membersRemoved[0].address), + }); + } + } + + return ( + + + {text} + + + ); + } + + // TODO: Add support for other content types + return null; +}; diff --git a/src/components/messageContent/MessageOptionsContainer.tsx b/src/components/messageContent/MessageOptionsContainer.tsx new file mode 100644 index 0000000..1a59d69 --- /dev/null +++ b/src/components/messageContent/MessageOptionsContainer.tsx @@ -0,0 +1,112 @@ +import {Button, Container, HStack, Pressable} from 'native-base'; +import React, { + FC, + PropsWithChildren, + useCallback, + useContext, + useState, +} from 'react'; +import {GroupContext} from '../../context/GroupContext'; +import {colors} from '../../theme/colors'; +import {Button as AppButton} from '../common/Button'; +import {Text} from '../common/Text'; +import {ReactionItems} from './ConversationMessageContent'; + +export const MessageOptionsContainer: FC< + PropsWithChildren<{ + isMe: boolean; + messageId: string; + reactions: ReactionItems; + }> +> = ({children, isMe, messageId, reactions}) => { + const [shown, setShown] = useState(false); + const {group, setReplyId} = useContext(GroupContext); + + const handleReactPress = useCallback( + (content: string) => { + group?.send({ + reaction: { + reference: messageId, + action: 'added', + schema: 'unicode', + content, + }, + }); + setShown(false); + }, + [group, messageId], + ); + + const handleReplyPress = useCallback(() => { + setReplyId(messageId); + }, [setReplyId, messageId]); + + const handleRemoveReplyPress = useCallback( + (content: string) => { + group?.send({ + reaction: { + reference: messageId, + action: 'removed', + schema: 'unicode', + content, + }, + }); + }, + [group, messageId], + ); + + return ( + setShown(prev => !prev)}> + + {children} + {reactions.length > 0 && ( + + {reactions.map(({content, count, addedByUser}) => ( + handleRemoveReplyPress(content) + : () => handleReactPress(content) + } + key={content} + style={{ + backgroundColor: addedByUser + ? colors.actionPrimary + : undefined, + borderRadius: 16, + height: 24, + padding: 4, + }}> + + {content} {count} + + + ))} + + )} + {shown && ( + + + Reply + + handleReactPress('👍')} variant={'ghost'}> + 👍 + + handleReactPress('👎')} variant={'ghost'}> + 👎 + + + )} + + + ); +}; diff --git a/src/components/messageContent/TextMessageContent.tsx b/src/components/messageContent/TextMessageContent.tsx new file mode 100644 index 0000000..84f2029 --- /dev/null +++ b/src/components/messageContent/TextMessageContent.tsx @@ -0,0 +1,70 @@ +import {DecodedMessage} from '@xmtp/react-native-sdk'; +import {Button, Container} from 'native-base'; +import React from 'react'; +import {useWindowDimensions} from 'react-native'; +import FastImage from 'react-native-fast-image'; +import {SupportedContentTypes} from '../../consts/ContentTypes'; +import {useFrame} from '../../hooks/useFrame'; +import {colors} from '../../theme/colors'; +import {Button as AppButton} from '../common/Button'; +import {Text} from '../common/Text'; + +export const TextMessageContent = ({ + isMe, + message, +}: { + isMe: boolean; + message: DecodedMessage; +}) => { + const {width} = useWindowDimensions(); + const {frameData, postFrame} = useFrame(message.content() as string); + if (!frameData) { + return ( + + + {message.content() as string} + + + ); + } + + return ( + + + + {frameData.buttons.map((it, id) => ( + postFrame(id + 1)}> + {it} + + ))} + + + ); +}; diff --git a/src/consts/ContentTypes.ts b/src/consts/ContentTypes.ts index fb453d9..7967779 100644 --- a/src/consts/ContentTypes.ts +++ b/src/consts/ContentTypes.ts @@ -1,14 +1,23 @@ -import {GroupChangeCodec, RemoteAttachmentCodec} from '@xmtp/react-native-sdk'; +import { + GroupChangeCodec, + ReactionCodec, + RemoteAttachmentCodec, + ReplyCodec, +} from '@xmtp/react-native-sdk'; export const ContentTypes = { Text: 'xmtp.org/text:1.0', RemoteStaticAttachment: 'xmtp.org/remoteStaticAttachment:1.0', GroupMembershipChange: 'xmtp.org/group_membership_change:1.0', + Reaction: 'xmtp.org/reaction:1.0', + Reply: 'xmtp.org/reply:1.0', }; export const supportedContentTypes = [ new RemoteAttachmentCodec(), new GroupChangeCodec(), + new ReactionCodec(), + new ReplyCodec(), ]; export type SupportedContentTypes = typeof supportedContentTypes; diff --git a/src/context/ClientContext.tsx b/src/context/ClientContext.tsx index 6e0e118..8be8bbb 100644 --- a/src/context/ClientContext.tsx +++ b/src/context/ClientContext.tsx @@ -1,18 +1,6 @@ -import {useAddress, useConnectionStatus} from '@thirdweb-dev/react-native'; import {Client} from '@xmtp/react-native-sdk'; -import React, { - FC, - PropsWithChildren, - createContext, - useEffect, - useState, -} from 'react'; -import {AppConfig} from '../consts/AppConfig'; -import { - SupportedContentTypes, - supportedContentTypes, -} from '../consts/ContentTypes'; -import {clearClientKeys, getClientKeys} from '../services/encryptedStorage'; +import React, {createContext} from 'react'; +import {SupportedContentTypes} from '../consts/ContentTypes'; interface ClientContextValue { client: Client | null; @@ -30,61 +18,6 @@ export const ClientContext = createContext({ loading: true, }); -export const ClientProvider: FC = ({children}) => { - const [client, setClient] = useState | null>( - null, - ); - const [loading, setLoading] = useState(true); - const address = useAddress(); - const status = useConnectionStatus(); - - useEffect(() => { - if (status === 'unknown' || status === 'connecting') { - return; - } - if (status === 'disconnected') { - return setLoading(false); - } - if (!address) { - // Address still shows as undefined even when connected - return; - } - getClientKeys(address as `0x${string}`) - .then(keys => { - if (!keys) { - return setLoading(false); - } - Client.createFromKeyBundle(keys, { - codecs: supportedContentTypes, - enableAlphaMls: true, - env: AppConfig.XMTP_ENV, - }) - .then(newClient => { - setClient(newClient as Client); - setLoading(false); - }) - .catch(() => { - clearClientKeys(address as `0x${string}`); - setLoading(false); - }); - }) - .catch(() => { - return setLoading(false); - }); - }, [address, status]); - - return ( - - {children} - - ); -}; - export const useClientContext = () => { return React.useContext(ClientContext); }; diff --git a/src/context/ConversationContext.tsx b/src/context/ConversationContext.tsx deleted file mode 100644 index d4cd3ea..0000000 --- a/src/context/ConversationContext.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import {createContext} from 'react'; - -export interface ConversationContextValue { - setReply: (id: string) => void; - clearReply: () => void; - - setReaction: (id: string) => void; - clearReaction: () => void; -} - -export const ConversationContext = createContext({ - setReply: () => { - throw new Error('not implemented'); - }, - clearReply: () => { - throw new Error('setClient not implemented'); - }, - setReaction: () => { - throw new Error('setClient not implemented'); - }, - clearReaction: () => { - throw new Error('setClient not implemented'); - }, -}); diff --git a/src/context/GroupContext.tsx b/src/context/GroupContext.tsx new file mode 100644 index 0000000..4091f36 --- /dev/null +++ b/src/context/GroupContext.tsx @@ -0,0 +1,19 @@ +import {Group} from '@xmtp/react-native-sdk'; +import {createContext} from 'react'; +import {SupportedContentTypes} from '../consts/ContentTypes'; + +export interface GroupContextValue { + group: Group | null; + setReplyId: (id: string) => void; + clearReplyId: () => void; +} + +export const GroupContext = createContext({ + group: null, + setReplyId: () => { + throw new Error('Not Implemented'); + }, + clearReplyId: () => { + throw new Error('Not Implemented'); + }, +}); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c1817b2..99d0aed 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -68,6 +68,7 @@ "valid_address": "Valid Ethereum Address", "message_requests_from_new_addresses": "Message requests from addresses you’ve never interacted with show up here", + "not_on_xmtp_group": "User can't be added to group because they are not on XMTP Group network", "wallet_error": "Wallet Error", "group_add_single": "%{initiatedByAddress} added %{address}", diff --git a/src/providers/ClientProvider.tsx b/src/providers/ClientProvider.tsx new file mode 100644 index 0000000..6a90d0c --- /dev/null +++ b/src/providers/ClientProvider.tsx @@ -0,0 +1,65 @@ +import {useAddress, useConnectionStatus} from '@thirdweb-dev/react-native'; +import {Client} from '@xmtp/react-native-sdk'; +import React, {FC, PropsWithChildren, useEffect, useState} from 'react'; +import {AppConfig} from '../consts/AppConfig'; +import { + SupportedContentTypes, + supportedContentTypes, +} from '../consts/ContentTypes'; +import {ClientContext} from '../context/ClientContext'; +import {clearClientKeys, getClientKeys} from '../services/encryptedStorage'; + +export const ClientProvider: FC = ({children}) => { + const [client, setClient] = useState | null>( + null, + ); + const [loading, setLoading] = useState(true); + const address = useAddress(); + const status = useConnectionStatus(); + + useEffect(() => { + if (status === 'unknown' || status === 'connecting') { + return; + } + if (status === 'disconnected') { + return setLoading(false); + } + if (!address) { + // Address still shows as undefined even when connected + return; + } + getClientKeys(address as `0x${string}`) + .then(keys => { + if (!keys) { + return setLoading(false); + } + Client.createFromKeyBundle(keys, { + codecs: supportedContentTypes, + enableAlphaMls: true, + env: AppConfig.XMTP_ENV, + }) + .then(newClient => { + setClient(newClient as Client); + setLoading(false); + }) + .catch(() => { + clearClientKeys(address as `0x${string}`); + setLoading(false); + }); + }) + .catch(() => { + return setLoading(false); + }); + }, [address, status]); + + return ( + + {children} + + ); +}; diff --git a/src/providers/NativeBaseProvider.tsx b/src/providers/NativeBaseProvider.tsx new file mode 100644 index 0000000..393b7f6 --- /dev/null +++ b/src/providers/NativeBaseProvider.tsx @@ -0,0 +1,29 @@ +import {NativeBaseProvider as BaseProvider, extendTheme} from 'native-base'; +import React, {FC, PropsWithChildren} from 'react'; +import {colors} from '../theme/colors'; + +const newColorTheme = { + primary: { + 900: colors.actionPrimary, + 800: colors.actionPrimary, + 700: colors.actionPrimary, + 600: colors.actionPrimary, + 500: colors.actionPrimary, + 400: colors.actionPrimary, + 300: colors.actionPrimary, + }, + brand: { + 900: colors.actionPrimary, + 800: colors.actionPrimary, + 700: colors.actionPrimary, + 600: colors.actionPrimary, + 500: colors.actionPrimary, + 400: colors.actionPrimary, + 300: colors.actionPrimary, + }, +}; + +export const NativeBaseProvider: FC = ({children}) => { + const theme = extendTheme({colors: newColorTheme}); + return {children}; +}; diff --git a/src/providers/QueryClientProvider.tsx b/src/providers/QueryClientProvider.tsx new file mode 100644 index 0000000..2dce8fd --- /dev/null +++ b/src/providers/QueryClientProvider.tsx @@ -0,0 +1,30 @@ +import {createSyncStoragePersister} from '@tanstack/query-sync-storage-persister'; +import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'; +import React, {FC, PropsWithChildren} from 'react'; +import {mmkvstorage} from '../services/mmkvStorage'; +import {queryClient} from '../services/queryClient'; + +const mmkvStoragePersister = createSyncStoragePersister({ + storage: { + setItem: (key, value) => { + mmkvstorage.set(key, value); + }, + getItem: key => { + const value = mmkvstorage.getString(key); + return value === undefined ? null : value; + }, + removeItem: key => { + mmkvstorage.delete(key); + }, + }, +}); + +export const QueryClientProvider: FC = ({children}) => { + return ( + + {children} + + ); +}; diff --git a/src/providers/ThirdwebProvider.tsx b/src/providers/ThirdwebProvider.tsx new file mode 100644 index 0000000..e9d0d95 --- /dev/null +++ b/src/providers/ThirdwebProvider.tsx @@ -0,0 +1,34 @@ +import {Ethereum} from '@thirdweb-dev/chains'; +import { + localWallet, + metamaskWallet, + ThirdwebProvider as ThirdWeb, + walletConnect, +} from '@thirdweb-dev/react-native'; +import React, {FC, PropsWithChildren} from 'react'; +import Config from 'react-native-config'; + +export const ThirdwebProvider: FC = ({children}) => { + return ( + + {children} + + ); +}; diff --git a/src/providers/WagmiProvider.tsx b/src/providers/WagmiProvider.tsx new file mode 100644 index 0000000..baaea2c --- /dev/null +++ b/src/providers/WagmiProvider.tsx @@ -0,0 +1,19 @@ +import React, {FC, PropsWithChildren} from 'react'; +import {mainnet} from 'viem/chains'; +import {WagmiConfig, configureChains, createConfig} from 'wagmi'; +import {publicProvider} from 'wagmi/providers/public'; + +const {publicClient, webSocketPublicClient} = configureChains( + [mainnet], + [publicProvider()], +); + +const config = createConfig({ + autoConnect: true, + publicClient, + webSocketPublicClient, +}); + +export const WagmiProvider: FC = ({children}) => { + return {children}; +}; diff --git a/src/providers/index.tsx b/src/providers/index.tsx new file mode 100644 index 0000000..f261ac2 --- /dev/null +++ b/src/providers/index.tsx @@ -0,0 +1,20 @@ +import React, {FC, PropsWithChildren} from 'react'; +import {ClientProvider} from './ClientProvider'; +import {NativeBaseProvider} from './NativeBaseProvider'; +import {QueryClientProvider} from './QueryClientProvider'; +import {ThirdwebProvider} from './ThirdwebProvider'; +import {WagmiProvider} from './WagmiProvider'; + +export const Providers: FC = ({children}) => { + return ( + + + + + {children} + + + + + ); +}; diff --git a/src/queries/useGroupMessagesQuery.ts b/src/queries/useGroupMessagesQuery.ts index d896c39..c70a7b5 100644 --- a/src/queries/useGroupMessagesQuery.ts +++ b/src/queries/useGroupMessagesQuery.ts @@ -1,8 +1,8 @@ import {useQuery} from '@tanstack/react-query'; -import {DecodedMessage} from '@xmtp/react-native-sdk'; -import {SupportedContentTypes} from '../consts/ContentTypes'; +import {DecodedMessage, ReactionContent} from '@xmtp/react-native-sdk'; +import {ContentTypes, SupportedContentTypes} from '../consts/ContentTypes'; import {useGroup} from '../hooks/useGroup'; -import {EntityObject, createEntityObject} from '../utils/entities'; +import {EntityObject} from '../utils/entities'; import {getMessageId} from '../utils/idExtractors'; import {withRequestLogger} from '../utils/logger'; import {QueryKeys} from './QueryKeys'; @@ -10,9 +10,22 @@ import {QueryKeys} from './QueryKeys'; export type GroupMessagesQueryRequestData = DecodedMessage[]; export type GroupMessagesQueryError = unknown; -export type GroupMessagesQueryData = EntityObject< - DecodedMessage + +export type ReactionConent = { + count: number; + addressSet: Set; + addedAddressSet: Set; + addedByUser: boolean; +}; + +export type MessageIdReactionsMapping = Record< + string, + Map >; +export interface GroupMessagesQueryData + extends EntityObject> { + reactionsEntities: MessageIdReactionsMapping; +} export const useGroupMessagesQuery = (id: string) => { const {data: group} = useGroup(id); @@ -35,6 +48,54 @@ export const useGroupMessagesQuery = (id: string) => { >; }, enabled: !!group, - select: data => createEntityObject(data, getMessageId), + select: data => { + const ids: string[] = []; + const entities: Record< + string, + DecodedMessage + > = {}; + const userId = group!.client.address; + const reactionsEntities: MessageIdReactionsMapping = {}; + data.forEach(item => { + const messageId = getMessageId(item); + ids.push(messageId); + if (messageId in entities) { + console.error('Duplicate id'); + } + const content = item.content(); + if ( + item.contentTypeId === ContentTypes.Reaction && + typeof content === 'object' + ) { + const reaction = content as ReactionContent; + const messageMap = reactionsEntities[reaction.reference] ?? new Map(); + if (!messageMap.has(reaction.content)) { + messageMap.set(reaction.content, { + count: 0, + addressSet: new Set(), + addedAddressSet: new Set(), + addedByUser: false, + }); + } + + const reactionContent = messageMap.get(reaction.content)!; + if (!reactionContent.addressSet.has(item.senderAddress)) { + reactionContent.addressSet.add(item.senderAddress); + if (reaction.action === 'added') { + reactionContent.count++; + reactionContent.addedAddressSet.add(item.senderAddress); + if (item.senderAddress.toLowerCase() === userId.toLowerCase()) { + reactionContent.addedByUser = true; + } + } + } + messageMap.set(reaction.content, reactionContent); + reactionsEntities[reaction.reference] = messageMap; + } + + entities[messageId] = item; + }); + return {ids, entities, reactionsEntities}; + }, }); }; diff --git a/src/screens/ConversationScreen.tsx b/src/screens/ConversationScreen.tsx deleted file mode 100644 index 7ad38e0..0000000 --- a/src/screens/ConversationScreen.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import {useQueryClient} from '@tanstack/react-query'; -import {RemoteAttachmentContent} from '@xmtp/react-native-sdk'; -import {Box, FlatList, HStack, Pressable, VStack} from 'native-base'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import { - Alert, - KeyboardAvoidingView, - ListRenderItem, - Platform, -} from 'react-native'; -import {Asset} from 'react-native-image-picker'; -import {ConversationHeader} from '../components/ConversationHeader'; -import {ConversationInput} from '../components/ConversationInput'; -import {ConversationMessageContent} from '../components/ConversationMessageContent'; -import {Button} from '../components/common/Button'; -import {Drawer} from '../components/common/Drawer'; -import {Icon} from '../components/common/Icon'; -import {Modal} from '../components/common/Modal'; -import {Screen} from '../components/common/Screen'; -import {Text} from '../components/common/Text'; -import { - ConversationContext, - ConversationContextValue, -} from '../context/ConversationContext'; -import {useClient} from '../hooks/useClient'; -import {useContactInfo} from '../hooks/useContactInfo'; -import {useConversation} from '../hooks/useConversation'; -import {useConversationMessages} from '../hooks/useConversationMessages'; -import {translate} from '../i18n'; -import {QueryKeys} from '../queries/QueryKeys'; -import {mmkvStorage} from '../services/mmkvStorage'; -import {AWSHelper} from '../services/s3'; -import {colors} from '../theme/colors'; - -const keyExtractor = (item: string) => item; - -const getTimestamp = (timestamp: number) => { - // If today, return hours and minutes if not return date - const date = new Date(timestamp); - const now = new Date(); - if ( - date.getDate() === now.getDate() && - date.getMonth() === now.getMonth() && - date.getFullYear() === now.getFullYear() - ) { - return date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }); - } - return date.toLocaleDateString(); -}; - -const useData = (topic: string) => { - const {data: messages} = useConversationMessages(topic); - const {client} = useClient(); - const {data: conversation} = useConversation(topic); - const cachedPeerAddress = mmkvStorage.getTopicAddresses(topic)?.[0]; - const {displayName, avatarUrl} = useContactInfo( - conversation?.peerAddress || '', - ); - - useEffect(() => { - if (topic && conversation?.peerAddress) { - mmkvStorage.saveTopicAddresses(topic, [conversation?.peerAddress]); - } - }, [conversation?.peerAddress, topic]); - - return { - profileImage: avatarUrl, - name: displayName, - address: conversation?.peerAddress ?? cachedPeerAddress, - myAddress: client?.address, - messages, - conversation, - client, - }; -}; - -const getInitialConsentState = (address: string, peerAddress: string) => { - const cachedConsent = mmkvStorage.getConsent(address, peerAddress); - if (cachedConsent === undefined) { - return 'unknown'; - } - if (cachedConsent) { - return 'allowed'; - } - return 'denied'; -}; - -/** - * - * @deprecated This screen is not used anymore - * keeping it for reference in case we want UI of 1 to 1 chats to be different - */ -export const ConversationScreen = () => { - const {params} = useRoute(); - const {topic} = params as {topic: string}; - const {name, myAddress, messages, address, conversation, client} = - useData(topic); - const [showProfileModal, setShowProfileModal] = useState(false); - const [consent, setConsent] = useState<'allowed' | 'denied' | 'unknown'>( - getInitialConsentState(myAddress ?? '', address ?? ''), - ); - const {ids, entities} = messages ?? {}; - const queryClient = useQueryClient(); - const [replyId, setReplyId] = useState(null); - const [reactId, setReactId] = useState(null); - - useEffect(() => { - if (!conversation) { - return; - } - conversation.consentState().then(currentConsent => { - setConsent(currentConsent); - }); - }, [conversation]); - - useEffect(() => { - if (consent === 'unknown') { - return; - } - mmkvStorage.saveConsent( - client?.address ?? '', - address ?? '', - consent === 'allowed', - ); - }, [address, client?.address, consent]); - - const sendMessage = useCallback( - async (payload: {text?: string; asset?: Asset}) => { - if (!conversation) { - return; - } - if (payload.text) { - conversation - ?.send(payload.text) - .then(() => {}) - .catch(err => { - Alert.alert('Error', err.message); - }); - } else if (payload.asset) { - client - ?.encryptAttachment({ - fileUri: payload.asset.uri ?? '', - mimeType: payload.asset.type, - }) - .then(encrypted => { - AWSHelper.uploadFile( - encrypted.encryptedLocalFileUri, - encrypted.metadata.filename ?? '', - ).then(response => { - const remote: RemoteAttachmentContent = { - ...encrypted.metadata, - scheme: 'https://', - url: response, - }; - conversation?.send({remoteAttachment: remote}).catch(() => {}); - }); - }) - .catch(() => {}); - } - }, - [client, conversation], - ); - - const renderItem: ListRenderItem = ({item}) => { - const message = entities?.[item]; - if (!message) { - return null; - } - const isMe = message.senderAddress === myAddress; - - return ( - - - - - - {getTimestamp(message.sent)} - - - - - ); - }; - - const onConsent = useCallback(() => { - if (address) { - client?.contacts.allow([address]); - } - setConsent('allowed'); - mmkvStorage.saveConsent(myAddress ?? '', address ?? '', true); - queryClient.invalidateQueries({ - queryKey: [QueryKeys.List, client?.address], - }); - }, [address, client?.address, client?.contacts, myAddress, queryClient]); - - const onBlock = useCallback(() => { - if (address) { - client?.contacts.deny([address]); - } - setConsent('denied'); - mmkvStorage.saveConsent(myAddress ?? '', address ?? '', false); - }, [address, client?.contacts, myAddress]); - - const setReply = useCallback( - (id: string) => { - setReplyId(id); - }, - [setReplyId], - ); - - const setReaction = useCallback( - (id: string) => { - setReactId(id); - }, - [setReactId], - ); - - const clearReply = useCallback(() => { - setReplyId(null); - }, [setReplyId]); - - const clearReaction = useCallback(() => { - setReactId(null); - }, [setReactId]); - - const conversationProviderValue = useMemo((): ConversationContextValue => { - return { - setReply, - setReaction, - clearReply, - clearReaction, - }; - }, [setReply, setReaction, clearReply, clearReaction]); - - return ( - - - - setShowProfileModal(true)} - /> - - - } - /> - - {consent !== 'unknown' ? ( - - ) : ( - - - - - )} - - - - - - - - Test - - - - - - - Test - - - - setShowProfileModal(false)} - isOpen={showProfileModal}> - - - {name} - - {translate('domain_origin')} - - - - - - ); -}; diff --git a/src/screens/GroupScreen.tsx b/src/screens/GroupScreen.tsx index 972667a..2d28be4 100644 --- a/src/screens/GroupScreen.tsx +++ b/src/screens/GroupScreen.tsx @@ -1,19 +1,19 @@ import {useFocusEffect, useRoute} from '@react-navigation/native'; import {RemoteAttachmentContent} from '@xmtp/react-native-sdk'; -import {Box, FlatList, HStack, Pressable, VStack} from 'native-base'; -import React, {useCallback, useEffect, useState} from 'react'; +import {Box, FlatList, HStack, VStack} from 'native-base'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {KeyboardAvoidingView, ListRenderItem, Platform} from 'react-native'; import {Asset} from 'react-native-image-picker'; import {ConversationInput} from '../components/ConversationInput'; -import {ConversationMessageContent} from '../components/ConversationMessageContent'; import {GroupHeader} from '../components/GroupHeader'; +import {Message} from '../components/Message'; import {Button} from '../components/common/Button'; import {Drawer} from '../components/common/Drawer'; import {Screen} from '../components/common/Screen'; import {Text} from '../components/common/Text'; import {AddGroupParticipantModal} from '../components/modals/AddGroupParticipantModal'; import {GroupInfoModal} from '../components/modals/GroupInfoModal'; -import {ContentTypes} from '../consts/ContentTypes'; +import {GroupContext, GroupContextValue} from '../context/GroupContext'; import {useClient} from '../hooks/useClient'; import {useGroup} from '../hooks/useGroup'; import {useGroupMessages} from '../hooks/useGroupMessages'; @@ -22,27 +22,9 @@ import {useGroupParticipantsQuery} from '../queries/useGroupParticipantsQuery'; import {mmkvStorage} from '../services/mmkvStorage'; import {AWSHelper} from '../services/s3'; import {colors} from '../theme/colors'; -import {formatAddress} from '../utils/formatAddress'; const keyExtractor = (item: string) => item; -const getTimestamp = (timestamp: number) => { - // If today, return hours and minutes if not return date - const date = new Date(timestamp); - const now = new Date(); - if ( - date.getDate() === now.getDate() && - date.getMonth() === now.getMonth() && - date.getFullYear() === now.getFullYear() - ) { - return date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }); - } - return date.toLocaleDateString(); -}; - const useData = (id: string) => { const {data: messages, refetch, isRefetching} = useGroupMessages(id); const {data: addresses} = useGroupParticipantsQuery(id); @@ -81,13 +63,15 @@ export const GroupScreen = () => { const {id} = params as {id: string}; const {myAddress, messages, addresses, group, client, refetch, isRefetching} = useData(id); - const [showReply, setShowReply] = useState(false); const [showGroupModal, setShowGroupModal] = useState(false); const [showAddModal, setShowAddModal] = useState(false); const [consent, setConsent] = useState<'allowed' | 'denied' | 'unknown'>( getInitialConsentState(myAddress ?? '', group?.id ?? ''), ); - const {ids, entities} = messages ?? {}; + const [replyId, setReplyId] = useState(null); + const [reactId, setReactId] = useState(null); + + const {ids, entities, reactionsEntities} = messages ?? {}; useEffect(() => { if (!group) { @@ -144,43 +128,12 @@ export const GroupScreen = () => { if (!message) { return null; } + const reactions = reactionsEntities?.[item] ?? new Map(); const isMe = message.senderAddress?.toLocaleLowerCase() === myAddress?.toLocaleLowerCase(); - return ( - - - - {!isMe && ( - - - {mmkvStorage.getEnsName(message.senderAddress) ?? - formatAddress(message.senderAddress)} - - - )} - - - {getTimestamp(message.sent)} - - - - - ); + + return ; }; const onConsent = useCallback(() => { @@ -199,8 +152,31 @@ export const GroupScreen = () => { mmkvStorage.saveConsent(myAddress ?? '', id ?? '', false); }, [addresses, client?.contacts, id, myAddress]); + const setReply = useCallback( + (id: string) => { + setReplyId(id); + }, + [setReplyId], + ); + + const clearReply = useCallback(() => { + setReplyId(null); + }, [setReplyId]); + + const clearReaction = useCallback(() => { + setReactId(null); + }, [setReactId]); + + const conversationProviderValue = useMemo((): GroupContextValue => { + return { + group: group ?? null, + setReplyId: setReply, + clearReplyId: clearReply, + }; + }, [group, setReply, clearReply]); + return ( - <> + { setShowReply(false)}> + isOpen={!!replyId} + onBackgroundPress={() => setReplyId(null)}> + + + Test + + + + { hide={() => setShowAddModal(false)} group={group!} /> - + ); }; diff --git a/src/screens/OnboardingConnectWalletScreen.tsx b/src/screens/OnboardingConnectWalletScreen.tsx index 3f21fa7..1ffceb9 100644 --- a/src/screens/OnboardingConnectWalletScreen.tsx +++ b/src/screens/OnboardingConnectWalletScreen.tsx @@ -56,11 +56,14 @@ export const OnboardingConnectWalletScreen = () => { try { await connect(config); setShowModal(false); + // Maybe a hack, feel like address should work from the useAddress hook + // But seems like something fairly consistently goes wrong with the hook + navigate(ScreenNames.OnboardingEnableIdentity); } catch (error: any) { Alert.alert(translate('wallet_error'), error?.message); } }, - [connect], + [connect, navigate], ); return ( diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index d266604..8888dd9 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -136,6 +136,8 @@ const ListItem: FC<{ export const SearchScreen = () => { const {goBack, navigate} = useTypedNavigation(); + const {client} = useClient(); + const [errorString, setErrorString] = useState(null); const [searchText, setSearchText] = useState(''); const [participants, setParticipants] = useState([]); @@ -147,10 +149,16 @@ export const SearchScreen = () => { [setParticipants], ); - const onGroupStart = useCallback(() => { + const onGroupStart = useCallback(async () => { + setErrorString(null); + const canMessage = await client?.canGroupMessage(participants); + if (!canMessage) { + setErrorString(translate('not_on_xmtp_group')); + return; + } goBack(); navigate(ScreenNames.NewConversation, {addresses: participants}); - }, [participants, navigate, goBack]); + }, [participants, navigate, goBack, client]); const {data: ensAddress} = useEnsAddress({ name: searchText, chainId: 1, @@ -239,6 +247,7 @@ export const SearchScreen = () => { const removeParticipant = useCallback( (address: string) => { + setErrorString(null); setParticipants(prev => prev.filter(it => it !== address)); }, [setParticipants], @@ -303,6 +312,17 @@ export const SearchScreen = () => { )} + {errorString && ( + + + + {errorString} + + + )} {participants.map(participant => { diff --git a/src/services/mmkvStorage.ts b/src/services/mmkvStorage.ts index b5e7cb2..07fbe78 100644 --- a/src/services/mmkvStorage.ts +++ b/src/services/mmkvStorage.ts @@ -23,8 +23,10 @@ enum MMKVKeys { GROUP_NAME = 'GROUP_NAME', } +export const mmkvstorage = new MMKV(); + class MMKVStorage { - storage = new MMKV(); + storage = mmkvstorage; //#region Ens Name private getEnsNameKey = (address: string) => { diff --git a/src/services/queryClient.ts b/src/services/queryClient.ts index e69de29..d0cb910 100644 --- a/src/services/queryClient.ts +++ b/src/services/queryClient.ts @@ -0,0 +1,9 @@ +import {QueryClient} from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index b9e8686..eb36e0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8279,10 +8279,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.36.1.tgz#79f8c1a539d47c83104210be2388813a7af2e524" integrity sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA== -"@tanstack/query-core@5.18.1": - version "5.18.1" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.18.1.tgz#b653ee354b7f4712d53565ccc5c6d8fb83ec866c" - integrity sha512-fYhrG7bHgSNbnkIJF2R4VUXb4lF7EBiQjKkDc5wOlB7usdQOIN4LxxHpDxyE3qjqIst1WBGvDtL48T0sHJGKCw== +"@tanstack/query-core@5.36.1": + version "5.36.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.36.1.tgz#ae46f935c4752812a56c6815305061a3da82e7b8" + integrity sha512-BteWYEPUcucEu3NBcDAgKuI4U25R9aPrHSP6YSf2NvaD2pSlIQTdqOfLRsxH9WdRYg7k0Uom35Uacb6nvbIMJg== "@tanstack/query-persist-client-core@4.36.1": version "4.36.1" @@ -8291,6 +8291,13 @@ dependencies: "@tanstack/query-core" "4.36.1" +"@tanstack/query-persist-client-core@5.36.1": + version "5.36.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.36.1.tgz#d81cd93f27c21a0c7cd32a2b52bbb9581f0d2443" + integrity sha512-GeiMlh46lElKcVx7vp/xbjo22sYHhEnZ8o977EwncDzb1DpCnqd8WFLoP0+u6BNCFGD3xuE7YjGUQU9PPp7oSQ== + dependencies: + "@tanstack/query-core" "5.36.1" + "@tanstack/query-sync-storage-persister@^4.27.1": version "4.36.1" resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.36.1.tgz#bf5d800d54416bc88f150792a53e25ed8aa8769f" @@ -8298,6 +8305,14 @@ dependencies: "@tanstack/query-persist-client-core" "4.36.1" +"@tanstack/query-sync-storage-persister@^5.36.1": + version "5.36.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.36.1.tgz#11ddfeefaf4bc6ac21c9a8a369c5754fcd09cd10" + integrity sha512-BrAvrQCQSlnoRXKbSYhwrCZ43rkuv6ldUM1s0Cn64xEkyK+SnbqR2OZRo67bzMpR6lycUUZppybtvLe6JroSog== + dependencies: + "@tanstack/query-core" "5.36.1" + "@tanstack/query-persist-client-core" "5.36.1" + "@tanstack/react-query-persist-client@^4.28.0": version "4.36.1" resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-4.36.1.tgz#d96fa44cdc661534379623423da596a7b5dc13a7" @@ -8305,6 +8320,13 @@ dependencies: "@tanstack/query-persist-client-core" "4.36.1" +"@tanstack/react-query-persist-client@^5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.36.2.tgz#f9f413aa3f27113701fbe8fc2ad30510103be37f" + integrity sha512-MEr95XKFL2rAnR9V7fsu1g47sGNhoKGplM1s23+uQzZUJNOeUqEhjWzhGFu56Ro0bwdgpGdxqdeudG82ogFf6Q== + dependencies: + "@tanstack/query-persist-client-core" "5.36.1" + "@tanstack/react-query@^4.28.0", "@tanstack/react-query@^4.33.0": version "4.36.1" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.36.1.tgz#acb589fab4085060e2e78013164868c9c785e5d2" @@ -8313,12 +8335,12 @@ "@tanstack/query-core" "4.36.1" use-sync-external-store "^1.2.0" -"@tanstack/react-query@^5.17.19": - version "5.18.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.18.1.tgz#fd4e7b87260e82c5277355ad64f0e431a9302e02" - integrity sha512-PdI07BbsahZ+04PxSuDQsQvBWe008eWFk/YYWzt8fvzt2sALUM0TpAJa/DFpqa7+SSo7j1EQR6Jx6znXNHyaXw== +"@tanstack/react-query@^5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.36.2.tgz#1b7dc4c2fa0e48912335f0a157dd942cfa269326" + integrity sha512-bHNa+5dead+j6SA8WVlEOPxcGfteVFgdyFTCFcxBgjnPf0fFpHUc7aNZBCnvmPXqy/BeQa9zTuU9ectb7i8ZXA== dependencies: - "@tanstack/query-core" "5.18.1" + "@tanstack/query-core" "5.36.1" "@thirdweb-dev/auth@4.1.22": version "4.1.22" @@ -9485,9 +9507,9 @@ undici "^5.8.1" "@xmtp/proto@^3.25.0": - version "3.41.0" - resolved "https://registry.yarnpkg.com/@xmtp/proto/-/proto-3.41.0.tgz#e21d28278f4ead3db2d719732bdd89875356ad72" - integrity sha512-QzN82CRkAytfFHluNhXB9ODpLAf1Zx0YBqcdAjzqbFQHwWWa/B6qmcmseMH/lo0NEvQoe9dLovMOTGBwf8kjDw== + version "3.57.0" + resolved "https://registry.yarnpkg.com/@xmtp/proto/-/proto-3.57.0.tgz#42e491328b55d07ccaedf60ca2cef3133ea70664" + integrity sha512-zjU7IZ6wkJK96RVcwqPEPxBpBE84i66o2u5Fvme5SwRhR0eLF+8f6hhbXzQHDT1ahwmjvCSEIiMNTjyvoNlH7A== dependencies: long "^5.2.0" protobufjs "^7.0.0"