diff --git a/example/App.tsx b/example/App.tsx
index f475f8792..fa7d87dd0 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -1,7 +1,7 @@
import { NavigationContainer } from '@react-navigation/native'
-import React from 'react'
import { Button, Platform } from 'react-native'
import { QueryClient, QueryClientProvider } from 'react-query'
+import { XmtpProvider } from 'xmtp-react-native-sdk'
import ConversationCreateScreen from './src/ConversationCreateScreen'
import ConversationScreen from './src/ConversationScreen'
@@ -9,13 +9,12 @@ import HomeScreen from './src/HomeScreen'
import LaunchScreen from './src/LaunchScreen'
import { Navigator } from './src/Navigation'
import TestScreen from './src/TestScreen'
-import { XmtpContextProvider } from './src/XmtpContext'
const queryClient = new QueryClient()
export default function App() {
return (
-
+
-
+
)
}
diff --git a/example/src/ConversationCreateScreen.tsx b/example/src/ConversationCreateScreen.tsx
index f2a80d984..9f0676dc9 100644
--- a/example/src/ConversationCreateScreen.tsx
+++ b/example/src/ConversationCreateScreen.tsx
@@ -1,9 +1,9 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { useState } from 'react'
import { Button, ScrollView, Text, TextInput } from 'react-native'
+import { useXmtp } from 'xmtp-react-native-sdk'
import { NavigationParamList } from './Navigation'
-import { useXmtp } from './XmtpContext'
export default function ConversationCreateScreen({
route,
diff --git a/example/src/ConversationScreen.tsx b/example/src/ConversationScreen.tsx
index b6e92a649..5b12e2b4b 100644
--- a/example/src/ConversationScreen.tsx
+++ b/example/src/ConversationScreen.tsx
@@ -8,7 +8,7 @@ import * as ImagePicker from 'expo-image-picker'
import type { ImagePickerAsset } from 'expo-image-picker'
import { PermissionStatus } from 'expo-modules-core'
import moment from 'moment'
-import React, { useRef, useState } from 'react'
+import React, { useCallback, useMemo, useRef, useState } from 'react'
import {
Button,
FlatList,
@@ -30,11 +30,10 @@ import {
DecodedMessage,
StaticAttachmentContent,
ReplyContent,
- Client,
+ useClient,
} from 'xmtp-react-native-sdk'
import { NavigationParamList } from './Navigation'
-import { useXmtp } from './XmtpContext'
import {
useConversation,
useMessage,
@@ -57,7 +56,7 @@ export default function ConversationScreen({
}: NativeStackScreenProps) {
const { topic } = route.params
const messageListRef = useRef(null)
- let {
+ const {
data: messages,
refetch: refreshMessages,
isFetching,
@@ -74,10 +73,15 @@ export default function ConversationScreen({
fileUri: attachment?.image?.uri || attachment?.file?.uri,
mimeType: attachment?.file?.mimeType,
})
- messages = (messages || []).filter(
- (message) => !hiddenMessageTypes.includes(message.contentTypeId)
+
+ const filteredMessages = useMemo(
+ () =>
+ (messages ?? [])?.filter(
+ (message) => !hiddenMessageTypes.includes(message.contentTypeId)
+ ),
+ [messages]
)
- // console.log("messages", JSON.stringify(messages, null, 2));
+
const sendMessage = async (content: any) => {
setSending(true)
console.log('Sending message', content)
@@ -102,16 +106,22 @@ export default function ConversationScreen({
const sendRemoteAttachmentMessage = () =>
sendMessage({ remoteAttachment }).then(() => setAttachment(null))
const sendTextMessage = () => sendMessage({ text }).then(() => setText(''))
- const scrollToMessageId = (messageId: string) => {
- const index = (messages || []).findIndex((m) => m.id === messageId)
- if (index === -1) {
- return
- }
- return messageListRef.current?.scrollToIndex({
- index,
- animated: true,
- })
- }
+ const scrollToMessageId = useCallback(
+ (messageId: string) => {
+ const index = (filteredMessages || []).findIndex(
+ (m) => m.id === messageId
+ )
+ if (index === -1) {
+ return
+ }
+ return messageListRef.current?.scrollToIndex({
+ index,
+ animated: true,
+ })
+ },
+ [filteredMessages]
+ )
+
return (
message.id}
@@ -154,9 +164,9 @@ export default function ConversationScreen({
onReply={() => setReplyingTo(message.id)}
onMessageReferencePress={scrollToMessageId}
showSender={
- index === (messages || []).length - 1 ||
- (index + 1 < (messages || []).length &&
- messages![index + 1].senderAddress !==
+ index === (filteredMessages || []).length - 1 ||
+ (index + 1 < (filteredMessages || []).length &&
+ filteredMessages![index + 1].senderAddress !==
message.senderAddress)
}
/>
@@ -1046,7 +1056,7 @@ function MessageContents({
contentTypeId: string
content: any
}) {
- const { client }: { client: Client } = useXmtp()
+ const { client } = useClient()
if (contentTypeId === 'xmtp.org/text:1.0') {
const text: string = content
@@ -1080,8 +1090,8 @@ function MessageContents({
if (contentTypeId === 'xmtp.org/reply:1.0') {
const replyContent: ReplyContent = content
const replyContentType = replyContent.contentType
- const codec = client.codecRegistry[replyContentType]
- const actualReplyContent = codec.decode(replyContent.content)
+ const codec = client?.codecRegistry[replyContentType]
+ const actualReplyContent = codec?.decode(replyContent.content)
return (
diff --git a/example/src/HomeScreen.tsx b/example/src/HomeScreen.tsx
index fe19e122a..f137b2d55 100644
--- a/example/src/HomeScreen.tsx
+++ b/example/src/HomeScreen.tsx
@@ -9,9 +9,8 @@ import {
Text,
View,
} from 'react-native'
-import { Conversation, Client } from 'xmtp-react-native-sdk'
+import { Conversation, Client, useXmtp } from 'xmtp-react-native-sdk'
-import { useXmtp } from './XmtpContext'
import { useConversationList, useMessages } from './hooks'
/// Show the user's list of conversations.
diff --git a/example/src/LaunchScreen.tsx b/example/src/LaunchScreen.tsx
index 191666314..ed889fc29 100644
--- a/example/src/LaunchScreen.tsx
+++ b/example/src/LaunchScreen.tsx
@@ -1,10 +1,10 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack'
-import React from 'react'
+import React, { useCallback } from 'react'
import { Button, ScrollView, StyleSheet, Text, View } from 'react-native'
import * as XMTP from 'xmtp-react-native-sdk'
+import { useXmtp } from 'xmtp-react-native-sdk'
import { NavigationParamList } from './Navigation'
-import { useXmtp } from './XmtpContext'
import { useSavedKeys } from './hooks'
const appVersion = 'XMTP_RN_EX/0.0.1'
@@ -22,24 +22,26 @@ export default function LaunchScreen({
}: NativeStackScreenProps) {
const { setClient } = useXmtp()
const savedKeys = useSavedKeys()
- const configureWallet = (
- label: string,
- configuring: Promise
- ) => {
- console.log('Connecting XMTP client', label)
- configuring
- .then(async (client) => {
- console.log('Connected XMTP client', label, {
- address: client.address,
+ const configureWallet = useCallback(
+ (label: string, configuring: Promise) => {
+ console.log('Connecting XMTP client', label)
+ configuring
+ .then(async (client) => {
+ console.log('Connected XMTP client', label, {
+ address: client.address,
+ })
+ setClient(client)
+ navigation.navigate('home')
+ // Save the configured client keys for use in later sessions.
+ const keyBundle = await client.exportKeyBundle()
+ await savedKeys.save(keyBundle)
})
- setClient(client)
- navigation.navigate('home')
- // Save the configured client keys for use in later sessions.
- const keyBundle = await client.exportKeyBundle()
- await savedKeys.save(keyBundle)
- })
- .catch((err) => console.log('Unable to connect XMTP client', label, err))
- }
+ .catch((err) =>
+ console.log('Unable to connect XMTP client', label, err)
+ )
+ },
+ []
+ )
return (
void
-}>({
- client: null,
- setClient: () => {},
-})
-export const useXmtp = () => useContext(XmtpContext)
-type Props = {
- children: ReactNode
-}
-export const XmtpContextProvider: FC = ({ children }) => {
- const [client, setClient] = useState(null)
- const context = useMemo(() => ({ client, setClient }), [client, setClient])
- return {children}
-}
diff --git a/example/src/hooks.tsx b/example/src/hooks.tsx
index ea34a2777..dc64123b2 100644
--- a/example/src/hooks.tsx
+++ b/example/src/hooks.tsx
@@ -7,9 +7,9 @@ import {
EncryptedLocalAttachment,
ReactionContent,
RemoteAttachmentContent,
+ useXmtp,
} from 'xmtp-react-native-sdk'
-import { useXmtp } from './XmtpContext'
import { downloadFile, uploadFile } from './storage'
/**
diff --git a/src/context/XmtpContext.tsx b/src/context/XmtpContext.tsx
new file mode 100644
index 000000000..cd25ee27f
--- /dev/null
+++ b/src/context/XmtpContext.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react'
+
+import { Client } from '../lib/Client'
+
+export interface XmtpContextValue {
+ /**
+ * The XMTP client instance
+ */
+ client: Client | null
+ /**
+ * Set the XMTP client instance
+ */
+ setClient: React.Dispatch | null>>
+}
+
+export const XmtpContext = React.createContext({
+ client: null,
+ setClient: () => {},
+})
+interface Props {
+ children: React.ReactNode
+ client?: Client
+}
+export const XmtpProvider: React.FC = ({
+ children,
+ client: initialClient,
+}) => {
+ const [client, setClient] = React.useState | null>(
+ initialClient ?? null
+ )
+ const context = React.useMemo(
+ () => ({ client, setClient }),
+ [client, setClient]
+ )
+ return {children}
+}
diff --git a/src/context/index.ts b/src/context/index.ts
new file mode 100644
index 000000000..8c967f1f9
--- /dev/null
+++ b/src/context/index.ts
@@ -0,0 +1 @@
+export * from './XmtpContext'
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 000000000..47428c4e1
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './useXmtp'
+export * from './useClient'
diff --git a/src/hooks/useClient.ts b/src/hooks/useClient.ts
new file mode 100644
index 000000000..9f7042e86
--- /dev/null
+++ b/src/hooks/useClient.ts
@@ -0,0 +1,80 @@
+import { Signer } from 'ethers'
+import { useCallback, useRef, useState } from 'react'
+
+import { useXmtp } from './useXmtp'
+import { Client, ClientOptions } from '../lib/Client'
+
+interface InitializeClientOptions {
+ signer: Signer
+ options?: ClientOptions
+}
+
+export const useClient = (onError?: (e: Error) => void) => {
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+ // client is initializing
+ const initializingRef = useRef(false)
+
+ const { client, setClient } = useXmtp()
+ /**
+ * Initialize an XMTP client
+ */
+ const initialize = useCallback(
+ async ({ options, signer }: InitializeClientOptions) => {
+ // only initialize a client if one doesn't already exist
+ if (!client && signer) {
+ // if the client is already initializing, don't do anything
+ if (initializingRef.current) {
+ return undefined
+ }
+
+ // flag the client as initializing
+ initializingRef.current = true
+
+ // reset error state
+ setError(null)
+ // reset loading state
+ setIsLoading(true)
+
+ let xmtpClient: Client
+
+ try {
+ // create a new XMTP client with the provided keys, or a wallet
+ xmtpClient = await Client.create(signer ?? null, {
+ ...options,
+ })
+ setClient(xmtpClient)
+ } catch (e) {
+ setClient(null)
+ setError(e as Error)
+ onError?.(e as Error)
+ // re-throw error for upstream consumption
+ throw e
+ }
+
+ setIsLoading(false)
+
+ return xmtpClient
+ }
+ return client
+ },
+ [client, onError, setClient]
+ )
+
+ /**
+ * Disconnect the XMTP client
+ */
+ const disconnect = useCallback(async () => {
+ if (client) {
+ setClient(null)
+ }
+ }, [client, setClient])
+
+ return {
+ client,
+ error,
+ initialize,
+ disconnect,
+ isLoading,
+ }
+}
diff --git a/src/hooks/useXmtp.ts b/src/hooks/useXmtp.ts
new file mode 100644
index 000000000..6e4e5a04a
--- /dev/null
+++ b/src/hooks/useXmtp.ts
@@ -0,0 +1,5 @@
+import { useContext } from 'react'
+
+import { XmtpContext } from '../context/XmtpContext'
+
+export const useXmtp = () => useContext(XmtpContext)
diff --git a/src/index.ts b/src/index.ts
index d924bf230..93ab5d90f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,6 +21,8 @@ export { ReadReceiptCodec } from './lib/NativeCodecs/ReadReceiptCodec'
export { StaticAttachmentCodec } from './lib/NativeCodecs/StaticAttachmentCodec'
export { RemoteAttachmentCodec } from './lib/NativeCodecs/RemoteAttachmentCodec'
export { TextCodec } from './lib/NativeCodecs/TextCodec'
+export * from './hooks'
+export * from './context'
const EncodedContent = content.EncodedContent