From ea938e382acb5a207c00b9235dd3af7feecab3cc Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Thu, 30 May 2024 13:22:01 -0600 Subject: [PATCH 1/2] feat: Replies Added handling for replies --- src/components/ConversationInput.tsx | 48 +++++- src/components/GroupListItem.tsx | 14 +- .../ConversationMessageContent.tsx | 12 ++ .../MessageOptionsContainer.tsx | 2 + .../messageContent/ReplyMessageContent.tsx | 88 ++++++++++ src/context/GroupContext.tsx | 4 + src/i18n/locales/en.json | 5 +- src/i18n/locales/th.json | 62 ++++--- src/screens/GroupScreen.tsx | 151 ++++++++++-------- src/utils/getContentFromMessage.test.ts | 64 ++++++++ src/utils/getContentFromMessage.ts | 22 +++ src/utils/getSenderNameFromMessage.test.ts | 53 ++++++ src/utils/getSenderNameFromMessage.ts | 16 ++ 13 files changed, 437 insertions(+), 104 deletions(-) create mode 100644 src/components/messageContent/ReplyMessageContent.tsx create mode 100644 src/utils/getContentFromMessage.test.ts create mode 100644 src/utils/getContentFromMessage.ts create mode 100644 src/utils/getSenderNameFromMessage.test.ts create mode 100644 src/utils/getSenderNameFromMessage.ts diff --git a/src/components/ConversationInput.tsx b/src/components/ConversationInput.tsx index a84eb54..286160a 100644 --- a/src/components/ConversationInput.tsx +++ b/src/components/ConversationInput.tsx @@ -1,5 +1,6 @@ +import {DecodedMessage} from '@xmtp/react-native-sdk'; import {Box, HStack, Image, Pressable, VStack} from 'native-base'; -import React, {FC, useCallback, useEffect, useState} from 'react'; +import React, {FC, useCallback, useEffect, useMemo, useState} from 'react'; import {Platform, StyleSheet, TextInput} from 'react-native'; import { Asset, @@ -7,20 +8,32 @@ import { launchImageLibrary, } from 'react-native-image-picker'; import {Icon} from '../components/common/Icon'; +import {SupportedContentTypes} from '../consts/ContentTypes'; import {translate} from '../i18n'; import {mmkvStorage} from '../services/mmkvStorage'; import {colors} from '../theme/colors'; +import {getContentFromMessage} from '../utils/getContentFromMessage'; +import {getSenderNameFromMessage} from '../utils/getSenderNameFromMessage'; +import {Text} from './common/Text'; interface ConversationInputProps { - sendMessage: (payload: {text?: string; asset?: Asset}) => void; + sendMessage: (payload: { + text?: string; + asset?: Asset; + replyId?: string; + }) => void; currentAddress?: string; id?: string; + replyMessage?: DecodedMessage; + clearReply: () => void; } export const ConversationInput: FC = ({ sendMessage, currentAddress, id, + replyMessage, + clearReply, }) => { const [focused, setFocus] = useState(false); const [text, setText] = useState( @@ -35,6 +48,13 @@ export const ConversationInput: FC = ({ const textInputRef = React.createRef(); + const replyContent = useMemo(() => { + if (!replyMessage) { + return undefined; + } + return getContentFromMessage(replyMessage); + }, [replyMessage]); + useEffect(() => { if (text && currentAddress && id) { mmkvStorage.saveDraftText(currentAddress, id, text); @@ -81,13 +101,33 @@ export const ConversationInput: FC = ({ if (!canSend) { return; } - sendMessage({text, asset: asset ?? undefined}); + sendMessage({text, asset: asset ?? undefined, replyId: replyMessage?.id}); setText(''); setAssetUri(null); - }, [sendMessage, text, asset, canSend]); + }, [canSend, replyMessage, sendMessage, text, asset]); return ( + {replyContent && ( + + + {translate('conversation_replying_to', { + name: getSenderNameFromMessage(replyMessage), + })} + + + {replyContent} + + + + + + )} = ({group}) => { if (!firstMessage) { return ''; } - let text = ''; - try { - const content = firstMessage.content(); - if (typeof content === 'string') { - text = content; - } else { - text = firstMessage.fallback ?? ''; - } - } catch (e) { - text = firstMessage.fallback ?? ''; - } - return text; + return getContentFromMessage(firstMessage); }, [firstMessage]); const lastMessageTime: number | undefined = useMemo(() => { diff --git a/src/components/messageContent/ConversationMessageContent.tsx b/src/components/messageContent/ConversationMessageContent.tsx index 1b3f8f5..2b96e51 100644 --- a/src/components/messageContent/ConversationMessageContent.tsx +++ b/src/components/messageContent/ConversationMessageContent.tsx @@ -13,6 +13,7 @@ import {formatAddress} from '../../utils/formatAddress'; import {ImageMessage} from '../ImageMessage'; import {Text} from '../common/Text'; import {MessageOptionsContainer} from './MessageOptionsContainer'; +import {ReplyMessageContent} from './ReplyMessageContent'; import {TextMessageContent} from './TextMessageContent'; interface ConversationMessageContentProps { @@ -128,7 +129,18 @@ export const ConversationMessageContent: FC< ); } + if (message.contentTypeId === ContentTypes.Reply) { + return ( + + + + ); + } + console.log('Unsupported content type', message.contentTypeId); // TODO: Add support for other content types return null; }; diff --git a/src/components/messageContent/MessageOptionsContainer.tsx b/src/components/messageContent/MessageOptionsContainer.tsx index 7272e72..e865930 100644 --- a/src/components/messageContent/MessageOptionsContainer.tsx +++ b/src/components/messageContent/MessageOptionsContainer.tsx @@ -42,7 +42,9 @@ export const MessageOptionsContainer: FC< ); const handleReplyPress = useCallback(() => { + Haptic.trigger('contextClick'); setReplyId(messageId); + setShown(false); }, [setReplyId, messageId]); const handleRemoveReplyPress = useCallback( diff --git a/src/components/messageContent/ReplyMessageContent.tsx b/src/components/messageContent/ReplyMessageContent.tsx new file mode 100644 index 0000000..013bb6a --- /dev/null +++ b/src/components/messageContent/ReplyMessageContent.tsx @@ -0,0 +1,88 @@ +import { + DecodedMessage, + RemoteAttachmentContent, + ReplyCodec, +} from '@xmtp/react-native-sdk'; +import {ReplyContent} from '@xmtp/react-native-sdk/build/lib/NativeCodecs/ReplyCodec'; +import {Container} from 'native-base'; +import React, {useCallback, useContext, useMemo} from 'react'; +import {Pressable} from 'react-native'; +import {ContentTypes, SupportedContentTypes} from '../../consts/ContentTypes'; +import {GroupContext} from '../../context/GroupContext'; +import {translate} from '../../i18n'; +import {colors} from '../../theme/colors'; +import {Text} from '../common/Text'; +import {ImageMessage} from '../ImageMessage'; + +interface ReplyMessageContentProps { + message: DecodedMessage; + isMe: boolean; +} + +export const ReplyMessageContent = ({ + message, + isMe, +}: ReplyMessageContentProps) => { + const {scrollToMessage} = useContext(GroupContext); + + const reply = ( + message as unknown as DecodedMessage<[ReplyCodec]> + ).content() as ReplyContent; + + const content = useMemo(() => { + if (typeof reply === 'string' || typeof reply.content === 'string') { + return reply; + } + switch (reply.contentType) { + case ContentTypes.Text: + const textContent = reply.content as unknown as {text: string}; + return ( + + + {textContent.text} + + + ); + case ContentTypes.RemoteStaticAttachment: + const remoteAttachmentContent = + reply.content as unknown as RemoteAttachmentContent; + return ; + default: + return null; + } + }, [isMe, reply]); + + const handlePress = useCallback(() => { + if (reply.reference) { + scrollToMessage(reply.reference); + } + }, [scrollToMessage, reply.reference]); + + if (!reply) { + return null; + } + return ( + + + + {translate('replied_to')} + + + {content} + + ); +}; diff --git a/src/context/GroupContext.tsx b/src/context/GroupContext.tsx index 4091f36..d9c52e8 100644 --- a/src/context/GroupContext.tsx +++ b/src/context/GroupContext.tsx @@ -6,6 +6,7 @@ export interface GroupContextValue { group: Group | null; setReplyId: (id: string) => void; clearReplyId: () => void; + scrollToMessage: (id: string) => void; } export const GroupContext = createContext({ @@ -16,4 +17,7 @@ export const GroupContext = createContext({ clearReplyId: () => { throw new Error('Not Implemented'); }, + scrollToMessage: () => { + throw new Error('Not Implemented'); + }, }); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7f58fb2..f3435c0 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -78,5 +78,8 @@ "group_remove_plural": "%{initiatedByAddress} removed %{addressCount} members", "error_group_remove": "An error occurred removing from group", "error_group_adding": "An error occurred adding to group", - "reply": "Reply" + "reply": "Reply", + + "conversation_replying_to": "Replying to %{name}", + "replied_to": "Replied to an earlier message" } diff --git a/src/i18n/locales/th.json b/src/i18n/locales/th.json index 13cdc1c..40f80ba 100644 --- a/src/i18n/locales/th.json +++ b/src/i18n/locales/th.json @@ -2,8 +2,8 @@ "notifications": "การแจ้งเตือน", "privacy": "ความเป็นส่วนตัว", "support": "สนับสนุน", - "clear_local_data": "ล้างข้อมูลในเครื่อง", - "disconnect_wallet": "ยกเลิกการเชื่อมต่อกระเป๋าเงิน", + "clear_local_data": "ลบข้อมูลในเครื่อง", + "disconnect_wallet": "ตัดการเชื่อมต่อกระเป๋าเงิน", "connect_existing_wallet": "เชื่อมต่อกระเป๋าเงินที่มีอยู่", "create_new_wallet": "สร้างกระเป๋าเงินใหม่", "you": "คุณ", @@ -15,53 +15,71 @@ "all_messages": "ข้อความทั้งหมด", "message_requests": "คำขอข้อความ", "message_requests_count": "%{count} คำขอข้อความ", - "this_address_has_never_sent_you": "ที่อยู่นี้ไม่เคยส่งข้อความให้คุณมาก่อน คุณต้องการอนุญาตให้พวกเขาส่งข้อความให้คุณหรือไม่?", + "this_address_has_never_sent_you": "ที่อยู่นี้ไม่เคยส่งข้อความถึงคุณมาก่อน คุณต้องการอนุญาตให้พวกเขาส่งข้อความถึงคุณหรือไม่?", "no": "ไม่", "yes": "ใช่", - "start": "เริ่ม", + "start": "เริ่มต้น", "recents": "ล่าสุด", - "contacts": "รายชื่อติดต่อ", + "contacts": "รายชื่อผู้ติดต่อ", - "your_interoperable_web3_inbox": "กล่องข้อความ web3 ที่ใช้งานร่วมกันได้ของคุณ", - "youre_just_a_few_steps_away_from_secure_wallet_to_wallet_messaging": "คุณอยู่ห่างจากการส่งข้อความจากกระเป๋าเงินไปยังกระเป๋าเงินอย่างปลอดภัยเพียงไม่กี่ขั้นตอน", + "your_interoperable_web3_inbox": "กล่องขาเข้าเว็บ3 ของคุณ", + "youre_just_a_few_steps_away_from_secure_wallet_to_wallet_messaging": "คุณเหลือเพียงไม่กี่ขั้นตอนจากการส่งข้อความจากกระเป๋าเงินถึงกระเป๋าเงินที่ปลอดภัย", "connect_your_wallet": "เชื่อมต่อกระเป๋าเงินของคุณ", - "you_can_connect_or_create": "คุณสามารถเชื่อมต่อหรือสร้างกระเป๋าเงินผ่าน Wallet Connect", + "you_can_connect_or_create": "คุณสามารถเชื่อมต่อหรือสร้างกระเป๋าเงินผ่าน Wallet Connect ได้", "whats_a_wallet": "กระเป๋าเงินคืออะไร?", + "no_private_keys_will_be_shared": "จะไม่มีการแบ่งปันกุญแจส่วนตัว", - "step_1_of_2": "ขั้นตอนที่ 1 จาก 2", + "step_1_of_2": "ขั้นตอน 1 จาก 2", "create_your_xmtp_identity": "สร้างตัวตน XMTP ของคุณ", - "now_that_your_wallet_is_connected": "ตอนนี้ที่กระเป๋าเงินของคุณได้เชื่อมต่อแล้ว เรากำลังจะสร้างตัวตน XMTP ของคุณบนเครือข่ายของเราด้วยลายเซ็นกระเป๋าเงิน", + "now_that_your_wallet_is_connected": "ตอนนี้ที่กระเป๋าเงินของคุณเชื่อมต่อแล้ว เราจะสร้างตัวตน XMTP ของคุณบนเครือข่ายของเราด้วยลายเซ็นกระเป๋าเงิน", "create_xmtp_identity": "สร้างตัวตน XMTP", - "step_2_of_2": "ขั้นตอนที่ 2 จาก 2", + "step_2_of_2": "ขั้นตอน 2 จาก 2", - "domain_origin": "ที่มาของโดเมน", + "domain_origin": "ต้นกำเนิดโดเมน", - "copy_your_code": "คัดลอกโค้ดของคุณ", + "copy_your_code": "คัดลอกรหัสของคุณ", "allow": "อนุญาต", "block": "บล็อก", - "yesterday": "เมื่อวาน", + "yesterday": "เมื่อวานนี้", - "conversation_image_alt": "รูปภาพที่แนบ", + "conversation_image_alt": "ภาพที่แนบมา", "walletconnect": "WalletConnect", "metamask": "Metamask", "coinbase_wallet": "Coinbase Wallet", - "guest_wallet": "กระเป๋าเงินแขก", + "guest_wallet": "Guest Wallet", "group_changed": "กลุ่มเปลี่ยนแปลง", "add_to_group": "เพิ่ม", "group": "กลุ่ม", "dev_screen": "หน้าจอพัฒนา", - "client": "ไคลเอนต์", + "client": "ลูกค้า", "address": "ที่อยู่", - "list_queries": "รายการคิวรี", - "conversation_messages_queries": "ข้อความสนทนา", - "group_messages_queries": "คิวรีข้อความกลุ่ม", - "group_members_queries": "คิวรีสมาชิกกลุ่ม", + "list_queries": "รายการคำถาม", + "conversation_messages_queries": "คำถามข้อความการสนทนา", + "group_messages_queries": "คำถามข้อความกลุ่ม", + "group_members_queries": "คำถามสมาชิกกลุ่ม", "get_conversations": "รับการสนทนา", "get_groups": "รับกลุ่ม", - "get_contacts": "รับรายชื่อติดต่อ" + "get_contacts": "รับรายชื่อผู้ติดต่อ", + "group_name": "ชื่อกลุ่ม", + + "valid_address": "ที่อยู่ Ethereum ที่ถูกต้อง", + "message_requests_from_new_addresses": "คำขอข้อความจากที่อยู่ที่คุณไม่เคยโต้ตอบด้วยจะแสดงที่นี่", + "not_on_xmtp_group": "ผู้ใช้ไม่สามารถเพิ่มในกลุ่มได้เพราะไม่ได้อยู่บนเครือข่าย XMTP Group", + + "wallet_error": "ข้อผิดพลาดกระเป๋าเงิน", + "group_add_single": "%{initiatedByAddress} เพิ่ม %{address}", + "group_add_plural": "%{initiatedByAddress} เพิ่มสมาชิก %{addressCount} คน", + "group_remove_single": "%{initiatedByAddress} ลบ %{address}", + "group_remove_plural": "%{initiatedByAddress} ลบสมาชิก %{addressCount} คน", + "error_group_remove": "เกิดข้อผิดพลาดในการลบออกจากกลุ่ม", + "error_group_adding": "เกิดข้อผิดพลาดในการเพิ่มในกลุ่ม", + "reply": "ตอบกลับ", + + "conversation_replying_to": "ตอบกลับถึง %{name}", + "replied_to": "ตอบกลับข้อความก่อนหน้า" } diff --git a/src/screens/GroupScreen.tsx b/src/screens/GroupScreen.tsx index 4f81585..8f39724 100644 --- a/src/screens/GroupScreen.tsx +++ b/src/screens/GroupScreen.tsx @@ -1,14 +1,19 @@ import {useFocusEffect, useRoute} from '@react-navigation/native'; import {RemoteAttachmentContent} from '@xmtp/react-native-sdk'; -import {Box, FlatList, HStack, VStack} from 'native-base'; +import {Box, FlatList, HStack} from 'native-base'; import React, {useCallback, useMemo, useState} from 'react'; -import {KeyboardAvoidingView, ListRenderItem, Platform} from 'react-native'; +import { + KeyboardAvoidingView, + ListRenderItem, + Platform, + FlatList as RNFlatList, + StyleSheet, +} from 'react-native'; import {Asset} from 'react-native-image-picker'; import {ConversationInput} from '../components/ConversationInput'; import {GroupHeader} from '../components/GroupHeader'; import {Message} from '../components/Message'; import {Button} from '../components/common/Button'; -import {Drawer} from '../components/common/Drawer'; import {Screen} from '../components/common/Screen'; import {Text} from '../components/common/Text'; import {AddGroupParticipantModal} from '../components/modals/AddGroupParticipantModal'; @@ -51,8 +56,8 @@ export const GroupScreen = () => { const [showGroupModal, setShowGroupModal] = useState(false); const [showAddModal, setShowAddModal] = useState(false); const [replyId, setReplyId] = useState(null); - const [reactId, setReactId] = useState(null); const {consent, allow, deny} = useGroupConsent(topic); + const scrollRef = React.useRef>(null); const {ids, entities, reactionsEntities} = messages ?? {}; @@ -62,37 +67,80 @@ export const GroupScreen = () => { }, [refetch]), ); + const scrollToMessage = useCallback( + (messageId: string) => { + scrollRef.current?.scrollToItem({ + animated: false, + item: messageId, + }); + }, + [scrollRef], + ); + + const clearReply = useCallback(() => { + setReplyId(null); + }, [setReplyId]); + const sendMessage = useCallback( - async (payload: {text?: string; asset?: Asset}) => { + async (payload: {text?: string; asset?: Asset; replyId?: string}) => { if (!group) { return; } - if (payload.text) { - group?.send(payload.text); - } + const textContent = payload.text; + let remoteAttachment: RemoteAttachmentContent | undefined; + if (payload.asset) { - client - ?.encryptAttachment({ + try { + const encrypted = await client?.encryptAttachment({ fileUri: payload.asset.uri ?? '', mimeType: payload.asset.type, - }) - .then(encrypted => { - AWSHelper.uploadFile( - encrypted.encryptedLocalFileUri, - encrypted.metadata.filename ?? '', - ).then(response => { - const remote: RemoteAttachmentContent = { - ...encrypted.metadata, - scheme: 'https://', - url: response, - }; - group?.send({remoteAttachment: remote}); - }); - }) - .catch(() => {}); + }); + if (!encrypted) { + return; + } + const response = await AWSHelper.uploadFile( + encrypted.encryptedLocalFileUri, + encrypted.metadata.filename ?? '', + ); + remoteAttachment = { + ...encrypted.metadata, + scheme: 'https://', + url: response, + }; + } catch (e) { + console.error(e); + } + } + if (replyId) { + if (remoteAttachment) { + group?.send({ + reply: { + reference: replyId, + content: { + remoteAttachment, + }, + } as any, + }); + } else if (textContent) { + group?.send({ + reply: { + reference: replyId, + content: { + text: textContent, + }, + } as any, + }); + } + clearReply(); + } else if (remoteAttachment) { + group?.send({ + remoteAttachment, + }); + } else if (textContent) { + group?.send(textContent); } }, - [client, group], + [clearReply, client, group, replyId], ); const renderItem: ListRenderItem = ({item}) => { @@ -115,21 +163,14 @@ export const GroupScreen = () => { [setReplyId], ); - const clearReply = useCallback(() => { - setReplyId(null); - }, [setReplyId]); - - const clearReaction = useCallback(() => { - setReactId(null); - }, [setReactId]); - const conversationProviderValue = useMemo((): GroupContextValue => { return { group: group ?? null, setReplyId: setReply, clearReplyId: clearReply, + scrollToMessage, }; - }, [group, setReply, clearReply]); + }, [group, setReply, clearReply, scrollToMessage]); return ( @@ -146,10 +187,11 @@ export const GroupScreen = () => { /> { sendMessage={sendMessage} currentAddress={myAddress} id={topic} + replyMessage={replyId ? entities?.[replyId] : undefined} + clearReply={clearReply} /> ) : ( @@ -186,35 +230,6 @@ export const GroupScreen = () => { - - setReplyId(null)}> - - - Test - - - - - - - Test - - - setShowGroupModal(false)} @@ -234,3 +249,9 @@ export const GroupScreen = () => { ); }; + +const styles = StyleSheet.create({ + container: { + height: '100%', + }, +}); diff --git a/src/utils/getContentFromMessage.test.ts b/src/utils/getContentFromMessage.test.ts new file mode 100644 index 0000000..efe0c9e --- /dev/null +++ b/src/utils/getContentFromMessage.test.ts @@ -0,0 +1,64 @@ +import {DecodedMessage} from '@xmtp/react-native-sdk'; +import {SupportedContentTypes} from '../consts/ContentTypes'; +import {getContentFromMessage} from './getContentFromMessage'; + +describe('getContentFromMessage', () => { + it('should return an empty string if the message is null', () => { + // @ts-expect-error + expect(getContentFromMessage(null)).toBe(''); + }); + + it('should return an empty string if the message is undefined', () => { + // @ts-expect-error + expect(getContentFromMessage(undefined)).toBe(''); + }); + + it('should return the content if it is a string', () => { + const message = { + content: () => 'Hello, world!', + fallback: 'Fallback content', + } as DecodedMessage; + + expect(getContentFromMessage(message)).toBe('Hello, world!'); + }); + + it('should return the fallback if content is not a string', () => { + const message = { + content: () => ({text: 'Hello, world!'}), + fallback: 'Fallback content', + } as unknown as DecodedMessage; + + expect(getContentFromMessage(message)).toBe('Fallback content'); + }); + + it('should return an empty string if content is not a string and no fallback is provided', () => { + const message = { + content: () => ({text: 'Hello, world!'}), + fallback: undefined, + } as unknown as DecodedMessage; + + expect(getContentFromMessage(message)).toBe(''); + }); + + it('should return the fallback if an error is thrown', () => { + const message = { + content: () => { + throw new Error('Error fetching content'); + }, + fallback: 'Fallback content', + } as unknown as DecodedMessage; + + expect(getContentFromMessage(message)).toBe('Fallback content'); + }); + + it('should return an empty string if an error is thrown and no fallback is provided', () => { + const message = { + content: () => { + throw new Error('Error fetching content'); + }, + fallback: undefined, + } as unknown as DecodedMessage; + + expect(getContentFromMessage(message)).toBe(''); + }); +}); diff --git a/src/utils/getContentFromMessage.ts b/src/utils/getContentFromMessage.ts new file mode 100644 index 0000000..bd2f6da --- /dev/null +++ b/src/utils/getContentFromMessage.ts @@ -0,0 +1,22 @@ +import {DecodedMessage} from '@xmtp/react-native-sdk'; +import {SupportedContentTypes} from '../consts/ContentTypes'; + +export const getContentFromMessage = ( + message: DecodedMessage, +) => { + if (!message) { + return ''; + } + let text = ''; + try { + const content = message.content(); + if (typeof content === 'string') { + text = content; + } else { + text = message.fallback ?? ''; + } + } catch (e) { + text = message.fallback ?? ''; + } + return text; +}; diff --git a/src/utils/getSenderNameFromMessage.test.ts b/src/utils/getSenderNameFromMessage.test.ts new file mode 100644 index 0000000..940eb28 --- /dev/null +++ b/src/utils/getSenderNameFromMessage.test.ts @@ -0,0 +1,53 @@ +import {DecodedMessage} from '@xmtp/react-native-sdk'; +import {SupportedContentTypes} from '../consts/ContentTypes'; +import {mmkvStorage} from '../services/mmkvStorage'; +import {formatAddress} from './formatAddress'; +import {getSenderNameFromMessage} from './getSenderNameFromMessage'; + +// Mock the dependencies +jest.mock('../services/mmkvStorage', () => ({ + mmkvStorage: { + getEnsName: jest.fn(), + }, +})); + +jest.mock('./formatAddress', () => ({ + formatAddress: jest.fn(), +})); + +describe('getSenderNameFromMessage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return an empty string if message is undefined', () => { + expect(getSenderNameFromMessage()).toBe(''); + }); + + it('should return ENS name if available', () => { + const message: DecodedMessage = { + senderAddress: '0x1234567890abcdef', + } as DecodedMessage; + + // @ts-expect-error + mmkvStorage.getEnsName.mockReturnValue('ensName.eth'); + + expect(getSenderNameFromMessage(message)).toBe('ensName.eth'); + expect(mmkvStorage.getEnsName).toHaveBeenCalledWith('0x1234567890abcdef'); + }); + + it('should return formatted address if ENS name is not available', () => { + const message: DecodedMessage = { + senderAddress: '0x1234567890abcdef', + } as DecodedMessage; + + // @ts-expect-error + mmkvStorage.getEnsName.mockReturnValue(null); + // @ts-expect-error + formatAddress.mockReturnValue('0x1234...cdef'); + + expect(getSenderNameFromMessage(message)).toBe('0x1234...cdef'); + expect(mmkvStorage.getEnsName).toHaveBeenCalledWith('0x1234567890abcdef'); + expect(formatAddress).toHaveBeenCalledWith('0x1234567890abcdef'); + }); +}); diff --git a/src/utils/getSenderNameFromMessage.ts b/src/utils/getSenderNameFromMessage.ts new file mode 100644 index 0000000..e6a485c --- /dev/null +++ b/src/utils/getSenderNameFromMessage.ts @@ -0,0 +1,16 @@ +import {DecodedMessage} from '@xmtp/react-native-sdk'; +import {SupportedContentTypes} from '../consts/ContentTypes'; +import {mmkvStorage} from '../services/mmkvStorage'; +import {formatAddress} from './formatAddress'; + +export const getSenderNameFromMessage = ( + message?: DecodedMessage, +) => { + if (!message) { + return ''; + } + return ( + mmkvStorage.getEnsName(message.senderAddress) ?? + formatAddress(message.senderAddress) + ); +}; From e23cedf3b8e37f643fba1542929dafe101d66e68 Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Thu, 30 May 2024 13:32:48 -0600 Subject: [PATCH 2/2] fix tsc Fixed TSC --- src/components/ConversationInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ConversationInput.tsx b/src/components/ConversationInput.tsx index 286160a..5924bb0 100644 --- a/src/components/ConversationInput.tsx +++ b/src/components/ConversationInput.tsx @@ -25,7 +25,7 @@ interface ConversationInputProps { currentAddress?: string; id?: string; replyMessage?: DecodedMessage; - clearReply: () => void; + clearReply?: () => void; } export const ConversationInput: FC = ({