Skip to content

Commit

Permalink
Merge pull request #111 from xmtp-labs/ar/replies
Browse files Browse the repository at this point in the history
feat: Replies
  • Loading branch information
alexrisch authored May 30, 2024
2 parents 1a772d9 + e23cedf commit d003e33
Show file tree
Hide file tree
Showing 13 changed files with 437 additions and 104 deletions.
48 changes: 44 additions & 4 deletions src/components/ConversationInput.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
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,
launchCamera,
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<SupportedContentTypes>;
clearReply?: () => void;
}

export const ConversationInput: FC<ConversationInputProps> = ({
sendMessage,
currentAddress,
id,
replyMessage,
clearReply,
}) => {
const [focused, setFocus] = useState<boolean>(false);
const [text, setText] = useState<string>(
Expand All @@ -35,6 +48,13 @@ export const ConversationInput: FC<ConversationInputProps> = ({

const textInputRef = React.createRef<TextInput>();

const replyContent = useMemo(() => {
if (!replyMessage) {
return undefined;
}
return getContentFromMessage(replyMessage);
}, [replyMessage]);

useEffect(() => {
if (text && currentAddress && id) {
mmkvStorage.saveDraftText(currentAddress, id, text);
Expand Down Expand Up @@ -81,13 +101,33 @@ export const ConversationInput: FC<ConversationInputProps> = ({
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 (
<VStack flexShrink={1}>
{replyContent && (
<HStack marginX={2}>
<Text typography="text-sm/regular" color={colors.textSecondary}>
{translate('conversation_replying_to', {
name: getSenderNameFromMessage(replyMessage),
})}
</Text>
<Text
paddingLeft={1}
numberOfLines={1}
ellipsizeMode="tail"
typography="text-sm/regular"
color={colors.textSecondary}>
{replyContent}
</Text>
<Pressable onPress={clearReply}>
<Icon name="x-circle" size={20} color={colors.textSecondary} />
</Pressable>
</HStack>
)}
<HStack
marginX={2}
alignItems={'flex-end'}
Expand Down
14 changes: 2 additions & 12 deletions src/components/GroupListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {useTypedNavigation} from '../hooks/useTypedNavigation';
import {ScreenNames} from '../navigation/ScreenNames';
import {useFirstGroupMessageQuery} from '../queries/useFirstGroupMessageQuery';
import {useGroupParticipantsQuery} from '../queries/useGroupParticipantsQuery';
import {getContentFromMessage} from '../utils/getContentFromMessage';
import {getMessageTimeDisplay} from '../utils/getMessageTimeDisplay';
import {GroupAvatarStack} from './GroupAvatarStack';
import {Text} from './common/Text';
Expand All @@ -34,18 +35,7 @@ export const GroupListItem: FC<GroupListItemProps> = ({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(() => {
Expand Down
12 changes: 12 additions & 0 deletions src/components/messageContent/ConversationMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -128,7 +129,18 @@ export const ConversationMessageContent: FC<
</Container>
);
}
if (message.contentTypeId === ContentTypes.Reply) {
return (
<MessageOptionsContainer
reactions={reacts}
isMe={isMe}
messageId={message.id}>
<ReplyMessageContent message={message} isMe={isMe} />
</MessageOptionsContainer>
);
}

console.log('Unsupported content type', message.contentTypeId);
// TODO: Add support for other content types
return null;
};
2 changes: 2 additions & 0 deletions src/components/messageContent/MessageOptionsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ export const MessageOptionsContainer: FC<
);

const handleReplyPress = useCallback(() => {
Haptic.trigger('contextClick');
setReplyId(messageId);
setShown(false);
}, [setReplyId, messageId]);

const handleRemoveReplyPress = useCallback(
Expand Down
88 changes: 88 additions & 0 deletions src/components/messageContent/ReplyMessageContent.tsx
Original file line number Diff line number Diff line change
@@ -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<SupportedContentTypes>;
isMe: boolean;
}

export const ReplyMessageContent = ({
message,
isMe,
}: ReplyMessageContentProps) => {
const {scrollToMessage} = useContext(GroupContext);

const reply = (
message as unknown as DecodedMessage<[ReplyCodec]>
).content() as ReplyContent<SupportedContentTypes>;

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 (
<Container
backgroundColor={
isMe ? colors.actionPrimary : colors.backgroundSecondary
}
alignSelf={isMe ? 'flex-end' : 'flex-start'}
borderRadius={'16px'}
borderBottomRightRadius={isMe ? 0 : '16px'}
borderTopLeftRadius={isMe ? '16px' : 0}
paddingY={3}
paddingX={5}>
<Text
typography="text-base/medium"
color={isMe ? colors.actionPrimaryText : colors.textPrimary}>
{textContent.text}
</Text>
</Container>
);
case ContentTypes.RemoteStaticAttachment:
const remoteAttachmentContent =
reply.content as unknown as RemoteAttachmentContent;
return <ImageMessage content={remoteAttachmentContent} />;
default:
return null;
}
}, [isMe, reply]);

const handlePress = useCallback(() => {
if (reply.reference) {
scrollToMessage(reply.reference);
}
}, [scrollToMessage, reply.reference]);

if (!reply) {
return null;
}
return (
<Container
borderRadius={'16px'}
borderBottomRightRadius={isMe ? 0 : '16px'}
borderTopLeftRadius={isMe ? '16px' : 0}>
<Pressable onPress={handlePress}>
<Text typography="text-xs/regular" color={colors.textSecondary}>
{translate('replied_to')}
</Text>
</Pressable>
{content}
</Container>
);
};
4 changes: 4 additions & 0 deletions src/context/GroupContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface GroupContextValue {
group: Group<SupportedContentTypes> | null;
setReplyId: (id: string) => void;
clearReplyId: () => void;
scrollToMessage: (id: string) => void;
}

export const GroupContext = createContext<GroupContextValue>({
Expand All @@ -16,4 +17,7 @@ export const GroupContext = createContext<GroupContextValue>({
clearReplyId: () => {
throw new Error('Not Implemented');
},
scrollToMessage: () => {
throw new Error('Not Implemented');
},
});
5 changes: 4 additions & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
62 changes: 40 additions & 22 deletions src/i18n/locales/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"notifications": "การแจ้งเตือน",
"privacy": "ความเป็นส่วนตัว",
"support": "สนับสนุน",
"clear_local_data": "ล้างข้อมูลในเครื่อง",
"disconnect_wallet": "ยกเลิกการเชื่อมต่อกระเป๋าเงิน",
"clear_local_data": "ลบข้อมูลในเครื่อง",
"disconnect_wallet": "ตัดการเชื่อมต่อกระเป๋าเงิน",
"connect_existing_wallet": "เชื่อมต่อกระเป๋าเงินที่มีอยู่",
"create_new_wallet": "สร้างกระเป๋าเงินใหม่",
"you": "คุณ",
Expand All @@ -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": "ตอบกลับข้อความก่อนหน้า"
}
Loading

0 comments on commit d003e33

Please sign in to comment.