Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hook Setup #178

Merged
merged 1 commit into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading