Skip to content

Commit

Permalink
Hook Setup
Browse files Browse the repository at this point in the history
Setup for hooks in module

Step 1 of many towards parity with Web on the JS side

Added xmtp context into module and out from example app
Added useClient hook to help manage client

Follow ups, adding codecs, processors, namespaces, validators
Adding client close method in disconnect callback
  • Loading branch information
Alex Risch authored and Alex Risch committed Dec 13, 2023
1 parent 061baa8 commit 0ac8158
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 77 deletions.
7 changes: 3 additions & 4 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
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'
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 (
<QueryClientProvider client={queryClient}>
<XmtpContextProvider>
<XmtpProvider>
<NavigationContainer>
<Navigator.Navigator>
<Navigator.Screen
Expand Down Expand Up @@ -71,7 +70,7 @@ export default function App() {
/>
</Navigator.Navigator>
</NavigationContainer>
</XmtpContextProvider>
</XmtpProvider>
</QueryClientProvider>
)
}
2 changes: 1 addition & 1 deletion example/src/ConversationCreateScreen.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
58 changes: 34 additions & 24 deletions example/src/ConversationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -57,7 +56,7 @@ export default function ConversationScreen({
}: NativeStackScreenProps<NavigationParamList, 'conversation'>) {
const { topic } = route.params
const messageListRef = useRef<FlatList>(null)
let {
const {
data: messages,
refetch: refreshMessages,
isFetching,
Expand All @@ -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)
Expand All @@ -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 (
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView
Expand Down Expand Up @@ -142,7 +152,7 @@ export default function ConversationScreen({
contentContainerStyle={{ paddingBottom: 100 }}
refreshing={isFetching || isRefetching}
onRefresh={refreshMessages}
data={messages}
data={filteredMessages}
inverted
keyboardDismissMode="none"
keyExtractor={(message) => message.id}
Expand All @@ -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)
}
/>
Expand Down Expand Up @@ -1046,7 +1056,7 @@ function MessageContents({
contentTypeId: string
content: any
}) {
const { client }: { client: Client<any> } = useXmtp()
const { client } = useClient()

if (contentTypeId === 'xmtp.org/text:1.0') {
const text: string = content
Expand Down Expand Up @@ -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 (
<View>
Expand Down
3 changes: 1 addition & 2 deletions example/src/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
40 changes: 21 additions & 19 deletions example/src/LaunchScreen.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -22,24 +22,26 @@ export default function LaunchScreen({
}: NativeStackScreenProps<NavigationParamList, 'launch'>) {
const { setClient } = useXmtp()
const savedKeys = useSavedKeys()
const configureWallet = (
label: string,
configuring: Promise<XMTP.Client>
) => {
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<XMTP.Client>) => {
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 (
<ScrollView>
<Text
Expand Down
26 changes: 0 additions & 26 deletions example/src/XmtpContext.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion example/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
EncryptedLocalAttachment,
ReactionContent,
RemoteAttachmentContent,
useXmtp,
} from 'xmtp-react-native-sdk'

import { useXmtp } from './XmtpContext'
import { downloadFile, uploadFile } from './storage'

/**
Expand Down
36 changes: 36 additions & 0 deletions src/context/XmtpContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react'

import { Client } from '../lib/Client'

export interface XmtpContextValue {
/**
* The XMTP client instance
*/
client: Client<any> | null
/**
* Set the XMTP client instance
*/
setClient: React.Dispatch<React.SetStateAction<Client<any> | null>>
}

export const XmtpContext = React.createContext<XmtpContextValue>({
client: null,
setClient: () => {},
})
interface Props {
children: React.ReactNode
client?: Client<any>
}
export const XmtpProvider: React.FC<Props> = ({
children,
client: initialClient,
}) => {
const [client, setClient] = React.useState<Client<any> | null>(
initialClient ?? null
)
const context = React.useMemo(
() => ({ client, setClient }),
[client, setClient]
)
return <XmtpContext.Provider value={context}>{children}</XmtpContext.Provider>
}
1 change: 1 addition & 0 deletions src/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './XmtpContext'
2 changes: 2 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useXmtp'
export * from './useClient'
80 changes: 80 additions & 0 deletions src/hooks/useClient.ts
Original file line number Diff line number Diff line change
@@ -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<Error | null>(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<any>

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,
}
}
Loading

0 comments on commit 0ac8158

Please sign in to comment.