Skip to content
This repository was archived by the owner on Jan 31, 2023. It is now read-only.
Draft
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
134 changes: 67 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,67 @@
# React Chat Example

![Test](https://github.com/xmtp/example-chat-react/actions/workflows/test.yml/badge.svg)
![Lint](https://github.com/xmtp/example-chat-react/actions/workflows/lint.yml/badge.svg)
![Build](https://github.com/xmtp/example-chat-react/actions/workflows/build.yml/badge.svg)

![x-red-sm](https://user-images.githubusercontent.com/510695/163488403-1fb37e86-c673-4b48-954e-8460ae4d4b05.png)

**Example chat application demonstrating the core concepts and capabilities of the XMTP client SDK**

This application is built with React, [Next.js](https://nextjs.org/), and the [`xmtp-js` client SDK](https://github.com/xmtp/xmtp-js).

Use the application to send and receive messages using the XMTP `dev` network environment, with some [important considerations](#considerations). You are also free to customize and deploy the application.

This application is maintained by [XMTP Labs](https://xmtp.com) and distributed under [MIT License](./LICENSE) for learning about and developing applications built with XMTP, a messaging protocol and decentralized communication network for blockchain wallets. The application has not undergone a formal security audit.

## Getting Started

### Configure Infura

Add your Infura ID to `.env.local` in the project's root.

```
NEXT_PUBLIC_INFURA_ID={YOUR_INFURA_ID}
```

If you do not have an Infura ID, you can follow [these instructions](https://blog.infura.io/getting-started-with-infura-28e41844cc89/) to get one.

_This example comes preconfigured with an Infura ID provided for demonstration purposes. If you plan to fork or host it, you must use your own Infura ID as detailed above._

### Install the package

```bash
npm install
```

### Run the development server

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the application.

## Functionality

### Wallet Connections

[`Web3Modal`](https://github.com/Web3Modal/web3modal) is used to inject a Metamask, Coinbase Wallet, or WalletConnect provider through [`ethers`](https://docs.ethers.io/v5/). Methods for connecting and disconnecting are included in `WalletProvider` alongside the provider, signer, wallet address, and ENS utilities.

To use the application's chat functionality, the connected wallet must provide two signatures:

1. A one-time signature that is used to generate the wallet's private XMTP identity
2. A signature that is used on application start-up to initialize the XMTP client with that identity

### Chat Conversations

The application uses the `xmtp-js` [Conversations](https://github.com/xmtp/xmtp-js#conversations) abstraction to list the available conversations for a connected wallet and to listen for or create new conversations. For each conversation, the application gets existing messages and listens for or creates new messages. Conversations and messages are kept in a lightweight store and made available through `XmtpProvider`.

### Considerations

Here are some important considerations when working with the example chat application:

- The application sends and receives messages using the XMTP `dev` network environment. To connect to the `production` network instead, set the following environment variable `NEXT_PUBLIC_XMTP_ENVIRONMENT=production`.
- XMTP may occasionally delete messages and keys from the `dev` network, and will provide advance notice in the XMTP Discord community ([request access](https://xmtp.typeform.com/to/yojTJarb?utm_source=docs_home)). The `production` network is configured to store messages indefinitely.
- You can't yet send a message to a wallet address that hasn't used XMTP. The client displays an error when it looks up an address that doesn't have an identity broadcast on the XMTP network.
- This limitation will soon be resolved by improvements to the `xmtp-js` library that will allow messages to be created and stored for future delivery, even if the recipient hasn't used XMTP yet.
# React Chat Example
![Test](https://github.com/xmtp/example-chat-react/actions/workflows/test.yml/badge.svg)
![Lint](https://github.com/xmtp/example-chat-react/actions/workflows/lint.yml/badge.svg)
![Build](https://github.com/xmtp/example-chat-react/actions/workflows/build.yml/badge.svg)
![x-red-sm](https://user-images.githubusercontent.com/510695/163488403-1fb37e86-c673-4b48-954e-8460ae4d4b05.png)
**Example chat application demonstrating the core concepts and capabilities of the XMTP client SDK**
This application is built with React, [Next.js](https://nextjs.org/), and the [`xmtp-js` client SDK](https://github.com/xmtp/xmtp-js).
Use the application to send and receive messages using the XMTP `dev` network environment, with some [important considerations](#considerations). You are also free to customize and deploy the application.
This application is maintained by [XMTP Labs](https://xmtp.com) and distributed under [MIT License](./LICENSE) for learning about and developing applications built with XMTP, a messaging protocol and decentralized communication network for blockchain wallets. The application has not undergone a formal security audit.
## Getting Started
### Configure Infura
Add your Infura ID to `.env.local` in the project's root.
```
NEXT_PUBLIC_INFURA_ID={YOUR_INFURA_ID}
```
If you do not have an Infura ID, you can follow [these instructions](https://blog.infura.io/getting-started-with-infura-28e41844cc89/) to get one.
_This example comes preconfigured with an Infura ID provided for demonstration purposes. If you plan to fork or host it, you must use your own Infura ID as detailed above._
### Install the package
```bash
npm install
```
### Run the development server
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the application.
## Functionality
### Wallet Connections
[`WAGMI`](https://wagmi.sh/) is used to manage Wallet Connections.
To use the application's chat functionality, the connected wallet must provide two signatures:
1. A one-time signature that is used to generate the wallet's private XMTP identity
2. A signature that is used on application start-up to initialize the XMTP client with that identity
### Chat Conversations
The application uses the `xmtp-js` [Conversations](https://github.com/xmtp/xmtp-js#conversations) abstraction to list the available conversations for a connected wallet and to listen for or create new conversations. For each conversation, the application gets existing messages and listens for or creates new messages. Conversations and messages are kept in a lightweight store and made available through `XmtpProvider`.
### Considerations
Here are some important considerations when working with the example chat application:
- The application sends and receives messages using the XMTP `dev` network environment. To connect to the `production` network instead, set the following environment variable `NEXT_PUBLIC_XMTP_ENVIRONMENT=production`.
- XMTP may occasionally delete messages and keys from the `dev` network, and will provide advance notice in the XMTP Discord community ([request access](https://xmtp.typeform.com/to/yojTJarb?utm_source=docs_home)). The `production` network is configured to store messages indefinitely.
- You can't yet send a message to a wallet address that hasn't used XMTP. The client displays an error when it looks up an address that doesn't have an identity broadcast on the XMTP network.
- This limitation will soon be resolved by improvements to the `xmtp-js` library that will allow messages to be created and stored for future delivery, even if the recipient hasn't used XMTP yet.
3 changes: 2 additions & 1 deletion components/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef, useCallback } from 'react'
import { checkIfPathIsEns, classNames } from '../helpers'
import useWalletProvider from '../hooks/useWalletProvider'
import { useAppStore } from '../store/app'
import { useAccount } from 'wagmi'

type AddressInputProps = {
recipientWalletAddress?: string
Expand All @@ -21,7 +22,7 @@ const AddressInput = ({
onInputChange,
}: AddressInputProps): JSX.Element => {
const { lookupAddress } = useWalletProvider()
const walletAddress = useAppStore((state) => state.address)
const { address: walletAddress } = useAccount()
const inputElement = useRef(null)
const [value, setValue] = useState<string>(recipientWalletAddress || '')
const conversations = useAppStore((state) => state.conversations)
Expand Down
4 changes: 2 additions & 2 deletions components/AddressPill.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react'
import { classNames } from '../helpers'
import { useAppStore } from '../store/app'
import Address from './Address'
import { useAccount } from 'wagmi'

type addressPillProps = {
address: string
}

const AddressPill = ({ address }: addressPillProps): JSX.Element => {
const walletAddress = useAppStore((state) => state.address)
const { address: walletAddress } = useAccount()
const userIsSender = address === walletAddress
return (
<Address
Expand Down
3 changes: 2 additions & 1 deletion components/ConversationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Conversation } from '@xmtp/xmtp-js'
import { classNames, formatDate, getConversationKey } from '../helpers'
import Avatar from './Avatar'
import { useAppStore } from '../store/app'
import { useAccount } from 'wagmi'

type ConversationTileProps = {
conversation: Conversation
Expand All @@ -15,7 +16,7 @@ const ConversationTile = ({
conversation,
}: ConversationTileProps): JSX.Element | null => {
const router = useRouter()
const address = useAppStore((state) => state.address)
const { address } = useAccount()
const previewMessages = useAppStore((state) => state.previewMessages)
const loadingConversations = useAppStore(
(state) => state.loadingConversations
Expand Down
33 changes: 22 additions & 11 deletions components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,41 @@ import NewMessageButton from './NewMessageButton'
import NavigationPanel from './NavigationPanel'
import XmtpInfoPanel from './XmtpInfoPanel'
import UserMenu from './UserMenu'
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'
import { useAppStore } from '../store/app'
import useInitXmtpClient from '../hooks/useInitXmtpClient'
import useListConversations from '../hooks/useListConversations'
import useWalletProvider from '../hooks/useWalletProvider'
import { useSigner, useDisconnect, useAccount } from 'wagmi'
import { useRouter } from 'next/router'

const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const client = useAppStore((state) => state.client)
const { initClient } = useInitXmtpClient()
useListConversations()
const walletAddress = useAppStore((state) => state.address)
const signer = useAppStore((state) => state.signer)

const { connect: connectWallet, disconnect: disconnectWallet } =
useWalletProvider()
const { address: walletAddress } = useAccount()
const { disconnect } = useDisconnect()
const { data: signer } = useSigner()
const router = useRouter()

const handleDisconnect = useCallback(async () => {
await disconnectWallet()
}, [disconnectWallet])
disconnect()
Object.keys(localStorage).forEach((key) => {
if (key.startsWith('xmtp')) {
localStorage.removeItem(key)
}
})
router.push('/')
}, [disconnect])

const handleConnect = useCallback(async () => {
await connectWallet()
signer && (await initClient(signer))
}, [connectWallet, initClient, signer])
}, [initClient, signer])

useEffect(() => {
if (!client && signer) {
handleConnect()
}
}, [signer, client])

return (
<>
Expand Down
84 changes: 84 additions & 0 deletions components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Dialog, Transition } from '@headlessui/react'
import { XIcon } from '@heroicons/react/outline'
import clsx from 'clsx'
import type { FC, ReactNode } from 'react'
import { Fragment } from 'react'

interface Props {
icon?: ReactNode
title: ReactNode
size?: 'sm' | 'md' | 'lg'
show: boolean
children: ReactNode[] | ReactNode
onClose: () => void
}

export const Modal: FC<Props> = ({
icon,
title,
size = 'sm',
show,
children,
onClose,
}) => {
return (
<Transition.Root show={show} as={Fragment}>
<Dialog
as="div"
className="overflow-y-auto fixed inset-0 z-10"
onClose={onClose}
>
<div className="flex justify-center items-center p-4 min-h-screen text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-100"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<span
className="hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
/>
<Transition.Child
as={Fragment}
enter="ease-out duration-100"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-100"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div
className={clsx(
{ 'sm:max-w-5xl': size === 'lg' },
{ 'sm:max-w-3xl': size === 'md' },
{ 'sm:max-w-lg': size === 'sm' },
'inline-block align-bottom bg-white dark:bg-gray-800 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle w-full rounded-xl'
)}
>
<div className="bg-white flex justify-between items-center py-3.5 px-5 divider">
<div className="flex items-center space-x-2 font-bold">
{icon}
<div>{title}</div>
</div>
<button
type="button"
className="p-1 text-black rounded-full hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
onClick={onClose}
>
<XIcon className="w-5 h-5" />
</button>
</div>
{children}
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
)
}
23 changes: 20 additions & 3 deletions components/NavigationPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { LinkIcon } from '@heroicons/react/outline'
import { ArrowSmRightIcon } from '@heroicons/react/solid'
import { useState } from 'react'
import { useAppStore } from '../store/app'
import ConversationsList from './ConversationsList'
import Loader from './Loader'
import WalletConnector from './WalletConnector'
import { useAccount } from 'wagmi'

type NavigationPanelProps = {
onConnect: () => Promise<void>
Expand All @@ -13,12 +16,12 @@ type ConnectButtonProps = {
}

const NavigationPanel = ({ onConnect }: NavigationPanelProps): JSX.Element => {
const walletAddress = useAppStore((state) => state.address)
const { address: walletAddress } = useAccount()
const client = useAppStore((state) => state.client)

return (
<div className="flex-grow flex flex-col h-[calc(100vh-8rem)] overflow-y-auto">
{walletAddress && client !== null ? (
{walletAddress && client ? (
<ConversationsPanel />
) : (
<NoWalletConnectedMessage>
Expand Down Expand Up @@ -52,15 +55,29 @@ const NoWalletConnectedMessage: React.FC<{ children?: React.ReactNode }> = ({
}

const ConnectButton = ({ onConnect }: ConnectButtonProps): JSX.Element => {
const { address } = useAccount()
const [showLoginModal, setShowLoginModal] = useState(false)

const onClick = () => {
onConnect()
if (!address) {
setShowLoginModal(!showLoginModal)
}
}

return (
<button
onClick={onConnect}
onClick={onClick}
className="rounded border border-l-300 mx-auto my-4 text-l-300 hover:text-white hover:bg-l-400 hover:border-l-400 hover:fill-white focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-n-100 focus-visible:outline-none active:bg-l-500 active:border-l-500 active:text-l-100 active:ring-0"
>
<div className="flex items-center justify-center text-xs font-semibold px-4 py-1">
Connect your wallet
<ArrowSmRightIcon className="h-4" />
</div>
<WalletConnector
showLoginModal={showLoginModal}
setShowLoginModal={setShowLoginModal}
/>
</button>
)
}
Expand Down
4 changes: 2 additions & 2 deletions components/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Address from './Address'
import useEns from '../hooks/useEns'
import { Tooltip } from './Tooltip/Tooltip'
import packageJson from '../package.json'
import { useAppStore } from '../store/app'
import { useAccount } from 'wagmi'

type UserMenuProps = {
onConnect?: () => Promise<void>
Expand Down Expand Up @@ -72,7 +72,7 @@ const NotConnected = ({ onConnect }: UserMenuProps): JSX.Element => {
}

const UserMenu = ({ onConnect, onDisconnect }: UserMenuProps): JSX.Element => {
const walletAddress = useAppStore((state) => state.address)
const { address: walletAddress } = useAccount()

const onClickCopy = () => {
if (walletAddress) {
Expand Down
Loading