diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx index f497e3ed..b8c52874 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx @@ -125,8 +125,7 @@ export const Chat: React.FC = ({ conversation, onBack }) => { await refreshConsentList(); await allow([conversation.peerAddress]); void refreshConsentList(); - // TODO: check if this is needed - // onRequestAllowed(); + setIsRequest(false) setIsRequestChangeLoading(false); }; @@ -162,6 +161,9 @@ export const Chat: React.FC = ({ conversation, onBack }) => { const computeHeight = useMemo(() => { const additionalHeight = []; const height = '100vh - 50px - 3rem - 1.5rem - 73px - 15px'; + if (isRequest) { + additionalHeight.push('-40px'); + } if (replyMessage) { if (isStringContent) { additionalHeight.push('46px'); @@ -169,6 +171,8 @@ export const Chat: React.FC = ({ conversation, onBack }) => { additionalHeight.push('61px'); } else if (type === 'video' || type === 'image') { additionalHeight.push('116px'); + } else { + additionalHeight.push('47px'); } } @@ -176,10 +180,9 @@ export const Chat: React.FC = ({ conversation, onBack }) => { additionalHeight.push('59px'); } - return `calc( ${height} ${ - additionalHeight.length > 0 ? ' - ' + additionalHeight.join(' - ') : '' - } )`; - }, [replyMessage, isMessagesSenderOnly, isStringContent, mimeType, type]); + return `calc( ${height} ${additionalHeight.length > 0 ? ' - ' + additionalHeight.join(' - ') : '' + } )`; + }, [replyMessage, isMessagesSenderOnly, isStringContent, mimeType, type, isRequest]); return ( = ({ conversation, onBack }) => { src={ primaryName ? sanitizeEnsImage({ - name: primaryName, - chainId: 1, - image: records?.sanitizedRecords?.avatar, - }) + name: primaryName, + chainId: 1, + image: records?.sanitizedRecords?.avatar, + }) : undefined } style={{ @@ -281,18 +284,22 @@ export const Chat: React.FC = ({ conversation, onBack }) => { ? primaryName : formatAddress(conversation.peerAddress)}

- {/* {(!!primaryName) && ( -

{formatAddress(conversation.peerAddress)}

- )} */} + {primaryName && ( +

+ {formatAddress(conversation.peerAddress)} +

+ )}
- + = ({ conversation, onBack }) => { }} /> - = ({ conversation, onBack }) => { }} onClick={() => blockAddress(conversation.peerAddress)} > -

- Block -

+

+ Block +

@@ -549,7 +556,7 @@ export const Chat: React.FC = ({ conversation, onBack }) => { {isMessagesSenderOnly && ( = ({ conversation, onBack }) => { fontSize: '14px', fontWeight: 900, lineHeight: '100%', - color: 'black', + color: 'var(--justweb3-foreground-color-3)', }} > Message in user’s Requests diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx index df728b2d..f0a41a67 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx @@ -1,27 +1,53 @@ +import { useMountedAccount } from '@justaname.id/react'; import { ArrowIcon, ClickableItem } from '@justweb3/ui'; -import { useClient } from '@xmtp/react-sdk'; -import { useWalletClient } from 'wagmi'; +import { Client, ClientOptions, useClient } from '@xmtp/react-sdk'; +import { useEthersSigner } from '../../hooks'; +import { XmtpEnvironment } from '../../plugins'; +import { storeKeys, loadKeys, wipeKeys } from '../../utils/xmtp'; export interface ChatButtonProps { handleOpen: (open: boolean) => void; + env: XmtpEnvironment; } -export const ChatButton: React.FC = ({ handleOpen }) => { +export const ChatButton: React.FC = ({ handleOpen, env }) => { const { initialize } = useClient(); const { client } = useClient(); - const { data: walletClient } = useWalletClient(); + const walletClient = useEthersSigner() + const { address } = useMountedAccount() const handleChat = async () => { if (!client) { - initialize({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - signer: walletClient, - options: { - env: 'dev', - }, - }).then(() => { - handleOpen(true); - }); + const signer = await walletClient + try { + if (!signer) { + return + } + const clientOptions: Partial> = { + appVersion: 'JustWeb3/1.0.0', + env: env + } + let keys = loadKeys(address ?? '', env) + if (!keys) { + keys = await Client.getKeys(signer, { + env: env, + skipContactPublishing: false, + // persistConversations: false, + }) + storeKeys(address ?? '', keys, env) + } + await initialize({ + keys, + options: clientOptions, + signer: signer, + }).then(() => { + handleOpen(true); + }) + + // handleClient(client) + } catch (error) { + console.error('Failed to initialize XMTP Client:', error) + wipeKeys(address ?? '', env) + } } else { handleOpen(true); } diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx index a9063ce3..54a37098 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx @@ -1,13 +1,14 @@ import { + AddIcon, + Flex, Sheet, SheetContent, SheetTitle, Tabs, TabsContent, TabsList, - TabsTrigger, + TabsTrigger } from '@justweb3/ui'; -import React, { useEffect, useMemo } from 'react'; import { CachedConversation, ContentTypeMetadata, @@ -16,6 +17,7 @@ import { useStreamAllMessages, useStreamConversations, } from '@xmtp/react-sdk'; +import React, { useEffect, useMemo } from 'react'; import { ChatList } from '../ChatList'; export interface ChatSheetProps { @@ -24,17 +26,19 @@ export interface ChatSheetProps { handleOpenChat: ( conversation: CachedConversation | null ) => void; + handleNewChat: () => void; } export const ChatSheet: React.FC = ({ open, handleOpen, handleOpenChat, + handleNewChat }) => { const [tab, setTab] = React.useState('Chats'); const { conversations, isLoading } = useConversations(); const [isConsentListLoading, setIsConsentListLoading] = React.useState(true); - const { loadConsentList, entries } = useConsent(); + const { loadConsentList, entries, } = useConsent(); const allowedConversations = useMemo(() => { return conversations.filter( @@ -74,6 +78,21 @@ export const ChatSheet: React.FC = ({ Chats + + + = ({ Requests + {requestConversations.length > 0 && ( + {requestConversations.length} + )} = ({ {playing ? = ({ }} >{playing || currentTime > 0 ? formatTime(currentTime) : formatTime(audioDuration ?? 0)}

); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx index 21c2c69f..0630fb58 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx @@ -1,11 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { CachedConversation, DecodedMessage } from '@xmtp/react-sdk'; -import { MessageWithReaction } from '../../utils/filterReactionsMessages'; import { useMountedAccount, usePrimaryName } from '@justaname.id/react'; -import { useSendReactionMessage } from '../../hooks'; -import { typeLookup } from '../../utils/attachments'; -import { findEmojiByName } from '../../utils/emojis'; -import { formatMessageSentTime } from '../../utils/messageTimeFormat'; import { DocumentIcon, DownloadIcon, @@ -14,10 +7,17 @@ import { ReactionIcon, ReplyIcon, } from '@justweb3/ui'; -import { formatAddress } from '../../utils/formatAddress'; +import { CachedConversation, DecodedMessage } from '@xmtp/react-sdk'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useSendReactionMessage } from '../../hooks'; +import { typeLookup } from '../../utils/attachments'; import { calculateFileSize } from '../../utils/calculateFileSize'; -import VoiceMessageCard from '../VoiceMessageCard'; +import { findEmojiByName } from '../../utils/emojis'; +import { MessageWithReaction } from '../../utils/filterReactionsMessages'; +import { formatAddress } from '../../utils/formatAddress'; +import { formatMessageSentTime } from '../../utils/messageTimeFormat'; import { CustomPlayer } from '../CustomPlayer'; +import VoiceMessageCard from '../VoiceMessageCard'; interface MessageCardProps { message: MessageWithReaction; @@ -259,17 +259,17 @@ const MessageCard: React.FC = ({ senderAddress: message.senderAddress, content: typeLookup[attachmentExtention] === 'image' || - typeLookup[attachmentExtention] === 'video' + typeLookup[attachmentExtention] === 'video' ? { - data: message.content.data, - mimeType: message.content.mimeType, - filename: message.content.filename, - url: URL.createObjectURL( - new Blob([message.content.data], { - type: message.content.mimeType, - }) - ), - } + data: message.content.data, + mimeType: message.content.mimeType, + filename: message.content.filename, + url: URL.createObjectURL( + new Blob([message.content.data], { + type: message.content.mimeType, + }) + ), + } : message.content, contentType: message.contentType, })} @@ -317,15 +317,15 @@ const MessageCard: React.FC = ({ {repliedMessage?.senderAddress === address ? 'YOU' : primaryName ?? - formatAddress(repliedMessage?.senderAddress ?? '')} + formatAddress(repliedMessage?.senderAddress ?? '')}

{isReplyText || isReplyReply ? (

= ({ src={ primaryName ? sanitizeEnsImage({ - name: primaryName, - chainId: 1, - image: records?.sanitizedRecords?.avatar, - }) + name: primaryName, + chainId: 1, + image: records?.sanitizedRecords?.avatar, + }) : undefined } /> @@ -140,7 +140,7 @@ export const MessageItem: React.FC = ({ }} > {blocked ? ( - ) : ( diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx index 4909da6b..c596cc07 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx @@ -47,6 +47,7 @@ interface MessageTextFieldProps { onCancelReply?: () => void; onNewConvo?: (message: string) => void; peerAddress?: string; + style?: React.CSSProperties; } const MessageTextField: React.FC = ({ @@ -57,6 +58,7 @@ const MessageTextField: React.FC = ({ conversation, onNewConvo, peerAddress, + style }) => { const [messageValue, setMessageValue] = React.useState(''); const [attachment, setAttachment] = React.useState(); @@ -211,7 +213,7 @@ const MessageTextField: React.FC = ({ }, [replyAttachmentExtention]); return ( - + {!replyMessage} {!newConvo && ( @@ -263,16 +265,17 @@ const MessageTextField: React.FC = ({ height: isReplyText ? '30px' : isReplyVoice - ? '45px' - : isReplyVideoOrImage - ? '100px' - : '30px', + ? '45px' + : isReplyVideoOrImage + ? '100px' + : '30px', borderRadius: '5px', background: 'white', + paddingLeft: '14px', border: '1px solid grey', borderBottom: 0, - borderTopLeftRadius: '6px', - borderTopRightRadius: '6px', + borderTopLeftRadius: '25px', + borderTopRightRadius: '25px', borderBottomRightRadius: 0, borderBottomLeftRadius: 0, }} @@ -395,9 +398,12 @@ const MessageTextField: React.FC = ({ justify="space-between" style={{ padding: '10px 15px', - borderRadius: '6px', + borderRadius: '100px', background: 'white', + height: 22, + maxHeight: 22!, border: '1px solid grey', + paddingLeft: attachment?.mimeType === 'audio/wav' ? '5px' : '15px' }} > {attachment?.mimeType !== 'audio/wav' && ( @@ -586,9 +592,9 @@ const MessageTextField: React.FC = ({ maxHeight: 22!, paddingLeft: !replyMessage && !newConvo ? '10px' : '16px', paddingRight: '10px', - borderRadius: '6px', - borderTopLeftRadius: replyMessage ? 0 : '6px', - borderTopRightRadius: replyMessage ? 0 : '6px', + borderRadius: replyMessage ? '25px' : '100px', + borderTopLeftRadius: replyMessage ? 0 : '100px', + borderTopRightRadius: replyMessage ? 0 : '100px', borderTop: replyMessage ? '0px' : '', }} placeholder={`Send message...`} diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx index cc44d44b..5a5954ad 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx @@ -1,254 +1,300 @@ import { - CachedConversation, - toCachedConversation, - useCanMessage, - useClient, - useConsent, - useConversation, - useStartConversation, + CachedConversation, + toCachedConversation, + useCanMessage, + useClient, + useConsent, + useConversation, + useStartConversation, } from '@xmtp/react-sdk'; import React, { useEffect, useMemo } from 'react'; import MessageTextField from '../MessageTextField'; import { useDebounce } from '@justweb3/widget'; import { - useMountedAccount, - usePrimaryName, - useRecords, + useMountedAccount, + usePrimaryName, + useRecords, } from '@justaname.id/react'; -import { ArrowIcon, Flex, Input, LoadingSpinner } from '@justweb3/ui'; - -const CancelIcon: React.FC> = (props) => ( - -); +import { ArrowIcon, CloseIcon, Flex, Input, LoadingSpinner, P, VerificationsIcon } from '@justweb3/ui'; + interface NewConversationProps { - onChatStarted: (conversation: CachedConversation) => void; - onBack: () => void; - selectedAddress?: string; + onChatStarted: (conversation: CachedConversation) => void; + onBack: () => void; + selectedAddress?: string; } const NewConversation: React.FC = ({ - onChatStarted, - onBack, - selectedAddress, + onChatStarted, + onBack, + selectedAddress, }) => { - // States - const [newAddress, setNewAddress] = React.useState( - selectedAddress ?? '' - ); - const [canMessage, setCanMessage] = React.useState(false); - //Queries - const { client } = useClient(); - const { startConversation } = useStartConversation(); - const { getCachedByPeerAddress } = useConversation(); - const { refreshConsentList, allow } = useConsent(); - const { address } = useMountedAccount(); - const { canMessage: xmtpCanMessage, isLoading } = useCanMessage(); - const { debouncedValue: debouncedAddress } = useDebounce( - newAddress, - 500 - ); - - // TODO: change to regex - const isAddressName = useMemo(() => { - return ( - !debouncedAddress.startsWith('0x') && - !debouncedAddress.startsWith('0X') && - debouncedAddress.length > 0 + // States + const [newAddress, setNewAddress] = React.useState( + selectedAddress ?? '' + ); + const [canMessage, setCanMessage] = React.useState(false); + //Queries + const { client } = useClient(); + const { startConversation } = useStartConversation(); + const { getCachedByPeerAddress } = useConversation(); + const { refreshConsentList, allow } = useConsent(); + const { address } = useMountedAccount(); + const { canMessage: xmtpCanMessage, isLoading } = useCanMessage(); + const { debouncedValue: debouncedAddress } = useDebounce( + newAddress, + 500 ); - }, [debouncedAddress]); - - const { records, isRecordsLoading } = useRecords({ - ens: !isAddressName ? debouncedAddress : undefined, - }); - - const { primaryName: name, isPrimaryNameLoading } = usePrimaryName({ - address: isAddressName ? debouncedAddress : undefined, - }); - - // const { name, isRecordsLoading } = useAddressResolutionName(debouncedAddress, !isAddressName); - // - // const { address: resolvedAddress, isPrimaryNameLoading } = useIdentityResolution(debouncedAddress, isAddressName); - const resolvedAddress = useMemo(() => { - return records?.sanitizedRecords?.ethAddress?.value; - }, [records]); - - const handleCanMessage = async () => { - if (!client) return; - try { - if (isAddressName && !!resolvedAddress) { - const res = await xmtpCanMessage(resolvedAddress); - setCanMessage(res); - } else { - if (debouncedAddress.length == 42) { - const res = await xmtpCanMessage(debouncedAddress); - setCanMessage(res); + + const isAddressName = useMemo(() => { + const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/; + return ( + !ethAddressRegex.test(debouncedAddress) && + debouncedAddress.length > 0 + ); + }, [debouncedAddress]); + + + const { records, isRecordsLoading } = useRecords({ + ens: debouncedAddress, + enabled: isAddressName + }); + + const { primaryName: name, isPrimaryNameLoading } = usePrimaryName({ + address: debouncedAddress as `0x${string}`, + enabled: !isAddressName + }); + + + const resolvedAddress = useMemo(() => { + return records?.sanitizedRecords?.ethAddress?.value; + }, [records]); + + const handleCanMessage = async () => { + if (!client) return; + try { + if (isAddressName) { + if (resolvedAddress) { + const res = await xmtpCanMessage(resolvedAddress); + setCanMessage(res); + } else { + // Resolved address is not available yet; do nothing + return; + } + } else if (debouncedAddress.length === 42) { + const res = await xmtpCanMessage(debouncedAddress); + setCanMessage(res); + } else { + setCanMessage(false); + } + } catch (e) { + console.log('error', e); + setCanMessage(false); + } + }; + + const handleNewConversation = async (message: string) => { + if (!client) return; + const peerAddress = + isAddressName && !!resolvedAddress ? resolvedAddress : debouncedAddress; + try { + const conv = await startConversation(peerAddress, message ?? {}); + if (!conv.cachedConversation) { + if (!conv.conversation) { + return; + } else { + const cachedConvo = toCachedConversation( + conv.conversation, + address ?? '' + ); + await allow([conv.conversation.peerAddress]); + await refreshConsentList(); + onChatStarted(cachedConvo); + onBack(); + } + } else { + await allow([conv.cachedConversation.peerAddress]); + await refreshConsentList(); + onChatStarted(conv.cachedConversation); + onBack(); + } + } catch (error) { + const e = error as Error; + console.log('error creating chat', e); + } + }; + + const checkIfConversationExists = async (peerAddress: string) => { + const convoExists = await getCachedByPeerAddress(peerAddress); + if (convoExists) { + onChatStarted(convoExists); + } + }; + + useEffect(() => { + if (debouncedAddress.length === 0) { + setCanMessage(false); + return; } - } - } catch (e) { - console.log('error', e); - setCanMessage(false); - } - }; - - const handleNewConversation = async (message: string) => { - if (!client) return; - const peerAddress = - isAddressName && !!resolvedAddress ? resolvedAddress : debouncedAddress; - try { - const conv = await startConversation(peerAddress, message ?? {}); - if (!conv.cachedConversation) { - if (!conv.conversation) { - return; + if (isAddressName) { + if (!isRecordsLoading && resolvedAddress) { + handleCanMessage(); + } } else { - const cachedConvo = toCachedConversation( - conv.conversation, - address ?? '' - ); - await allow([conv.conversation.peerAddress]); - await refreshConsentList(); - onChatStarted(cachedConvo); + if (!isLoading && !isPrimaryNameLoading) { + handleCanMessage(); + } + } + }, [ + debouncedAddress, + isLoading, + isPrimaryNameLoading, + isRecordsLoading, + resolvedAddress, + isAddressName + ]); + + + useEffect(() => { + const checkConversation = async () => { + if (canMessage) { + await checkIfConversationExists( + isAddressName && resolvedAddress ? resolvedAddress : debouncedAddress + ); + } + }; + + checkConversation(); + }, [canMessage, resolvedAddress, debouncedAddress]); + + + const isSearchLoading = useMemo(() => { + return ( + (isAddressName && isPrimaryNameLoading && debouncedAddress.length > 4) || + isLoading + ); + }, [isLoading, debouncedAddress, resolvedAddress, isAddressName]); + + + useEffect(() => { + if (isRecordsLoading || isPrimaryNameLoading) { + return; + } + + if (name) { + setNewAddress(name); + return; } - } else { - await allow([conv.cachedConversation.peerAddress]); - await refreshConsentList(); - onChatStarted(conv.cachedConversation); - } - } catch (error) { - const e = error as Error; - console.log('error creating chat', e); - } - }; - - const checkIfConversationExists = async (peerAddress: string) => { - const convoExists = await getCachedByPeerAddress(peerAddress); - if (convoExists) { - onChatStarted(convoExists); - } - }; - - useEffect(() => { - if (!isLoading && !isPrimaryNameLoading && debouncedAddress.length > 0) { - handleCanMessage(); - } else if (canMessage && debouncedAddress.length === 0) { - setCanMessage(false); - } - }, [debouncedAddress, isLoading, isPrimaryNameLoading]); - - useEffect(() => { - if (canMessage) { - checkIfConversationExists( - isAddressName && !!resolvedAddress ? resolvedAddress : debouncedAddress - ); - } - }, [canMessage, resolvedAddress, debouncedAddress]); - - const isSearchLoading = useMemo(() => { + }, [name, isRecordsLoading, isPrimaryNameLoading]); + return ( - (isAddressName && isPrimaryNameLoading && debouncedAddress.length > 4) || - isLoading - ); - }, [isLoading, debouncedAddress, resolvedAddress, isAddressName]); - - useEffect(() => { - if (!isRecordsLoading && !isPrimaryNameLoading) { - return; - } - - if (name) { - setNewAddress(name); - return; - } - }, [name, isRecordsLoading, isPrimaryNameLoading]); - - return ( - - - - - - To -

- } - right={ - isSearchLoading ? ( - - ) : ( - { - setNewAddress(''); + > + + + + + +

+ To +

+ {isSearchLoading ? ( + + ) : ( + debouncedAddress.length > 0 ? + + : null + )} +
+ } + right={ + isSearchLoading ? ( + + ) : ( + + { + setNewAddress(''); + }} + > + + + + ) + } + placeholder={'ENS, Wallet Address...'} + onChange={(e) => setNewAddress(e.target.value)} + style={{ + flex: 1, + height: 22, + maxHeight: 22!, + gap: 5, + }} + /> +
+ - ) - } - placeholder={'Subname, Wallet...'} - onChange={(e) => setNewAddress(e.target.value)} - /> - - - - - - ); + > + + + + ); }; export default NewConversation; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/index.ts index 8f6b6a3d..0a628dc4 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/hooks/index.ts +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/index.ts @@ -5,4 +5,5 @@ export * from './useSendReplyMessage'; export * from './useAttachmentChange'; export * from './useDebounced'; export * from './useRecordingTimer'; +export * from './useEthersSigner'; export * from './useVoiceRecording'; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts new file mode 100644 index 00000000..6edf47fc --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts @@ -0,0 +1,23 @@ +import { BrowserProvider, JsonRpcSigner } from 'ethers'; +import { useMemo } from 'react'; +import type { Account, Chain, Client, Transport } from 'viem'; +import { useWalletClient } from 'wagmi'; + +export function clientToSigner(client: Client) { + const { account, chain, transport } = client + if (!account || !chain || !transport) return undefined + const network = { + chainId: chain?.id, + name: chain?.name, + ensAddress: chain?.contracts?.ensRegistry?.address, + } + const provider = new BrowserProvider(transport, network) + const signer = new JsonRpcSigner(provider, account.address) + return signer +} + +/** Hook to convert a viem Wallet Client to an ethers.js Signer. */ +export async function useEthersSigner({ chainId }: { chainId?: number } = {}) { + const { data: client } = useWalletClient({ chainId }) + return useMemo(() => (client ? clientToSigner(client) : undefined), [client, client?.chain, client?.account, client?.transport]) +} diff --git a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx index 5f0d27c9..fd6a6032 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx @@ -2,25 +2,30 @@ import { JustaPlugin } from '@justweb3/widget'; import { JustWeb3XMTPProvider } from '../providers/JustWeb3XMTPProvider'; import { ChatButton } from '../components/ChatButton'; -export const XMTPPlugin: JustaPlugin = { - name: 'XMTPPlugin', - components: { - Provider: (pluginApi, children) => { - return ( - pluginApi.setState('xmtpOpen', open)} - > - {children} - - ); - }, - SignInMenu: (pluginApi) => { - return ( - pluginApi.setState('xmtpOpen', open)} - /> - ); - }, - }, +export type XmtpEnvironment = 'local' | 'production' | 'dev'; + +export const XMTPPlugin = (env: XmtpEnvironment): JustaPlugin => { + return ({ + name: 'XMTPPlugin', + components: { + Provider: (pluginApi, children) => { + return ( + pluginApi.setState('xmtpOpen', open)} + > + {children} + + ); + }, + SignInMenu: (pluginApi) => { + return ( + pluginApi.setState('xmtpOpen', open)} + env={env} + /> + ); + }, + } + }) }; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx index c5e135b3..f7824e73 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx @@ -11,6 +11,7 @@ import { import { useJustWeb3 } from '@justweb3/widget'; import { ChatSheet } from '../../components/ChatSheet'; import { MessageSheet } from '../../components/MessageSheet'; +import { NewMessageSheet } from '../../components/NewMessageSheet'; const contentTypeConfigs = [ attachmentContentTypeConfig, @@ -36,18 +37,27 @@ export const JustWeb3XMTPProvider: React.FC = ({ handleOpen, }) => { const [isXmtpEnabled, setIsXmtpEnabled] = React.useState(false); + const [isNewChat, setIsNewChat] = React.useState(false); const [conversation, setConversation] = React.useState | null>(null); const handleXmtpEnabled = (enabled: boolean) => { setIsXmtpEnabled(enabled); }; + const handleOpenNewChat = (open: boolean) => { + setIsNewChat(open); + }; + const handleOpenChat = ( conversation: CachedConversation | null ) => { setConversation(conversation); }; + const handleNewChat = () => { + setIsNewChat(true); + } + return ( @@ -57,11 +67,17 @@ export const JustWeb3XMTPProvider: React.FC = ({ openChat={!!conversation} conversation={conversation} /> + {isXmtpEnabled && ( )} {children} diff --git a/packages/@justweb3/xmtp-plugin/src/lib/utils/xmtp/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/utils/xmtp/index.ts new file mode 100644 index 00000000..7907a5f2 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/utils/xmtp/index.ts @@ -0,0 +1,32 @@ +import { XmtpEnvironment } from '../../plugins'; + +export const buildLocalStorageKey = ( + walletAddress: string, + env: XmtpEnvironment +) => { + return `xmtp:${env}:keys:${walletAddress}`; +}; +const ENCODING = 'binary'; + +export const storeKeys = ( + walletAddress: string, + keys: Uint8Array, + env: XmtpEnvironment +) => { + localStorage.setItem( + buildLocalStorageKey(walletAddress, env), + Buffer.from(keys).toString(ENCODING) + ); +}; + +export const loadKeys = ( + walletAddress: string, + env: XmtpEnvironment +): Uint8Array | null => { + const val = localStorage.getItem(buildLocalStorageKey(walletAddress, env)); + return val ? Buffer.from(val, ENCODING) : null; +}; + +export const wipeKeys = (walletAddress: string, env: XmtpEnvironment) => { + localStorage.removeItem(buildLocalStorageKey(walletAddress, env)); +};