From fb31ee794f466d664b500bae7c3f265a6b5ded2c Mon Sep 17 00:00:00 2001 From: HadiKhai Date: Thu, 5 Dec 2024 09:39:18 +0200 Subject: [PATCH] feat: chat from profile --- .../react/src/lib/helpers/validateEns.ts | 15 + .../hooks/primaryName/usePrimaryNameBatch.ts | 6 +- .../react/src/lib/hooks/records/useRecords.ts | 59 +- .../sdk/src/lib/api/axiosController.ts | 1 + .../src/lib/dialogs/ProfileDialog/index.tsx | 1 + .../lib/providers/JustWeb3Provider/index.tsx | 12 +- .../xmtp-plugin/.storybook/preview.tsx | 14 +- .../src/lib/components/Chat/index.tsx | 45 +- .../src/lib/components/ChatList/index.tsx | 20 +- .../src/lib/components/ChatSheet/index.tsx | 53 +- .../ChatWithProfileButton/index.tsx | 82 +++ .../src/lib/components/MessageItem/index.tsx | 22 +- .../lib/components/NewConversation/index.tsx | 538 +++++++++--------- .../lib/components/NewMessageSheet/index.tsx | 20 +- .../xmtp-plugin/src/lib/plugins/index.tsx | 10 +- .../providers/JustWeb3XMTPProvider/index.tsx | 30 +- .../xmtp-plugin/src/stories/xmtp.stories.tsx | 13 +- 17 files changed, 591 insertions(+), 350 deletions(-) create mode 100644 packages/@justaname.id/react/src/lib/helpers/validateEns.ts create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatWithProfileButton/index.tsx diff --git a/packages/@justaname.id/react/src/lib/helpers/validateEns.ts b/packages/@justaname.id/react/src/lib/helpers/validateEns.ts new file mode 100644 index 00000000..0f58d763 --- /dev/null +++ b/packages/@justaname.id/react/src/lib/helpers/validateEns.ts @@ -0,0 +1,15 @@ +import { normalize } from 'viem/ens'; + +export const validateEns = (name: string | undefined): string | undefined => { + if (typeof name !== 'string' || name.trim() === '') { + return; + } + + try { + const normalized = normalize(name); + + return normalized; + } catch (error) { + return; + } +}; diff --git a/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts b/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts index ddd06a45..48ff1dce 100644 --- a/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts +++ b/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts @@ -17,10 +17,10 @@ export const buildPrimaryNameBatchKey = (chainId: ChainId | undefined) => [ chainId, ]; -export type PrimaryNameRecord = Record; +export type PrimaryNameRecord = Record; export interface UsePrimaryNameBatchParams { - addresses?: Address[]; + addresses?: string[]; chainId?: ChainId; enabled?: boolean; } @@ -143,7 +143,7 @@ export const usePrimaryNameBatch = ( ), queryFn: () => getPrimaryNameBatch({ - addresses: params?.addresses, + addresses: params?.addresses as Address[], }), enabled: Boolean(params?.addresses) && Boolean(ensClient) && Boolean(_enabled), diff --git a/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts b/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts index 58557c3b..b9918bd8 100644 --- a/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts +++ b/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts @@ -18,10 +18,11 @@ import { useMemo } from 'react'; import { Records } from '../../types'; import { defaultOptions } from '../../query'; import { RecordsTaskQueue } from './records-task-queue'; -import { checkEnsValid } from '../../helpers/checkEnsValid'; import { useEnsPublicClient } from '../client/useEnsPublicClient'; import { useOffchainResolvers } from '../offchainResolver'; import { getRecords as getEnsRecords } from '@ensdomains/ensjs/public'; +import { checkEnsValid } from '../../helpers/checkEnsValid'; +import { validateEns } from '../../helpers/validateEns'; export const buildRecordsBySubnameKey = ( subname: string, @@ -72,6 +73,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { () => params?.chainId || chainId, [params?.chainId, chainId] ); + const _ens = useMemo(() => validateEns(params?.ens), [params?.ens]); const { offchainResolvers } = useOffchainResolvers(); const { ensClient } = useEnsPublicClient({ chainId: _chainId, @@ -86,12 +88,19 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { const getRecords = async ( _params: SubnameRecordsRoute['params'] ): Promise => { + const __ens = validateEns(_params.ens); + + if (!__ens) { + throw new Error('Invalid ENS name'); + } + const result = await justaname.subnames.getRecords({ - ens: _params.ens, + ens: __ens, providerUrl: _params.providerUrl, chainId: _params.chainId, }); + console.log(__ens, result); checkEnsValid(result); const sanitized = sanitizeRecords(result); @@ -109,8 +118,14 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { throw new Error('Public client not found'); } + const __ens = validateEns(_params.ens) || _ens; + + if (!__ens) { + throw new Error('Invalid ENS name'); + } + const result = await getEnsRecords(ensClient, { - name: _params.ens, + name: __ens, coins: Object.keys(coinTypeMap), texts: [ ...generalKeys, @@ -126,7 +141,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { ); const record = { - ens: _params.ens, + ens: __ens, isJAN: result.resolverAddress === offchainResolver?.resolverAddress, records: { ...result, @@ -137,6 +152,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { }, }; + console.log(__ens, record); checkEnsValid(record); const sanitized = sanitizeRecords(record); @@ -152,11 +168,15 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { forceUpdate = false ): Promise => { const __chainId = _params?.chainId || _chainId; + const __ens = validateEns(_params?.ens) || _ens; + if (!__ens) { + throw new Error('Invalid ENS name'); + } // const __standard = _params?.standard || params?.standard; const __standard = false; if (!forceUpdate) { const cachedRecords = queryClient.getQueryData( - buildRecordsBySubnameKey(_params?.ens, __chainId, __standard) + buildRecordsBySubnameKey(__ens, __chainId, __standard) ) as Records; if (cachedRecords) { return cachedRecords; @@ -176,13 +196,16 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { let records: Records; try { records = await getRecords({ - ens: _params.ens, + ens: __ens, chainId: __chainId, providerUrl: __providerUrl, }); } catch (error) { + if (error instanceof Error && error.message.includes('NotFound')) { + throw error; + } records = await getStandardRecords({ - ens: _params.ens, + ens: __ens, chainId: __chainId, providerUrl: __providerUrl, }); @@ -195,7 +218,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { // if (__standard) { // records = await getStandardRecords({ - // ens: _params.ens, + // ens: __ens, // chainId: __chainId, // providerUrl: __providerUrl, // }); @@ -208,7 +231,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { // } queryClient.setQueryData( - buildRecordsBySubnameKey(_params.ens, __chainId, __standard), + buildRecordsBySubnameKey(__ens, __chainId, __standard), records ); return records; @@ -216,21 +239,33 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { const query = useQuery({ ...defaultOptions, + retry: (failureCount, error) => { + console.log(error); + if (error instanceof Error) { + if ( + error.message.includes('NotFound') || + error.message.includes('ETH address not found') + ) { + return false; + } + } + return failureCount < 3; + }, queryKey: buildRecordsBySubnameKey( - params?.ens || '', + _ens || '', _chainId // params?.standard ), queryFn: () => getRecordsInternal( { - ens: params?.ens || '', + ens: _ens || '', chainId: _chainId, }, true ), enabled: - Boolean(params?.ens) && + Boolean(_ens) && Boolean(_chainId) && Boolean(_providerUrl) && Boolean(_enabled), diff --git a/packages/@justaname.id/sdk/src/lib/api/axiosController.ts b/packages/@justaname.id/sdk/src/lib/api/axiosController.ts index 4e84ddc5..9a2dad0b 100644 --- a/packages/@justaname.id/sdk/src/lib/api/axiosController.ts +++ b/packages/@justaname.id/sdk/src/lib/api/axiosController.ts @@ -42,6 +42,7 @@ export const controlledAxiosPromise = >( return res.data.result.data as T; }) .catch((err: AxiosError>) => { + console.error(err); if (err?.response) { if (err?.response?.data?.result) { if (err?.response?.data?.result?.error) { diff --git a/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx b/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx index beedc810..15146d32 100644 --- a/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx +++ b/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx @@ -350,6 +350,7 @@ export const ProfileDialog: FC = ({ contentStyle={{ width: '100%', height: '100%', + pointerEvents: 'auto', }} fullScreen={minimized} header={ diff --git a/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx b/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx index f670f2f8..90058cf0 100644 --- a/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx +++ b/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx @@ -47,6 +47,7 @@ export interface JustWeb3ContextProps { ) => Promise; handleJustWeb3Config: (config: JustWeb3ProviderConfig) => void; handleOpenEnsProfile: (ens: string, chainId?: ChainId) => void; + handleCloseEnsProfile: () => void; isSignInOpen: boolean; config: JustWeb3ProviderConfig; plugins: JustaPlugin[]; @@ -58,6 +59,7 @@ export const JustWeb3Context = createContext({ handleOpenSignInDialog: () => {}, handleUpdateRecords: async () => {}, handleOpenEnsProfile: () => {}, + handleCloseEnsProfile: () => {}, handleJustWeb3Config: () => {}, config: {}, plugins: [], @@ -129,6 +131,10 @@ export const JustWeb3Provider: FC = ({ setEnsOpen({ ens, chainId }); }; + const handleCloseEnsProfile = () => { + setEnsOpen(null); + }; + useEffect(() => { if (!updateRecord && updateRecordPromiseResolveRef.current) { updateRecordPromiseResolveRef.current(); @@ -212,6 +218,7 @@ export const JustWeb3Provider: FC = ({ handleUpdateRecords: handleUpdateRecords, handleJustWeb3Config, handleOpenEnsProfile, + handleCloseEnsProfile, }} > void; connectedEns: UseEnsAuthReturn['connectedEns']; openEnsProfile: (ens: string, chainId?: ChainId) => void; + closeEnsProfile: () => void; updateRecords: ( records: Omit & { ens?: string } ) => Promise; @@ -305,7 +313,8 @@ export const useJustWeb3 = (): useJustWeb3 => { } = useEnsAuth({ local: !context.config.enableAuth, }); - const { handleUpdateRecords, handleOpenEnsProfile } = context; + const { handleUpdateRecords, handleOpenEnsProfile, handleCloseEnsProfile } = + context; const handleUpdateRecordsInternal = async ( records: Omit & { ens?: string; @@ -362,6 +371,7 @@ export const useJustWeb3 = (): useJustWeb3 => { connectedEns, refreshEnsAuth, openEnsProfile: handleOpenEnsProfile, + closeEnsProfile: handleCloseEnsProfile, chainId: justanameContext?.chainId, }; }; diff --git a/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx b/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx index ef63dacb..d4619ace 100644 --- a/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx +++ b/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx @@ -1,12 +1,10 @@ import './polyfills'; -import { scan } from 'react-scan'; // import this BEFORE react - -if (typeof window !== 'undefined') { - scan({ - enabled: true, - log: true, // logs render info to console (default: false) - }); -} +// if (typeof window !== 'undefined') { +// scan({ +// enabled: true, +// log: true, // logs render info to console (default: false) +// }); +// } export const decorators = [(Story) => ]; 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 b8c52874..9ff2c0d1 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx @@ -38,6 +38,7 @@ import EmojiSelector from '../EmojiSelector'; import MessageCard from '../MessageCard'; import { MessageSkeletonCard } from '../MessageSkeletonCard'; import MessageTextField from '../MessageTextField'; +import { useJustWeb3 } from '@justweb3/widget'; export interface ChatProps { conversation: CachedConversation; @@ -45,6 +46,7 @@ export interface ChatProps { } export const Chat: React.FC = ({ conversation, onBack }) => { + const { openEnsProfile } = useJustWeb3(); const [replyMessage, setReplyMessage] = React.useState(null); const [reactionMessage, setReactionMessage] = @@ -125,7 +127,7 @@ export const Chat: React.FC = ({ conversation, onBack }) => { await refreshConsentList(); await allow([conversation.peerAddress]); void refreshConsentList(); - setIsRequest(false) + setIsRequest(false); setIsRequestChangeLoading(false); }; @@ -180,9 +182,17 @@ export const Chat: React.FC = ({ conversation, onBack }) => { additionalHeight.push('59px'); } - return `calc( ${height} ${additionalHeight.length > 0 ? ' - ' + additionalHeight.join(' - ') : '' - } )`; - }, [replyMessage, isMessagesSenderOnly, isStringContent, mimeType, type, isRequest]); + return `calc( ${height} ${ + additionalHeight.length > 0 ? ' - ' + additionalHeight.join(' - ') : '' + } )`; + }, [ + replyMessage, + isMessagesSenderOnly, + isStringContent, + mimeType, + type, + isRequest, + ]); return ( = ({ conversation, onBack }) => { }} /> - + { + if (primaryName) { + openEnsProfile(primaryName); + } + }} + > = ({ conversation, onBack }) => { - + = ({ conversation, onBack }) => { }} /> - = ({ conversation, onBack }) => { style={{ cursor: 'pointer', }} - fill='var(--justweb3-background-color)' + fill="var(--justweb3-background-color)" />

[]; handleOpenChat: ( conversation: CachedConversation | null ) => void; - blockedList?: boolean + blockedList?: boolean; } export const ChatList: React.FC = ({ conversations, handleOpenChat, - blockedList + blockedList, }) => { + const { allPrimaryNames } = usePrimaryNameBatch({ + addresses: conversations.map((conversation) => conversation.peerAddress), + }); + return ( - + {conversations.map((conversation) => ( handleOpenChat(conversation)} key={conversation.topic} 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 54a37098..780480f3 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx @@ -7,7 +7,7 @@ import { Tabs, TabsContent, TabsList, - TabsTrigger + TabsTrigger, } from '@justweb3/ui'; import { CachedConversation, @@ -33,12 +33,12 @@ export const ChatSheet: React.FC = ({ open, handleOpen, handleOpenChat, - handleNewChat + 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( @@ -78,20 +78,27 @@ export const ChatSheet: React.FC = ({ Chats - + + width={35} + height={35} + /> = ({ Requests {requestConversations.length > 0 && ( = ({ height: 17, borderRadius: '50%', lineHeight: 0.5, - fontSize: '10px' + fontSize: '10px', }} - >{requestConversations.length} + > + {requestConversations.length} + )} = ({

Loading...
) : ( <> - + - + - + = ({ + ens, + env, +}) => { + const { closeEnsProfile } = useJustWeb3(); + const { handleOpenChatWithAddressOrEns } = useJustWeb3XMTP(); + const { records } = useRecords({ + ens, + chainId: 1, + }); + const [canMessageAddress, setCanMessageAddress] = useState( + null + ); + + console.log('records', records, canMessageAddress); + const { canMessage } = useCanMessage(); + + useEffect(() => { + if (canMessageAddress === null) { + if (records) { + if (records?.sanitizedRecords?.ethAddress) { + console.log( + 'records?.sanitizedRecords?.ethAddress?.value', + records?.sanitizedRecords?.ethAddress?.value + ); + Client.canMessage(records?.sanitizedRecords?.ethAddress?.value, { + env: 'production', + }).then((canMessage) => { + console.log('canMessage', canMessage); + setCanMessageAddress(canMessage); + }); + } + } + } + }, [canMessage, canMessageAddress, records]); + + return ( +
+ { + if (!canMessageAddress) { + return; + } + closeEnsProfile(); + handleOpenChatWithAddressOrEns(ens); + }} + > + + +
+ ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx index 0babf793..e7abb66d 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx @@ -8,7 +8,7 @@ import { useLastMessage, useStreamMessages, } from '@xmtp/react-sdk'; -import { useEnsAvatar, usePrimaryName, useRecords } from '@justaname.id/react'; +import { useEnsAvatar, useRecords } from '@justaname.id/react'; import { Avatar, Button, Flex, formatText, P, SPAN } from '@justweb3/ui'; import React, { useMemo } from 'react'; import { formatChatDate } from '../../utils/formatChatDate'; @@ -17,18 +17,20 @@ export interface MessageItemProps { conversation: CachedConversation; onClick?: () => void; blocked?: boolean; + primaryName?: string; } export const MessageItem: React.FC = ({ conversation, onClick, blocked, + primaryName, }) => { const lastMessage = useLastMessage(conversation.topic); useStreamMessages(conversation); - const { primaryName } = usePrimaryName({ - address: conversation.peerAddress as `0x${string}`, - }); + // const { primaryName } = usePrimaryName({ + // address: conversation.peerAddress as `0x${string}`, + // }); const { records } = useRecords({ ens: primaryName, }); @@ -89,10 +91,10 @@ export const MessageItem: React.FC = ({ src={ primaryName ? sanitizeEnsImage({ - name: primaryName, - chainId: 1, - image: records?.sanitizedRecords?.avatar, - }) + name: primaryName, + chainId: 1, + image: records?.sanitizedRecords?.avatar, + }) : undefined } /> @@ -101,7 +103,9 @@ export const MessageItem: React.FC = ({ direction={'column'} style={{ marginLeft: '10px', - maxWidth: 'calc(100% - 50px - 32px - 10px)', + maxWidth: blocked + ? 'calc(100% - 120px)' + : 'calc(100% - 50px - 32px - 10px)', justifyContent: 'space-between', }} > 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 5a5954ad..40d89c38 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx @@ -1,300 +1,320 @@ 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, CloseIcon, Flex, Input, LoadingSpinner, P, VerificationsIcon } from '@justweb3/ui'; - +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 - ); - - const isAddressName = useMemo(() => { - const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/; - return ( - !ethAddressRegex.test(debouncedAddress) && - debouncedAddress.length > 0 - ); - }, [debouncedAddress]); + // 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, + isDebouncing: isDebouncingAddress, + } = useDebounce(newAddress, 500); + 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 { records, isRecordsLoading, isRecordsFetching } = useRecords({ + ens: debouncedAddress, + enabled: isAddressName, + }); + const { + primaryName: name, + isPrimaryNameLoading, + isPrimaryNameFetching, + } = usePrimaryName({ + address: debouncedAddress as `0x${string}`, + enabled: !isAddressName, + }); - const resolvedAddress = useMemo(() => { - return records?.sanitizedRecords?.ethAddress?.value; - }, [records]); + console.log(records); + 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); + 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); + } + }; - useEffect(() => { - if (debouncedAddress.length === 0) { - setCanMessage(false); - return; - } - if (isAddressName) { - if (!isRecordsLoading && resolvedAddress) { - handleCanMessage(); - } + 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 { - if (!isLoading && !isPrimaryNameLoading) { - handleCanMessage(); - } + const cachedConvo = toCachedConversation( + conv.conversation, + address ?? '' + ); + await allow([conv.conversation.peerAddress]); + await refreshConsentList(); + onChatStarted(cachedConvo); + onBack(); } - }, [ - debouncedAddress, - isLoading, - isPrimaryNameLoading, - isRecordsLoading, - resolvedAddress, - isAddressName - ]); - + } 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); + } + }; - useEffect(() => { - const checkConversation = async () => { - if (canMessage) { - await checkIfConversationExists( - isAddressName && resolvedAddress ? resolvedAddress : debouncedAddress - ); - } - }; + const checkIfConversationExists = async (peerAddress: string) => { + const convoExists = await getCachedByPeerAddress(peerAddress); + if (convoExists) { + onChatStarted(convoExists); + } + }; - checkConversation(); - }, [canMessage, resolvedAddress, debouncedAddress]); + useEffect(() => { + if (debouncedAddress.length === 0) { + setCanMessage(false); + return; + } + if (isAddressName) { + if (!isRecordsLoading && resolvedAddress) { + handleCanMessage(); + } + } else { + if (!isLoading && !isPrimaryNameLoading) { + handleCanMessage(); + } + } + }, [ + debouncedAddress, + isLoading, + isPrimaryNameLoading, + isRecordsLoading, + resolvedAddress, + isAddressName, + ]); - - const isSearchLoading = useMemo(() => { - return ( - (isAddressName && isPrimaryNameLoading && debouncedAddress.length > 4) || - isLoading + useEffect(() => { + const checkConversation = async () => { + if (canMessage) { + await checkIfConversationExists( + isAddressName && resolvedAddress ? resolvedAddress : debouncedAddress ); - }, [isLoading, debouncedAddress, resolvedAddress, isAddressName]); + } + }; + checkConversation(); + }, [ + canMessage, + resolvedAddress, + debouncedAddress, + checkIfConversationExists, + isAddressName, + ]); - useEffect(() => { - if (isRecordsLoading || isPrimaryNameLoading) { - return; - } + const isSearchLoading = useMemo(() => { + return ( + // (isAddressName && isPrimaryNameLoading && debouncedAddress.length > 4) || + // isLoading + isDebouncingAddress || isRecordsFetching || isPrimaryNameFetching + ); + }, [isDebouncingAddress, isRecordsFetching, isPrimaryNameFetching]); + // }, [isAddressName, isPrimaryNameLoading, debouncedAddress.length, isLoading]); - if (name) { - setNewAddress(name); - return; - } - }, [name, isRecordsLoading, isPrimaryNameLoading]); + useEffect(() => { + if (isRecordsLoading || isPrimaryNameLoading) { + return; + } - return ( + if (name) { + setNewAddress(name); + return; + } + }, [name, isRecordsLoading, isPrimaryNameLoading]); + + return ( + + + - - - - - -

- 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, - }} - /> -
- + + +

- + 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, + }} + /> +
+ + + + + ); }; export default NewConversation; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx index 33213d9e..69a14e41 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx @@ -4,25 +4,33 @@ import NewConversation from '../NewConversation'; export interface MessageSheetProps { openNewChat: boolean; - handleOpenNewChat: ( - open: boolean - ) => void; + handleOpenNewChat: (open: boolean) => void; onChatStarted: (conversation: CachedConversation) => void; + addressOrEns?: string; } export const NewMessageSheet: React.FC = ({ handleOpenNewChat, openNewChat, - onChatStarted + onChatStarted, + addressOrEns, }) => { return ( !open && handleOpenNewChat(false)} > - + New Conversation - handleOpenNewChat(false)} /> + handleOpenNewChat(false)} + selectedAddress={addressOrEns} + /> ); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx index fd6a6032..8f826f08 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx @@ -1,11 +1,12 @@ import { JustaPlugin } from '@justweb3/widget'; import { JustWeb3XMTPProvider } from '../providers/JustWeb3XMTPProvider'; import { ChatButton } from '../components/ChatButton'; +import { ChatWithProfileButton } from '../components/ChatWithProfileButton'; export type XmtpEnvironment = 'local' | 'production' | 'dev'; export const XMTPPlugin = (env: XmtpEnvironment): JustaPlugin => { - return ({ + return { name: 'XMTPPlugin', components: { Provider: (pluginApi, children) => { @@ -18,6 +19,9 @@ export const XMTPPlugin = (env: XmtpEnvironment): JustaPlugin => { ); }, + ProfileHeader: (pluginApi, ens, chainId, address) => { + return ; + }, SignInMenu: (pluginApi) => { return ( { /> ); }, - } - }) + }, + }; }; 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 f7824e73..a8b0297d 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx @@ -19,7 +19,9 @@ const contentTypeConfigs = [ replyContentTypeConfig, ]; -interface JustWeb3XMTPContextProps { } +interface JustWeb3XMTPContextProps { + handleOpenChatWithAddressOrEns: (addressOrEns: string) => void; +} const JustWeb3XMTPContext = React.createContext< JustWeb3XMTPContextProps | undefined @@ -38,6 +40,8 @@ export const JustWeb3XMTPProvider: React.FC = ({ }) => { const [isXmtpEnabled, setIsXmtpEnabled] = React.useState(false); const [isNewChat, setIsNewChat] = React.useState(false); + const [isNewChatWithAddressOrEns, setIsNewChatWithAddressOrEns] = + React.useState(''); const [conversation, setConversation] = React.useState | null>(null); const handleXmtpEnabled = (enabled: boolean) => { @@ -56,11 +60,20 @@ export const JustWeb3XMTPProvider: React.FC = ({ const handleNewChat = () => { setIsNewChat(true); - } + }; + + const handleNewChatWithAddressOrEns = (addressOrEns: string) => { + setIsNewChat(true); + setIsNewChatWithAddressOrEns(addressOrEns); + }; return ( - + = ({ openNewChat={isNewChat} handleOpenNewChat={handleOpenNewChat} onChatStarted={handleOpenChat} + addressOrEns={isNewChatWithAddressOrEns} /> {isXmtpEnabled && ( = ({ open, handleXmtpEnabled }) => { return null; }; + +export const useJustWeb3XMTP = () => { + const context = React.useContext(JustWeb3XMTPContext); + if (context === undefined) { + throw new Error( + 'useJustWeb3XMTP must be used within a JustWeb3XMTPProvider' + ); + } + return context; +}; diff --git a/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx b/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx index f69bc4e4..097feea4 100644 --- a/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx +++ b/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx @@ -20,7 +20,6 @@ import { XMTPPlugin } from '../lib'; import { EFPPlugin } from '@justweb3/efp-plugin'; import { TalentProtocolPlugin } from '@justweb3/talent-protocol-plugin'; import { POAPPlugin } from '@justweb3/poap-plugin'; -import { JustVerifiedPlugin } from '@justverified/plugin'; const queryClient = new QueryClient(); @@ -51,7 +50,7 @@ const JustWeb3Config: JustWeb3ProviderConfig = { // // allowedEns: 'all', // // dev: import.meta.env.STORYBOOK_APP_DEV === 'true', plugins: [ - XMTPPlugin, + XMTPPlugin('production'), EFPPlugin, POAPPlugin({ apiKey: import.meta.env.STORYBOOK_APP_POAP_KEY, @@ -59,12 +58,12 @@ const JustWeb3Config: JustWeb3ProviderConfig = { TalentProtocolPlugin({ apiKey: import.meta.env.STORYBOOK_APP_TALENT_PROTOCOL_API_KEY, }), - JustVerifiedPlugin(['email', 'telegram', 'twitter', 'discord']), + // JustVerifiedPlugin(['email', 'telegram', 'twitter', 'discord']), ], - color: { - primary: '#FF00FF', - background: '#000000', - }, + // color: { + // primary: '#FF00FF', + // background: '#000000', + // }, }; export const Example = () => {